1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package secretbox_test
4
5import (
6 "bytes"
7 "strings"
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "bnema.dev/ermite/internal/adapters/secretbox"
14 "bnema.dev/ermite/internal/domain"
15 "bnema.dev/ermite/internal/ports"
16)
17
18func keyOf(b byte) []byte { return bytes.Repeat([]byte{b}, secretbox.KeyLen) }
19
20func box(t *testing.T, b byte) *secretbox.Box {
21 t.Helper()
22 x, err := secretbox.New(keyOf(b))
23 require.NoError(t, err)
24 return x
25}
26
27func TestSealOpenRoundTrip(t *testing.T) {
28 t.Parallel()
29 b := box(t, 1)
30 repo, _ := domain.ParseRepoName("proj")
31 aad := domain.SecretAAD(domain.RepoScope(repo), "TOKEN")
32 for _, plain := range []string{"x", "multi\nline\r\nvalue", strings.Repeat("é", 20000), "\x00\xff"} {
33 sealed := b.Seal([]byte(plain), aad)
34 if len(plain) >= 8 { // a shorter value can turn up in random bytes by chance
35 assert.NotContains(t, string(sealed), plain, "the value is not stored in clear")
36 }
37 got, err := b.Open(sealed, aad)
38 require.NoError(t, err)
39 assert.Equal(t, plain, string(got))
40 }
41}
42
43func TestEachValueHasItsOwnNonce(t *testing.T) {
44 t.Parallel()
45 b := box(t, 1)
46 a := b.Seal([]byte("same"), []byte("aad"))
47 c := b.Seal([]byte("same"), []byte("aad"))
48 assert.NotEqual(t, a, c, "the same value sealed twice differs")
49}
50
51// A sealed value moved to another secret (another name or another scope) does
52// not open: the additional data binds it.
53func TestAdditionalDataBindsScopeAndName(t *testing.T) {
54 t.Parallel()
55 b := box(t, 1)
56 repo, _ := domain.ParseRepoName("proj")
57 other, _ := domain.ParseRepoName("other")
58 sealed := b.Seal([]byte("v"), domain.SecretAAD(domain.RepoScope(repo), "TOKEN"))
59
60 for name, aad := range map[string][]byte{
61 "other name": domain.SecretAAD(domain.RepoScope(repo), "OTHER"),
62 "instance scope": domain.SecretAAD(domain.InstanceScope(), "TOKEN"),
63 "other repository": domain.SecretAAD(domain.RepoScope(other), "TOKEN"),
64 "an environment": domain.SecretAAD(domain.EnvironmentScope(repo, "prod"), "TOKEN"),
65 "no additional data": nil,
66 } {
67 _, err := b.Open(sealed, aad)
68 assert.ErrorIs(t, err, ports.ErrSecretNotOpened, name)
69 }
70}
71
72func TestAnotherKeyOrDamagedValueDoesNotOpen(t *testing.T) {
73 t.Parallel()
74 a, other := box(t, 1), box(t, 2)
75 sealed := a.Seal([]byte("v"), []byte("aad"))
76 _, err := other.Open(sealed, []byte("aad"))
77 assert.ErrorIs(t, err, ports.ErrSecretNotOpened)
78
79 for i := range sealed {
80 damaged := bytes.Clone(sealed)
81 damaged[i] ^= 1
82 _, err := a.Open(damaged, []byte("aad"))
83 assert.ErrorIs(t, err, ports.ErrSecretNotOpened, "byte %d", i)
84 }
85 for _, short := range [][]byte{nil, {1}, sealed[:10], sealed[:len(sealed)-1]} {
86 _, err := a.Open(short, []byte("aad"))
87 assert.ErrorIs(t, err, ports.ErrSecretNotOpened)
88 }
89}
90
91func TestKeyCheckTellsKeysApartWithoutRevealingThem(t *testing.T) {
92 t.Parallel()
93 a, a2, other := box(t, 1), box(t, 1), box(t, 2)
94 assert.Equal(t, a.KeyCheck(), a2.KeyCheck(), "stable for a key")
95 assert.NotEqual(t, a.KeyCheck(), other.KeyCheck())
96 assert.NotContains(t, string(a.KeyCheck()), string(keyOf(1)))
97 a.KeyCheck()[0] ^= 1
98 assert.Equal(t, a2.KeyCheck(), a.KeyCheck(), "the caller cannot change the stored check")
99}
100
101func TestNewRefusesAWrongKeyLengthWithoutEchoingIt(t *testing.T) {
102 t.Parallel()
103 for _, n := range []int{0, 16, 31, 33, 64} {
104 _, err := secretbox.New(bytes.Repeat([]byte("k"), n))
105 require.Error(t, err)
106 assert.NotContains(t, err.Error(), "kkkk")
107 }
108}