@@ -2,13 +2,23 @@ package config
22
33import (
44 "bytes"
5+ "crypto/aes"
6+ "crypto/cipher"
7+ "crypto/ecdh"
8+ "crypto/ed25519"
9+ cryptorand "crypto/rand"
10+ "crypto/sha256"
511 "encoding/base64"
12+ "errors"
13+ "io"
614 "net/http"
715 "os"
816 "strings"
917 "testing"
1018
1119 utiltest "github.com/databacker/mysql-backup/pkg/internal/test"
20+ "golang.org/x/crypto/hkdf"
21+ "golang.org/x/crypto/nacl/box"
1222 "gopkg.in/yaml.v3"
1323
1424 "github.com/databacker/api/go/api"
@@ -88,3 +98,147 @@ func TestGetRemoteConfig(t *testing.T) {
8898 }
8999
90100}
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