package cryptox import ( "encoding/json" "errors" "fmt" "sort" "strconv" "strings" ) type Cipher interface { Encrypt([]byte) ([]byte, int, error) Decrypt([]byte, int) ([]byte, error) } type Keyring struct { activeVersion int ciphers map[int]*AESGCM } func NewKeyring(activeKey string, activeVersion int, encodedKeyring, purpose string) (*Keyring, error) { if activeVersion < 1 || purpose == "" { return nil, errors.New("encryption version and purpose are required") } encoded := make(map[int]string) if strings.TrimSpace(encodedKeyring) != "" { var values map[string]string if err := json.Unmarshal([]byte(encodedKeyring), &values); err != nil { return nil, fmt.Errorf("encryption keyring must be a JSON object: %w", err) } for rawVersion, key := range values { version, err := strconv.Atoi(rawVersion) if err != nil || version < 1 || strings.TrimSpace(key) == "" { return nil, fmt.Errorf("invalid encryption keyring version %q", rawVersion) } encoded[version] = strings.TrimSpace(key) } } if strings.TrimSpace(activeKey) != "" { if existing, ok := encoded[activeVersion]; ok && existing != strings.TrimSpace(activeKey) { return nil, fmt.Errorf("active encryption key version %d is configured twice with different values", activeVersion) } encoded[activeVersion] = strings.TrimSpace(activeKey) } if len(encoded) == 0 { return nil, nil } if _, ok := encoded[activeVersion]; !ok { return nil, fmt.Errorf("active encryption key version %d is not loaded", activeVersion) } keyring := &Keyring{activeVersion: activeVersion, ciphers: make(map[int]*AESGCM, len(encoded))} for version, key := range encoded { cipher, err := NewAESGCM(key, version, purpose) if err != nil { return nil, fmt.Errorf("encryption key version %d: %w", version, err) } keyring.ciphers[version] = cipher } return keyring, nil } func (k *Keyring) Encrypt(plaintext []byte) ([]byte, int, error) { if k == nil { return nil, 0, ErrKeyUnavailable } return k.ciphers[k.activeVersion].Encrypt(plaintext) } func (k *Keyring) Decrypt(encrypted []byte, version int) ([]byte, error) { if k == nil { return nil, ErrKeyUnavailable } cipher, ok := k.ciphers[version] if !ok { return nil, fmt.Errorf("encryption key version %d is not loaded", version) } return cipher.Decrypt(encrypted, version) } func (k *Keyring) ActiveVersion() int { if k == nil { return 0 } return k.activeVersion } func (k *Keyring) Versions() []int { if k == nil { return nil } versions := make([]int, 0, len(k.ciphers)) for version := range k.ciphers { versions = append(versions, version) } sort.Ints(versions) return versions } var _ Cipher = (*Keyring)(nil)