ficha/framing.go
2026-04-29 01:28:05 +00:00

58 lines
1.6 KiB
Go

package ficha
import (
"encoding/base64"
"strings"
)
const tokenVersion = "v1"
// encodeToken builds the wire format: v1.<keyID>.<base64url(none||ciphertext)>
func encodeToken(keyID string, nonce, ciphertext []byte) string {
body := make([]byte, 0, len(nonce)+len(ciphertext))
body = append(body, nonce...)
body = append(body, ciphertext...)
var b strings.Builder
b.Grow(len(tokenVersion) + 1 + len(keyID) + 1 + base64.RawURLEncoding.EncodedLen(len(body)))
b.WriteString(tokenVersion)
b.WriteByte('.')
b.WriteString(keyID)
b.WriteByte('.')
b.WriteString(base64.RawURLEncoding.EncodeToString(body))
return b.String()
}
// decodeToken parses the wire format and splits the body back into nonce and ciphertext.
// Returns ErrInvalidToken for any malformed input.
func decodeToken(token string) (keyID string, nonce, ciphertext []byte, err error) {
parts := strings.SplitN(token, ".", 3)
if len(parts) != 3 {
return "", nil, nil, ErrInvalidToken
}
if parts[0] != tokenVersion {
return "", nil, nil, ErrInvalidToken
}
if parts[1] == "" {
return "", nil, nil, ErrInvalidToken
}
body, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil {
return "", nil, nil, ErrInvalidToken
}
// body must be at least nonce + auth tag (16 bytes)
if len(body) < nonceSize+16 {
return "", nil, nil, ErrInvalidToken
}
return parts[1], body[:nonceSize], body[nonceSize:], nil
}
// aadFor builds the AAD bytes for a given version+keyID. Both encrypt
// and decrypt must use the same construction.
func aadFor(keyID string) []byte {
return []byte(tokenVersion + "." + keyID)
}