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 assert.NotContains(t, string(sealed), plain, "the value is not stored in clear")
35 got, err := b.Open(sealed, aad)
36 require.NoError(t, err)
37 assert.Equal(t, plain, string(got))
38 }
39}
40
41func TestEachValueHasItsOwnNonce(t *testing.T) {
42 t.Parallel()
43 b := box(t, 1)
44 a := b.Seal([]byte("same"), []byte("aad"))
45 c := b.Seal([]byte("same"), []byte("aad"))
46 assert.NotEqual(t, a, c, "the same value sealed twice differs")
47}
48
49// A sealed value moved to another secret (another name or another scope) does
50// not open: the additional data binds it.
51func TestAdditionalDataBindsScopeAndName(t *testing.T) {
52 t.Parallel()
53 b := box(t, 1)
54 repo, _ := domain.ParseRepoName("proj")
55 other, _ := domain.ParseRepoName("other")
56 sealed := b.Seal([]byte("v"), domain.SecretAAD(domain.RepoScope(repo), "TOKEN"))
57
58 for name, aad := range map[string][]byte{
59 "other name": domain.SecretAAD(domain.RepoScope(repo), "OTHER"),
60 "instance scope": domain.SecretAAD(domain.InstanceScope(), "TOKEN"),
61 "other repository": domain.SecretAAD(domain.RepoScope(other), "TOKEN"),
62 "an environment": domain.SecretAAD(domain.EnvironmentScope(repo, "prod"), "TOKEN"),
63 "no additional data": nil,
64 } {
65 _, err := b.Open(sealed, aad)
66 assert.ErrorIs(t, err, ports.ErrSecretNotOpened, name)
67 }
68}
69
70func TestAnotherKeyOrDamagedValueDoesNotOpen(t *testing.T) {
71 t.Parallel()
72 a, other := box(t, 1), box(t, 2)
73 sealed := a.Seal([]byte("v"), []byte("aad"))
74 _, err := other.Open(sealed, []byte("aad"))
75 assert.ErrorIs(t, err, ports.ErrSecretNotOpened)
76
77 for i := range sealed {
78 damaged := bytes.Clone(sealed)
79 damaged[i] ^= 1
80 _, err := a.Open(damaged, []byte("aad"))
81 assert.ErrorIs(t, err, ports.ErrSecretNotOpened, "byte %d", i)
82 }
83 for _, short := range [][]byte{nil, {1}, sealed[:10], sealed[:len(sealed)-1]} {
84 _, err := a.Open(short, []byte("aad"))
85 assert.ErrorIs(t, err, ports.ErrSecretNotOpened)
86 }
87}
88
89func TestKeyCheckTellsKeysApartWithoutRevealingThem(t *testing.T) {
90 t.Parallel()
91 a, a2, other := box(t, 1), box(t, 1), box(t, 2)
92 assert.Equal(t, a.KeyCheck(), a2.KeyCheck(), "stable for a key")
93 assert.NotEqual(t, a.KeyCheck(), other.KeyCheck())
94 assert.NotContains(t, string(a.KeyCheck()), string(keyOf(1)))
95 a.KeyCheck()[0] ^= 1
96 assert.Equal(t, a2.KeyCheck(), a.KeyCheck(), "the caller cannot change the stored check")
97}
98
99func TestNewRefusesAWrongKeyLengthWithoutEchoingIt(t *testing.T) {
100 t.Parallel()
101 for _, n := range []int{0, 16, 31, 33, 64} {
102 _, err := secretbox.New(bytes.Repeat([]byte("k"), n))
103 require.Error(t, err)
104 assert.NotContains(t, err.Error(), "kkkk")
105 }
106}