mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 13:03:00 +08:00
132 lines
4.6 KiB
Go
132 lines
4.6 KiB
Go
// Package srun is the SRun authentication protocol's byte layer.
|
|
//
|
|
// Everything here is a pure function over bytes. Nothing reads the clock,
|
|
// touches the network or opens a file: timestamps and callback names arrive as
|
|
// arguments. That is what makes the wire format testable against fixed vectors
|
|
// instead of against a campus gateway.
|
|
//
|
|
// The byte sequences are fixed by spec 04 and cross-checked two ways: vectors
|
|
// generated from the frozen baseline Python, and independent bases in the tests
|
|
// (RFC 2202/3174 digests, the standard Base64 alphabet, and an XXTEA decrypt
|
|
// written from the published algorithm that has to recover the plaintext).
|
|
package srun
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
|
)
|
|
|
|
// PadChar is the padding byte. SRun keeps standard Base64 padding even though
|
|
// the table is custom.
|
|
const PadChar = '='
|
|
|
|
// AlphabetSize is how many bytes a table has: one per six-bit group. Named so
|
|
// the configuration layer can bound the field without restating 64.
|
|
const AlphabetSize = 64
|
|
|
|
// DefaultAlphabetTable is the table the baseline shipped. A school may use a
|
|
// different one; the algorithm does not change with it.
|
|
const DefaultAlphabetTable = "LVoJPiCN2R8G90yg+hmFHuacZ1OWMnrsSTXkYpUq/3dlbfKwv6xztjI7DeBE45QA"
|
|
|
|
// StandardAlphabetTable is RFC 4648's table.
|
|
//
|
|
// It is here so the tests can run this encoder with it and compare against
|
|
// encoding/base64: an encoder that agrees with the standard library on the
|
|
// standard table, and with the baseline vectors on the SRun table, is doing
|
|
// Base64 rather than something that merely matches one set of expectations.
|
|
const StandardAlphabetTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
|
|
// Alphabet is a validated 64-byte Base64 table.
|
|
type Alphabet struct {
|
|
table [64]byte
|
|
}
|
|
|
|
// DefaultAlphabet is the baseline table, validated at startup.
|
|
var DefaultAlphabet = mustAlphabet(DefaultAlphabetTable)
|
|
|
|
func mustAlphabet(table string) *Alphabet {
|
|
alphabet, err := NewAlphabet(table)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
return alphabet
|
|
}
|
|
|
|
// NewAlphabet validates a table and returns an encoder for it.
|
|
//
|
|
// The rules are what makes the encoding reversible and the output usable in a
|
|
// query string: exactly 64 bytes, all distinct, all printable ASCII, and none
|
|
// of them the padding byte. A table with a repeat would map two different
|
|
// six-bit groups to the same character, so the gateway could not decode what it
|
|
// was sent -- and it would do so silently, which is why this is refused up
|
|
// front rather than discovered as an authentication failure.
|
|
func NewAlphabet(table string) (*Alphabet, error) {
|
|
if len(table) != AlphabetSize {
|
|
return nil, domain.Errorf(domain.CodeInvalidConfig,
|
|
"Base64 字母表必须是 %d 个字节,收到 %d 个", AlphabetSize, len(table))
|
|
}
|
|
|
|
var alphabet Alphabet
|
|
var seen [256]bool
|
|
for index := range AlphabetSize {
|
|
symbol := table[index]
|
|
switch {
|
|
case symbol < 0x21 || symbol > 0x7e:
|
|
return nil, domain.Errorf(domain.CodeInvalidConfig,
|
|
"Base64 字母表第 %d 位不是可见 ASCII 字符", index)
|
|
case symbol == PadChar:
|
|
return nil, domain.Errorf(domain.CodeInvalidConfig,
|
|
"Base64 字母表不能包含填充字符 %q", string(PadChar))
|
|
case seen[symbol]:
|
|
return nil, domain.Errorf(domain.CodeInvalidConfig,
|
|
"Base64 字母表中 %q 重复出现", string(symbol))
|
|
}
|
|
seen[symbol] = true
|
|
alphabet.table[index] = symbol
|
|
}
|
|
return &alphabet, nil
|
|
}
|
|
|
|
// Table returns the alphabet as a string.
|
|
func (a *Alphabet) Table() string { return string(a.table[:]) }
|
|
|
|
// Encode encodes src with this alphabet.
|
|
//
|
|
// Written out rather than delegated to encoding/base64 with a custom encoding
|
|
// because the two must be shown to agree, and using one to implement the other
|
|
// would make that comparison prove nothing.
|
|
func (a *Alphabet) Encode(src []byte) string {
|
|
if len(src) == 0 {
|
|
return ""
|
|
}
|
|
|
|
full := len(src) - len(src)%3
|
|
var out strings.Builder
|
|
out.Grow((len(src)+2)/3*4 + 1)
|
|
|
|
for index := 0; index < full; index += 3 {
|
|
group := uint32(src[index])<<16 | uint32(src[index+1])<<8 | uint32(src[index+2])
|
|
out.WriteByte(a.table[group>>18])
|
|
out.WriteByte(a.table[group>>12&63])
|
|
out.WriteByte(a.table[group>>6&63])
|
|
out.WriteByte(a.table[group&63])
|
|
}
|
|
|
|
switch len(src) - full {
|
|
case 1:
|
|
group := uint32(src[full]) << 16
|
|
out.WriteByte(a.table[group>>18])
|
|
out.WriteByte(a.table[group>>12&63])
|
|
out.WriteByte(PadChar)
|
|
out.WriteByte(PadChar)
|
|
case 2:
|
|
group := uint32(src[full])<<16 | uint32(src[full+1])<<8
|
|
out.WriteByte(a.table[group>>18])
|
|
out.WriteByte(a.table[group>>12&63])
|
|
out.WriteByte(a.table[group>>6&63])
|
|
out.WriteByte(PadChar)
|
|
}
|
|
return out.String()
|
|
}
|