nodetoken.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. // Package nodetoken encrypts replayable per-node bearer tokens at rest with
  2. // row-bound AES-GCM and versioned key IDs.
  3. package nodetoken
  4. import (
  5. "crypto/aes"
  6. "crypto/cipher"
  7. "crypto/rand"
  8. "encoding/base64"
  9. "errors"
  10. "fmt"
  11. "strings"
  12. "sync"
  13. )
  14. // Mode is explicit so a missing key cannot silently downgrade encrypted
  15. // deployments to plaintext.
  16. type Mode int
  17. const (
  18. // ModeOff: legacy plaintext operation. Writes store plaintext; an encrypted
  19. // value cannot be interpreted (no key) and is rejected rather than guessed.
  20. ModeOff Mode = iota
  21. // ModeMigration: key required. Reads accept plaintext OR ciphertext; writes
  22. // always produce ciphertext. Used while migrating existing rows.
  23. ModeMigration
  24. // ModeRequired: key required (startup fails if it cannot load). Reads decrypt
  25. // ciphertext (error on failure) and accept any still-unmigrated plaintext;
  26. // writes always produce ciphertext.
  27. ModeRequired
  28. )
  29. const (
  30. encPrefix = "enc:"
  31. encScheme = "enc:v1:"
  32. keyLen = 32 // AES-256
  33. nonceLen = 12 // GCM standard nonce
  34. aadKeyFormat = "nodes/api_token/%d"
  35. )
  36. // ParseMode maps the NODE_TOKEN_ENCRYPTION env value to a Mode.
  37. func ParseMode(s string) (Mode, error) {
  38. switch strings.ToLower(strings.TrimSpace(s)) {
  39. case "", "off":
  40. return ModeOff, nil
  41. case "migration":
  42. return ModeMigration, nil
  43. case "required":
  44. return ModeRequired, nil
  45. default:
  46. return ModeOff, fmt.Errorf("nodetoken: unknown NODE_TOKEN_ENCRYPTION %q (want off|migration|required)", s)
  47. }
  48. }
  49. // Keyring holds the active write key and previous decryption keys.
  50. type Keyring struct {
  51. ActiveID string
  52. Keys map[string][keyLen]byte
  53. }
  54. func (kr *Keyring) active() ([keyLen]byte, error) {
  55. k, ok := kr.Keys[kr.ActiveID]
  56. if !ok {
  57. return [keyLen]byte{}, fmt.Errorf("nodetoken: active key %q not in keyring", kr.ActiveID)
  58. }
  59. return k, nil
  60. }
  61. // Codec encrypts/decrypts node tokens under a fixed policy and keyring.
  62. type Codec struct {
  63. mode Mode
  64. ring *Keyring // nil only in ModeOff
  65. }
  66. // NewCodec requires an active key outside ModeOff.
  67. func NewCodec(mode Mode, ring *Keyring) (*Codec, error) {
  68. if mode == ModeOff {
  69. return &Codec{mode: ModeOff}, nil
  70. }
  71. if ring == nil || len(ring.Keys) == 0 {
  72. return nil, errors.New("nodetoken: encryption mode requires a key, but none was loaded")
  73. }
  74. if _, err := ring.active(); err != nil {
  75. return nil, err
  76. }
  77. return &Codec{mode: mode, ring: ring}, nil
  78. }
  79. // Enabled reports whether the codec writes ciphertext (mode != off).
  80. func (c *Codec) Enabled() bool { return c.mode != ModeOff }
  81. func aad(nodeID int) []byte { return []byte(fmt.Sprintf(aadKeyFormat, nodeID)) }
  82. // IsEncrypted reports whether a stored value is in this package's ciphertext form.
  83. func IsEncrypted(stored string) bool { return strings.HasPrefix(stored, encPrefix) }
  84. // Encrypt returns plaintext in ModeOff or row-bound enc:v1 ciphertext otherwise.
  85. // Empty and already-valid encrypted values remain unchanged.
  86. func (c *Codec) Encrypt(nodeID int, plaintext string) (string, error) {
  87. if c.mode == ModeOff || plaintext == "" {
  88. return plaintext, nil
  89. }
  90. if IsEncrypted(plaintext) {
  91. // Validate it actually decrypts for this node; if so keep verbatim.
  92. if _, err := c.Decrypt(nodeID, plaintext); err != nil {
  93. return "", fmt.Errorf("nodetoken: refusing to store undecryptable ciphertext: %w", err)
  94. }
  95. return plaintext, nil
  96. }
  97. key, err := c.ring.active()
  98. if err != nil {
  99. return "", err
  100. }
  101. gcm, err := newGCM(key)
  102. if err != nil {
  103. return "", err
  104. }
  105. nonce := make([]byte, nonceLen)
  106. if _, err := rand.Read(nonce); err != nil {
  107. return "", err
  108. }
  109. ct := gcm.Seal(nil, nonce, []byte(plaintext), aad(nodeID))
  110. blob := append(nonce, ct...)
  111. return encScheme + c.ring.ActiveID + ":" + base64.RawURLEncoding.EncodeToString(blob), nil
  112. }
  113. // Decrypt passes legacy plaintext through; enc: values must authenticate and
  114. // are never reinterpreted as plaintext after an error.
  115. func (c *Codec) Decrypt(nodeID int, stored string) (string, error) {
  116. if c.mode == ModeOff {
  117. return stored, nil
  118. }
  119. if !IsEncrypted(stored) {
  120. return stored, nil
  121. }
  122. rest, ok := strings.CutPrefix(stored, encScheme)
  123. if !ok {
  124. return "", fmt.Errorf("nodetoken: unsupported ciphertext scheme in %q", firstN(stored, 12))
  125. }
  126. keyID, b64, ok := strings.Cut(rest, ":")
  127. if !ok || keyID == "" {
  128. return "", errors.New("nodetoken: malformed ciphertext (missing key id)")
  129. }
  130. if c.ring == nil {
  131. return "", errors.New("nodetoken: encrypted token encountered but encryption is disabled (no key)")
  132. }
  133. key, ok := c.ring.Keys[keyID]
  134. if !ok {
  135. return "", fmt.Errorf("nodetoken: no key %q in keyring to decrypt token", keyID)
  136. }
  137. blob, err := base64.RawURLEncoding.DecodeString(b64)
  138. if err != nil {
  139. return "", fmt.Errorf("nodetoken: base64 decode: %w", err)
  140. }
  141. if len(blob) < nonceLen {
  142. return "", errors.New("nodetoken: ciphertext too short")
  143. }
  144. gcm, err := newGCM(key)
  145. if err != nil {
  146. return "", err
  147. }
  148. pt, err := gcm.Open(nil, blob[:nonceLen], blob[nonceLen:], aad(nodeID))
  149. if err != nil {
  150. return "", fmt.Errorf("nodetoken: authentication failed for node %d: %w", nodeID, err)
  151. }
  152. return string(pt), nil
  153. }
  154. // ActiveKeyID returns the id new writes use (empty in ModeOff).
  155. func (c *Codec) ActiveKeyID() string {
  156. if c.ring == nil {
  157. return ""
  158. }
  159. return c.ring.ActiveID
  160. }
  161. // EncryptedWithActive reports whether migration can skip a ciphertext row.
  162. func (c *Codec) EncryptedWithActive(stored string) bool {
  163. if c.ring == nil || !IsEncrypted(stored) {
  164. return false
  165. }
  166. rest, ok := strings.CutPrefix(stored, encScheme)
  167. if !ok {
  168. return false
  169. }
  170. keyID, _, ok := strings.Cut(rest, ":")
  171. return ok && keyID == c.ring.ActiveID
  172. }
  173. func newGCM(key [keyLen]byte) (cipher.AEAD, error) {
  174. block, err := aes.NewCipher(key[:])
  175. if err != nil {
  176. return nil, err
  177. }
  178. return cipher.NewGCM(block)
  179. }
  180. func firstN(s string, n int) string {
  181. if len(s) <= n {
  182. return s
  183. }
  184. return s[:n]
  185. }
  186. // --- package singleton, initialized once at startup ---
  187. var (
  188. mu sync.RWMutex
  189. current *Codec
  190. )
  191. // Init installs the process-wide codec. Call once during startup after building
  192. // the keyring; in ModeOff a nil keyring is fine.
  193. func Init(c *Codec) {
  194. mu.Lock()
  195. defer mu.Unlock()
  196. current = c
  197. }
  198. // get returns the installed codec, or a permissive ModeOff codec if Init was
  199. // never called (e.g. unit tests / sqlite dev) so callers never nil-panic.
  200. func get() *Codec {
  201. mu.RLock()
  202. c := current
  203. mu.RUnlock()
  204. if c == nil {
  205. return &Codec{mode: ModeOff}
  206. }
  207. return c
  208. }
  209. // Encrypt/Decrypt/Enabled operate on the process-wide codec.
  210. func Encrypt(nodeID int, plaintext string) (string, error) { return get().Encrypt(nodeID, plaintext) }
  211. func Decrypt(nodeID int, stored string) (string, error) { return get().Decrypt(nodeID, stored) }
  212. func Enabled() bool { return get().Enabled() }
  213. func Active() *Codec { return get() }