NewDKIMCipher derives a dedicated 32-byte KEK from the master secret via HKDF-SHA256 (mirroring oauth.deriveOAuthSigningKey: a per-purpose label so the DKIM key can rotate independently of header/OAuth/HITL signing) and returns an AES-256-GCM cipher. It fails closed when the master is shorter than 3
(master []byte)
| 52 | // the config layer only enforces that in production, so a weak dev secret must |
| 53 | // not silently produce weak at-rest encryption. |
| 54 | func NewDKIMCipher(master []byte) (*DKIMCipher, error) { |
| 55 | if len(master) < 32 { |
| 56 | return nil, fmt.Errorf("dkim: master secret is %d bytes, need ≥32 to derive a KEK", len(master)) |
| 57 | } |
| 58 | kek := make([]byte, 32) |
| 59 | if _, err := io.ReadFull(hkdf.New(sha256.New, master, nil, []byte(dkimKEKLabel)), kek); err != nil { |
| 60 | return nil, fmt.Errorf("dkim: derive KEK: %w", err) |
| 61 | } |
| 62 | block, err := aes.NewCipher(kek) |
| 63 | if err != nil { |
| 64 | return nil, fmt.Errorf("dkim: aes cipher: %w", err) |
| 65 | } |
| 66 | aead, err := cipher.NewGCM(block) |
| 67 | if err != nil { |
| 68 | return nil, fmt.Errorf("dkim: gcm: %w", err) |
| 69 | } |
| 70 | return &DKIMCipher{aead: aead}, nil |
| 71 | } |
| 72 | |
| 73 | // seal encrypts a private key, binding the (normalized) domain as AAD so a |
| 74 | // ciphertext cannot be moved onto another domain's row and still decrypt. |