Something went wrong. Try again.
A community based topic aggregation platform built on atproto
Something went wrong. Try again.
3.7 kB · 102 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103// Package credentialcipher encrypts stored credentials (community PDS// passwords and tokens, aggregator OAuth sessions and DPoP keys) with a key// held by the process rather than by the database, so a database read or a// backup does not yield the plaintext.package credentialcipher
import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt")
// KeySize is the number of raw key bytes AES-256 requires.const KeySize = 32
var ( // ErrInvalidKey reports a key that is not exactly KeySize bytes. ErrInvalidKey = errors.New("credentialcipher: key must be 32 bytes") // ErrInvalidCiphertext reports a value that is too short, tampered with, // or sealed under a different key or context. ErrInvalidCiphertext = errors.New("credentialcipher: invalid ciphertext") // ErrUnsupportedVersion reports invalid ciphertext whose leading version // byte this build does not understand. ErrUnsupportedVersion = fmt.Errorf("%w: unsupported ciphertext version", ErrInvalidCiphertext))
// Version is the leading byte of every value this package writes. Migration// 046 and the startup conversion pass compare against it; pgcrypto output// starts with 0xC3, so the two never collide.const Version = byte(0x01)
// Cipher seals and opens credential values with AES-256-GCM.type Cipher struct { aead cipher.AEAD}
// New builds a Cipher from exactly KeySize raw key bytes.func New(key []byte) (*Cipher, error) { if len(key) != KeySize { return nil, fmt.Errorf("%w: got %d bytes", ErrInvalidKey, len(key)) }
block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("%w: initialize AES", ErrInvalidKey) } aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("credentialcipher: initialize GCM: %w", err) }
return &Cipher{aead: aead}, nil}
// NewFromBase64 builds a Cipher from a standard-base64 encoding of KeySize bytes.func NewFromBase64(encoded string) (*Cipher, error) { key, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return nil, fmt.Errorf("%w: invalid base64", ErrInvalidKey) } return New(key)}
// Encrypt seals plaintext bound to context. The same context must be passed// to Decrypt; a value moved to a different row or column will not open.func (c *Cipher) Encrypt(plaintext, context string) ([]byte, error) { nonce := make([]byte, c.aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, fmt.Errorf("credentialcipher: generate nonce: %w", err) }
sealed := make([]byte, 1+len(nonce)) sealed[0] = Version copy(sealed[1:], nonce) authenticatedData := append([]byte{Version}, context...) return c.aead.Seal(sealed, nonce, []byte(plaintext), authenticatedData), nil}
// Decrypt opens a value produced by Encrypt under the same key and context.func (c *Cipher) Decrypt(ciphertext []byte, context string) (string, error) { minimumLength := 1 + c.aead.NonceSize() + c.aead.Overhead() if len(ciphertext) < minimumLength { return "", fmt.Errorf("%w: too short", ErrInvalidCiphertext) } if ciphertext[0] != Version { if ciphertext[0] == 0xC3 { return "", fmt.Errorf("%w: pgcrypto legacy ciphertext (version 195); the AppView startup conversion has not run on this database", ErrUnsupportedVersion) } return "", fmt.Errorf("%w: version %d", ErrUnsupportedVersion, ciphertext[0]) }
nonceEnd := 1 + c.aead.NonceSize() authenticatedData := append([]byte{Version}, context...) plaintext, err := c.aead.Open(nil, ciphertext[1:nonceEnd], ciphertext[nonceEnd:], authenticatedData) if err != nil { return "", fmt.Errorf("%w: authentication failed", ErrInvalidCiphertext) } return string(plaintext), nil}