1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3// Package secretbox seals the values of CI secrets with AES-256-GCM under the
4// server's master key (ERMITE_SECRETS_KEY, ADR 0019). Each value gets its own
5// random nonce, and the additional data given by the caller (the scope and the
6// name of the secret) is authenticated with it, so a sealed value moved to
7// another secret does not open.
8package secretbox
9
10import (
11 "crypto/aes"
12 "crypto/cipher"
13 "crypto/hmac"
14 "crypto/rand"
15 "crypto/sha256"
16 "errors"
17 "fmt"
18
19 "bnema.dev/ermite/internal/ports"
20)
21
22// KeyLen is the length of the master key.
23const KeyLen = 32
24
25// version is the first byte of a sealed value, so that the format can change.
26const version byte = 1
27
28// Box seals and opens secret values. It is safe for concurrent use.
29type Box struct {
30 aead cipher.AEAD
31 check []byte
32}
33
34var _ ports.SecretSealer = (*Box)(nil)
35
36// New builds a Box from a key of KeyLen bytes. The error never holds the key.
37func New(key []byte) (*Box, error) {
38 if len(key) != KeyLen {
39 return nil, fmt.Errorf("the secrets key must be %d bytes, got %d", KeyLen, len(key))
40 }
41 block, err := aes.NewCipher(key)
42 if err != nil {
43 return nil, errors.New("the secrets key is not usable")
44 }
45 aead, err := cipher.NewGCM(block)
46 if err != nil {
47 return nil, errors.New("the secrets key is not usable")
48 }
49 mac := hmac.New(sha256.New, key)
50 mac.Write([]byte("ermite secrets key check v1"))
51 return &Box{aead: aead, check: mac.Sum(nil)}, nil
52}
53
54// Seal returns version, nonce and ciphertext (with its tag) of plaintext,
55// bound to aad.
56func (b *Box) Seal(plaintext, aad []byte) []byte {
57 out := make([]byte, 1, 1+b.aead.NonceSize()+len(plaintext)+b.aead.Overhead())
58 out[0] = version
59 nonce := make([]byte, b.aead.NonceSize())
60 _, _ = rand.Read(nonce) // crypto/rand does not fail: it aborts the program
61 out = append(out, nonce...)
62 return b.aead.Seal(out, nonce, plaintext, aad)
63}
64
65// Open returns the plaintext of a value sealed with the same key and aad. Any
66// other key, aad or a changed byte is ports.ErrSecretNotOpened.
67func (b *Box) Open(sealed, aad []byte) ([]byte, error) {
68 ns := b.aead.NonceSize()
69 if len(sealed) < 1+ns+b.aead.Overhead() || sealed[0] != version {
70 return nil, ports.ErrSecretNotOpened
71 }
72 plain, err := b.aead.Open(nil, sealed[1:1+ns], sealed[1+ns:], aad)
73 if err != nil {
74 return nil, ports.ErrSecretNotOpened
75 }
76 return plain, nil
77}
78
79// KeyCheck is a value derived from the key that does not reveal it (an HMAC of
80// a constant), stored beside the secrets so that the server can tell, at start,
81// that it runs with another key than the one they were sealed with.
82func (b *Box) KeyCheck() []byte { return append([]byte(nil), b.check...) }