@@ -2,13 +2,23 @@ package config
2
2
3
3
import (
4
4
"bytes"
5
+ "crypto/aes"
6
+ "crypto/cipher"
7
+ "crypto/ecdh"
8
+ "crypto/ed25519"
9
+ cryptorand "crypto/rand"
10
+ "crypto/sha256"
5
11
"encoding/base64"
12
+ "errors"
13
+ "io"
6
14
"net/http"
7
15
"os"
8
16
"strings"
9
17
"testing"
10
18
11
19
utiltest "github.com/databacker/mysql-backup/pkg/internal/test"
20
+ "golang.org/x/crypto/hkdf"
21
+ "golang.org/x/crypto/nacl/box"
12
22
"gopkg.in/yaml.v3"
13
23
14
24
"github.com/databacker/api/go/api"
@@ -88,3 +98,147 @@ func TestGetRemoteConfig(t *testing.T) {
88
98
}
89
99
90
100
}
101
+
102
+ func TestDecryptConfig (t * testing.T ) {
103
+ configFile := "./testdata/config.yml"
104
+ content , err := os .ReadFile (configFile )
105
+ if err != nil {
106
+ t .Fatalf ("failed to read config file: %v" , err )
107
+ }
108
+ var validConfig api.Config
109
+ if err := yaml .Unmarshal (content , & validConfig ); err != nil {
110
+ t .Fatalf ("failed to unmarshal config: %v" , err )
111
+ }
112
+
113
+ senderCurve := ecdh .X25519 ()
114
+ senderPrivateKey , err := senderCurve .GenerateKey (cryptorand .Reader )
115
+ if err != nil {
116
+ t .Fatalf ("failed to generate sender random seed: %v" , err )
117
+ }
118
+ senderPublicKey := senderPrivateKey .PublicKey ()
119
+ senderPublicKeyBytes := senderPublicKey .Bytes ()
120
+
121
+ recipientCurve := ecdh .X25519 ()
122
+ recipientPrivateKey , err := recipientCurve .GenerateKey (cryptorand .Reader )
123
+ if err != nil {
124
+ t .Fatalf ("failed to generate recipient random seed: %v" , err )
125
+ }
126
+ recipientPublicKey := recipientPrivateKey .PublicKey ()
127
+ recipientPublicKeyBytes := recipientPublicKey .Bytes ()
128
+
129
+ var recipientPublicKeyArray , senderPrivateKeyArray [32 ]byte
130
+ copy (recipientPublicKeyArray [:], recipientPublicKeyBytes )
131
+ copy (senderPrivateKeyArray [:], senderPrivateKey .Bytes ())
132
+
133
+ senderPublicKeyB64 := base64 .StdEncoding .EncodeToString (senderPublicKeyBytes )
134
+
135
+ recipientPublicKeyB64 := base64 .StdEncoding .EncodeToString (recipientPublicKeyBytes )
136
+
137
+ // compute the shared secret using the sender's private key and the recipient's public key
138
+ var sharedSecret [32 ]byte
139
+ box .Precompute (& sharedSecret , & recipientPublicKeyArray , & senderPrivateKeyArray )
140
+
141
+ // Derive the symmetric key using HKDF with the shared secret
142
+ hkdfReader := hkdf .New (sha256 .New , sharedSecret [:], nil , []byte (api .SymmetricKey ))
143
+ symmetricKey := make ([]byte , 32 ) // AES-GCM requires 32 bytes
144
+ if _ , err := hkdfReader .Read (symmetricKey ); err != nil {
145
+ t .Fatalf ("failed to derive symmetric key: %v" , err )
146
+ }
147
+
148
+ // Create AES cipher block
149
+ block , err := aes .NewCipher (symmetricKey )
150
+ if err != nil {
151
+ t .Fatalf ("failed to create AES cipher" )
152
+ }
153
+ // Create GCM instance
154
+ aesGCM , err := cipher .NewGCM (block )
155
+ if err != nil {
156
+ t .Fatalf ("failed to create AES-GCM" )
157
+ }
158
+
159
+ // Generate a random nonce
160
+ nonce := make ([]byte , aesGCM .NonceSize ())
161
+ _ , err = cryptorand .Read (nonce )
162
+ if err != nil {
163
+ t .Fatalf ("failed to generate nonce" )
164
+ }
165
+
166
+ // Encrypt the plaintext
167
+ ciphertext := aesGCM .Seal (nil , nonce , content , nil )
168
+
169
+ // Embed the nonce in the ciphertext
170
+ fullCiphertext := append (nonce , ciphertext ... )
171
+
172
+ algo := api .AesGcm256
173
+ data := base64 .StdEncoding .EncodeToString (fullCiphertext )
174
+
175
+ // this is a valid spec, we want to be able to change fields
176
+ // without modifying the original, so we have a utility function after
177
+ validSpec := api.EncryptedSpec {
178
+ Algorithm : & algo ,
179
+ Data : & data ,
180
+ RecipientPublicKey : & recipientPublicKeyB64 ,
181
+ SenderPublicKey : & senderPublicKeyB64 ,
182
+ }
183
+
184
+ // copy a spec, changing specific fields
185
+ copyModifySpec := func (opts ... func (* api.EncryptedSpec )) api.EncryptedSpec {
186
+ copy := validSpec
187
+ for _ , opt := range opts {
188
+ opt (& copy )
189
+ }
190
+ return copy
191
+ }
192
+
193
+ unusedSeed := make ([]byte , ed25519 .SeedSize )
194
+ if _ , err := io .ReadFull (cryptorand .Reader , unusedSeed ); err != nil {
195
+ t .Fatalf ("failed to generate sender random seed: %v" , err )
196
+ }
197
+
198
+ // recipient private key credentials
199
+ recipientCreds := []string {base64 .StdEncoding .EncodeToString (recipientPrivateKey .Bytes ())}
200
+ unusedCreds := []string {base64 .StdEncoding .EncodeToString (unusedSeed )}
201
+
202
+ tests := []struct {
203
+ name string
204
+ inSpec api.EncryptedSpec
205
+ credentials []string
206
+ config api.Config
207
+ err error
208
+ }{
209
+ {"no algorithm" , copyModifySpec (func (s * api.EncryptedSpec ) { s .Algorithm = nil }), recipientCreds , api.Config {}, errors .New ("empty algorithm" )},
210
+ {"no data" , copyModifySpec (func (s * api.EncryptedSpec ) { s .Data = nil }), recipientCreds , api.Config {}, errors .New ("empty data" )},
211
+ {"bad base64 data" , copyModifySpec (func (s * api.EncryptedSpec ) { data := "abcdef" ; s .Data = & data }), recipientCreds , api.Config {}, errors .New ("failed to decode encrypted data: illegal base64 data" )},
212
+ {"short encrypted data" , copyModifySpec (func (s * api.EncryptedSpec ) {
213
+ data := base64 .StdEncoding .EncodeToString ([]byte ("abcdef" ))
214
+ s .Data = & data
215
+ }), recipientCreds , api.Config {}, errors .New ("invalid encrypted data length" )},
216
+ {"invalid encrypted data" , copyModifySpec (func (s * api.EncryptedSpec ) {
217
+ bad := nonce
218
+ bad = append (bad , 1 , 2 , 3 , 4 )
219
+ data := base64 .StdEncoding .EncodeToString (bad )
220
+ s .Data = & data
221
+ }), recipientCreds , api.Config {}, errors .New ("failed to decrypt data: cipher: message authentication failed" )},
222
+ {"empty credentials" , validSpec , nil , api.Config {}, errors .New ("no private key found that matches public key" )},
223
+ {"unmatched credentials" , validSpec , unusedCreds , api.Config {}, errors .New ("no private key found that matches public key" )},
224
+ {"success with just one credential" , validSpec , recipientCreds , validConfig , nil },
225
+ {"success with multiple credentials" , validSpec , append (recipientCreds , unusedCreds ... ), validConfig , nil },
226
+ }
227
+ for _ , tt := range tests {
228
+ t .Run (tt .name , func (t * testing.T ) {
229
+ conf , err := decryptConfig (tt .inSpec , tt .credentials )
230
+ switch {
231
+ case err == nil && tt .err != nil :
232
+ t .Fatalf ("expected error: %v" , tt .err )
233
+ case err != nil && tt .err == nil :
234
+ t .Fatalf ("unexpected error: %v" , err )
235
+ case err != nil && tt .err != nil && ! strings .HasPrefix (err .Error (), tt .err .Error ()):
236
+ t .Fatalf ("mismatched error: %v" , err )
237
+ }
238
+ diff := cmp .Diff (tt .config , conf )
239
+ if diff != "" {
240
+ t .Fatalf ("mismatched config: %s" , diff )
241
+ }
242
+ })
243
+ }
244
+ }
0 commit comments