From 8e7eca5daafda33975645144d489bfd42604e2ec Mon Sep 17 00:00:00 2001 From: Filip Burlacu Date: Thu, 19 Sep 2019 14:44:32 -0400 Subject: [PATCH] Legacy encrypt (current aries RFC 19), compatible with ACA-Py Closes #139 Signed-off-by: Filip Burlacu --- go.mod | 2 + go.sum | 4 + .../crypto/legacy/authcrypt/authcrypt.go | 224 ++++++++++++++++++ .../crypto/legacy/authcrypt/authcrypt_test.go | 198 ++++++++++++++++ .../crypto/legacy/authcrypt/decrypt.go | 44 ++++ .../crypto/legacy/authcrypt/encrypt.go | 211 +++++++++++++++++ 6 files changed, 683 insertions(+) create mode 100644 pkg/didcomm/crypto/legacy/authcrypt/authcrypt.go create mode 100644 pkg/didcomm/crypto/legacy/authcrypt/authcrypt_test.go create mode 100644 pkg/didcomm/crypto/legacy/authcrypt/decrypt.go create mode 100644 pkg/didcomm/crypto/legacy/authcrypt/encrypt.go diff --git a/go.mod b/go.mod index 0561fcd3de..b39690e5dd 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ module github.com/hyperledger/aries-framework-go require ( + github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.1.1 @@ -21,6 +22,7 @@ require ( golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f // indirect golang.org/x/text v0.3.2 // indirect + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index d5eb1ccff5..83a350697d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= @@ -71,6 +73,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/pkg/didcomm/crypto/legacy/authcrypt/authcrypt.go b/pkg/didcomm/crypto/legacy/authcrypt/authcrypt.go new file mode 100644 index 0000000000..d900785758 --- /dev/null +++ b/pkg/didcomm/crypto/legacy/authcrypt/authcrypt.go @@ -0,0 +1,224 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package authcrypt + +import ( + "bytes" + "crypto/rand" + "crypto/sha512" + "encoding/json" + "errors" + "fmt" + + "github.com/agl/ed25519/extra25519" + "golang.org/x/crypto/blake2b" + chacha "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/nacl/box" +) + +type privateEd25519 [ed25519.PrivateKeySize]byte +type publicEd25519 [ed25519.PublicKeySize]byte + +type keyPairEd25519 struct { + priv *privateEd25519 + pub *publicEd25519 +} + +// CurveKeySize is the size of public and private Curve25519 keys in bytes +const CurveKeySize int = 32 + +type privateCurve25519 [CurveKeySize]byte +type publicCurve25519 [CurveKeySize]byte + +type keyPairCurve25519 struct { + priv *privateCurve25519 + pub *publicCurve25519 +} + +// contentEncryption represents a content encryption algorithm. +type contentEncryption string + +// C20P Chacha20Poly1035 algorithm +const C20P = contentEncryption("C20P") // Chacha20 encryption + Poly1035 authenticator cipher (96 bits nonce) + +// XC20P XChacha20Poly1035 algorithm +const XC20P = contentEncryption("XC20P") // XChacha20 encryption + Poly1035 authenticator cipher (192 bits nonce) + +// Crypter represents an Authcrypt Encrypter (Decrypter) that outputs/reads legacy Aries envelopes +type Crypter struct { + sender keyPairEd25519 + recipients []*publicEd25519 + alg contentEncryption + nonceSize int +} + +// New will create a Crypter that encrypts messages using the legacy Aries format +// Note: legacy crypter does not support XChacha20Poly1035 (XC20P), only Chacha20Poly1035 (C20P) +func New(sender keyPairEd25519, recipients []*publicEd25519, alg contentEncryption) (*Crypter, error) { + var nonceSize int + switch alg { + case C20P: + nonceSize = chacha.NonceSize + default: + return nil, fmt.Errorf("encryption algorithm '%s' not supported", alg) + } + + if len(recipients) == 0 { + return nil, errors.New("empty recipients keys, must have at least one recipient") + } + var recipientsKey []*publicEd25519 + recipientsKey = append(recipientsKey, recipients...) + + c := &Crypter{ + sender: sender, + recipients: recipientsKey, + alg: alg, + nonceSize: nonceSize, + } + + if !isKeyPairValid(sender) { + return nil, fmt.Errorf( + "sender keyPair not supported, it must have a %d byte private key and %d byte public key", + ed25519.PrivateKeySize, + ed25519.PublicKeySize) + } + + return c, nil +} + +func isKeyPairValid(kp keyPairEd25519) bool { + if kp.priv == nil || kp.pub == nil { + return false + } + + return true +} + +// Envelope is the full payload envelope for the JSON message +type Envelope struct { + Protected string `json:"protected,omitempty"` + IV string `json:"iv,omitempty"` + CipherText string `json:"ciphertext,omitempty"` + Tag string `json:"tag,omitempty"` +} + +// Protected is the protected header of the JSON envelope +type Protected struct { + Enc string `json:"enc,omitempty"` + Typ string `json:"typ,omitempty"` + Alg string `json:"alg,omitempty"` + Recipients []Recipient `json:"recipients,omitempty"` +} + +// Recipient holds the data for a recipient in the envelope header +type Recipient struct { + EncryptedKey string `json:"encrypted_key,omitempty"` + Header RecipientHeader `json:"header,omitempty"` +} + +// RecipientHeader holds the header data for a recipient +type RecipientHeader struct { + KID string `json:"kid,omitempty"` + Sender string `json:"sender,omitempty"` + IV string `json:"iv,omitempty"` +} + +// publicEd25519toCurve25519 takes an Ed25519 public key and provides the corresponding Curve25519 public key +// This function wraps PublicKeyToCurve25519 from Adam Langley's ed25519 repo: github.com/agl/ed25519 +func publicEd25519toCurve25519(pub *publicEd25519) (*publicCurve25519, error) { + pkOut := new([CurveKeySize]byte) + success := extra25519.PublicKeyToCurve25519(pkOut, (*[CurveKeySize]byte)(pub)) + if !success { + return nil, errors.New("failed to convert public key") + } + return (*publicCurve25519)(pkOut), nil +} + +// TODO: consider wrapping agl's secret key conversion instead of reimplementing + +// secretEd25519toCurve25519 converts a secret key from Ed25519 to curve25519 format +// Made with reference to https://github.com/agl/ed25519/blob/master/extra25519/extra25519.go and +// https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_sign/ed25519/ref10/keypair.c#L70 +func secretEd25519toCurve25519(priv *privateEd25519) (*privateCurve25519, error) { + hasher := sha512.New() + _, err := hasher.Write(priv[:32]) + if err != nil { + return nil, err + } + + hash := hasher.Sum(nil) + + hash[0] &= 248 // clr lower 3 bits + hash[31] &= 127 // clr upper 1 bit + hash[31] |= 64 // set 6th bit + + out := new([CurveKeySize]byte) + copy(out[:], hash) + return (*privateCurve25519)(out), nil +} + +func makeNonce(pub1, pub2 []byte) ([]byte, error) { + var nonce [24]byte + // generate an equivalent nonce to libsodium's (see link above) + nonceWriter, err := blake2b.New(24, nil) + if err != nil { + return nil, err + } + _, err = nonceWriter.Write(pub1) + if err != nil { + return nil, err + } + _, err = nonceWriter.Write(pub2) + if err != nil { + return nil, err + } + + nonceOut := nonceWriter.Sum(nil) + copy(nonce[:], nonceOut) + + return nonce[:], nil +} + +// TODO dupe of jwe/authcrypt encryptOID, refactor out + +// sodiumBoxSeal will encrypt a msg (in the case of this package, it will be +// an ephemeral key concatenated to the sender's public key) using the +// recipient's pubKey, this is equivalent to libsodium's C function: crypto_box_seal() +// https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes#usage +func sodiumBoxSeal(msg []byte, recPub *publicCurve25519) ([]byte, error) { + var nonce [24]byte + // generate ephemeral curve25519 asymmetric keys + epk, esk, err := box.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + // generate an equivalent nonce to libsodium's (see link above) + nonceSlice, err := makeNonce(epk[:], recPub[:]) + if err != nil { + return nil, err + } + copy(nonce[:], nonceSlice) + + var out = make([]byte, len(epk)) + copy(out, epk[:]) + + // now seal the msg with the ephemeral key, nonce and recPub (which is recipient's publicKey) + ret := box.Seal(out, msg, &nonce, (*[32]byte)(recPub), esk) + + return ret, nil +} + +func prettyPrint(msg []byte) (string, error) { + var prettyJSON bytes.Buffer + err := json.Indent(&prettyJSON, msg, "", "\t") + if err != nil { + return "", err + } + + return prettyJSON.String(), nil +} diff --git a/pkg/didcomm/crypto/legacy/authcrypt/authcrypt_test.go b/pkg/didcomm/crypto/legacy/authcrypt/authcrypt_test.go new file mode 100644 index 0000000000..2946862cb9 --- /dev/null +++ b/pkg/didcomm/crypto/legacy/authcrypt/authcrypt_test.go @@ -0,0 +1,198 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package authcrypt + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "testing" + + "github.com/btcsuite/btcutil/base58" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/nacl/box" + "golang.org/x/crypto/nacl/sign" +) + +func TestEncrypt(t *testing.T) { + + /* + Note: box.GenerateKey() generates a curve25519 keypair, not an Ed25519 keypair. + I think we should be generating Ed25519 keypairs since they are converted into + curve25519 keypairs for processing. + + TODO: use Ed25519 keys, convert to curve25519 internally, encode the Ed25519 form for message packing + */ + + t.Run("Success test case: given keys, generate envelope", func(t *testing.T) { + + senderKey := getB58EdKey( + "Bxp2KpXeh6RgXXRVGRQUskT9qT35aSSz1JvdbMUcB2Yc", + "2QqgiHtrUtDPpfoZG2C3Qi8a1MbLQuTZaaScu5LzQbUCkw5YnXngKLMJ8VuPgoN3Piqt1PBUACVd6uQRmtayZp2x") + + print("sendPK: ", base58.Encode(senderKey.pub[:]), "\n") + print("sendSK: ", base58.Encode(senderKey.priv[:]), "\n") + + recipientKey := getB58EdKey( + "9ZeipG91uMRDkMbqgkJK2Fq59CoWwfeJx2e5Q543mU5Q", + "23Y2dbcT78KEV1T7niUAuf83J8Zta11FG88n6y6pWgZY35nWrhyxGcqCJV5ddHveRZecrhwku77ik7gPSLaXnJXt") + + crypter, e := New(senderKey, []*publicEd25519{recipientKey.pub}, C20P) + require.NoError(t, e) + require.NotEmpty(t, crypter) + enc, e := crypter.Encrypt([]byte("lorem ipsum dolor sit amet")) + require.NoError(t, e) + require.NotEmpty(t, enc) + + printPythonData(recipientKey.pub[:], recipientKey.priv[:], enc) + }) + + t.Run("Generate keys, generate envelope", func(t *testing.T) { + senderKey, err := randEdKeyPair(rand.Reader) + require.NoError(t, err) + recipientKey, err := randEdKeyPair(rand.Reader) + require.NoError(t, err) + + crypter, e := New(*senderKey, []*publicEd25519{recipientKey.pub}, C20P) + require.NoError(t, e) + require.NotEmpty(t, crypter) + enc, e := crypter.Encrypt([]byte("lorem ipsum dolor sit amet")) + require.NoError(t, e) + require.NotEmpty(t, enc) + + printPythonData(recipientKey.pub[:], recipientKey.priv[:], enc) + }) + + t.Run("Generate testcase with multiple recipients", func(t *testing.T) { + senderKey, err := randEdKeyPair(rand.Reader) + require.NoError(t, err) + rec1Key, err := randEdKeyPair(rand.Reader) + require.NoError(t, err) + rec2Key, err := randEdKeyPair(rand.Reader) + require.NoError(t, err) + rec3Key, err := randEdKeyPair(rand.Reader) + require.NoError(t, err) + rec4Key, err := randEdKeyPair(rand.Reader) + require.NoError(t, err) + + recipientKeys := []*publicEd25519{ + rec1Key.pub, + rec2Key.pub, + rec3Key.pub, + rec4Key.pub, + } + + crypter, e := New(*senderKey, recipientKeys, C20P) + require.NoError(t, e) + require.NotEmpty(t, crypter) + enc, e := crypter.Encrypt([]byte("The quick brown fox jumped over the lazy dog")) + require.NoError(t, e) + require.NotEmpty(t, enc) + + printPythonData(rec3Key.pub[:], rec3Key.priv[:], enc) + }) +} + +func TestSodiumBoxSeal(t *testing.T) { + var err error + + recipient1Key, err := randCurveKeyPair(rand.Reader) + require.NoError(t, err) + + t.Run("Generate a box_seal message to compare to ACA-Py:", func(t *testing.T) { + + msg := []byte("lorem ipsum dolor sit amet consectetur adipiscing elit ") + + enc, err := sodiumBoxSeal(msg, recipient1Key.pub) + require.NoError(t, err) + + // Python implementation expects the nacl 64-byte key format + t.Logf("Recipient VK: %s", base64.URLEncoding.EncodeToString(recipient1Key.pub[:])) + t.Logf("Recipient SK: %s", base64.URLEncoding.EncodeToString(recipient1Key.priv[:])) + t.Logf("sodiumBoxSeal() -> %s", base64.URLEncoding.EncodeToString(enc)) + }) + + t.Run("Seal a message with sodiumBoxSeal and unseal it with sodiumBoxSealOpen", func(t *testing.T) { + + msg := []byte("lorem ipsum dolor sit amet consectetur adipiscing elit ") + + enc, err := sodiumBoxSeal(msg, recipient1Key.pub) + require.NoError(t, err) + dec, err := sodiumBoxSealOpen(enc, recipient1Key.pub, recipient1Key.priv) + require.NoError(t, err) + + require.Equal(t, msg, dec) + }) + + t.Run("Seal message, present signing key", func(t *testing.T) { + + rec := getB58CurveKey( + "DJuB84EKcHjMcwRKV2CP6pDSWG8xL8V2yntcLpvuHTj4", + "9foNvM6BPbcAohay8cEkDG6BZj26Tave6k1mGcPx63yW") + + msg := []byte("lorem ipsum dolor sit amet consectetur adipiscing elit ") + + enc, err := sodiumBoxSeal(msg, rec.pub) + require.NoError(t, err) + + // Python implementation expects the nacl 64-byte key format + t.Logf("sodiumBoxSeal() -> %s", base64.URLEncoding.EncodeToString(enc)) + }) +} + +// printPythonData prints the recipient key and message envelope +func printPythonData(recPub, recPriv, envelope []byte) { + print("## PASTE DATA ##\n") + print("b58_pub = \"", base58.Encode(recPub), "\"\n") + print("b58_priv = \"", base58.Encode(recPriv), "\"\n") + fmt.Printf("msg_in = \"\"\"%s\n\"\"\"\n", envelope) + print("## END PASTE ##\n") +} + +func randEdKeyPair(randReader io.Reader) (*keyPairEd25519, error) { + keyPair := keyPairEd25519{} + pk, sk, err := sign.GenerateKey(randReader) + if err != nil { + return nil, err + } + keyPair.pub, keyPair.priv = (*publicEd25519)(pk), (*privateEd25519)(sk) + return &keyPair, nil +} + +func randCurveKeyPair(randReader io.Reader) (*keyPairCurve25519, error) { + keyPair := keyPairCurve25519{} + pk, sk, err := box.GenerateKey(randReader) + if err != nil { + return nil, err + } + keyPair.pub, keyPair.priv = (*publicCurve25519)(pk), (*privateCurve25519)(sk) + return &keyPair, nil +} + +func getB58EdKey(pub, priv string) keyPairEd25519 { + key := keyPairEd25519{new(privateEd25519), new(publicEd25519)} + pubk := base58.Decode(pub) + privk := base58.Decode(priv) + + copy(key.pub[:], pubk) + copy(key.priv[:], privk) + + return key +} + +func getB58CurveKey(pub, priv string) keyPairCurve25519 { + + key := keyPairCurve25519{new(privateCurve25519), new(publicCurve25519)} + pubk := base58.Decode(pub) + privk := base58.Decode(priv) + + copy(key.pub[:], pubk) + copy(key.priv[:], privk) + + return key +} diff --git a/pkg/didcomm/crypto/legacy/authcrypt/decrypt.go b/pkg/didcomm/crypto/legacy/authcrypt/decrypt.go new file mode 100644 index 0000000000..6191188e99 --- /dev/null +++ b/pkg/didcomm/crypto/legacy/authcrypt/decrypt.go @@ -0,0 +1,44 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package authcrypt + +import ( + "errors" + + chacha "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/nacl/box" +) + +// Decrypt will decode the envelope using the legacy format +// Using (X)Chacha20 encryption algorithm and Poly1035 authenticator +func (c *Crypter) Decrypt(envelope []byte, recipientPrivKey *[chacha.KeySize]byte) ([]byte, error) { + // TODO implement legacy auth decrypt + return nil, nil +} + +// Open a box sealed by sodiumBoxSeal +func sodiumBoxSealOpen(msg []byte, recPub *publicCurve25519, recPriv *privateCurve25519) ([]byte, error) { + if len(msg) < 32 { + return nil, errors.New("message too short") + } + var epk [32]byte + copy(epk[:], msg[:32]) + + var nonce [24]byte + nonceSlice, err := makeNonce(epk[:], recPub[:]) + if err != nil { + return nil, err + } + copy(nonce[:], nonceSlice) + + out, success := box.Open(nil, msg[32:], &nonce, &epk, (*[32]byte)(recPriv)) + if !success { + return nil, errors.New("failed to unpack") + } + + return out, nil +} diff --git a/pkg/didcomm/crypto/legacy/authcrypt/encrypt.go b/pkg/didcomm/crypto/legacy/authcrypt/encrypt.go new file mode 100644 index 0000000000..bd2a9b8725 --- /dev/null +++ b/pkg/didcomm/crypto/legacy/authcrypt/encrypt.go @@ -0,0 +1,211 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package authcrypt + +import ( + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + + "github.com/btcsuite/btcutil/base58" + chacha "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/nacl/box" + "golang.org/x/crypto/poly1305" +) + +// TODO: refactor Crypter into legacy/authcrypt/authcrypt.go, encrypt into legacy/authcrypt/encrypt.go, +// shared code into crypto/internal, and also add decrypt.go + +// TODO generate testcases in ACA-Py, make tests here + +// Encrypt will encode the payload argument +// Using the protocol defined by Aries RFC 0019 +func (c *Crypter) Encrypt(payload []byte) ([]byte, error) { + var _, err error + + nonce := make([]byte, c.nonceSize) + _, err = rand.Reader.Read(nonce) + if err != nil { + return nil, err + } + + var cek *[chacha.KeySize]byte + + // cek (content encryption key) is a symmetric key + _, cek, err = box.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + + // create a cipher for the given nonceSize and generated cek above + chachaCipher, err := createCipher(c.nonceSize, cek[:]) + if err != nil { + return nil, err + } + + var recipients []Recipient + + recipients, err = c.buildRecipients(cek) + if err != nil { + return nil, err + } + + protectedBytes, err := c.buildProtected(recipients) + if err != nil { + return nil, err + } + + pld := payload + + AAD := base64.URLEncoding.EncodeToString(protectedBytes) + + // Additional data is b64encode(jsonenc(encodedRecipients)) + // see https://github.com/hyperledger/aries-cloudagent-python/blob/master/aries_cloudagent/wallet/crypto.py#L425 + symPld := chachaCipher.Seal(nil, nonce, pld, []byte(AAD)) + + // symPld has a length of len(pld) + poly1035.TagSize + // fetch the tag from the tail + tag := symPld[len(symPld)-poly1305.TagSize:] + // fetch the cipherText from the head (0:up to the trailing tag) + cipherText := symPld[0 : len(symPld)-poly1305.TagSize] + + out, err := c.buildJSON(protectedBytes, nonce, cipherText, tag) + if err != nil { + return nil, err + } + + return out, nil +} + +// TODO refactor dupe +// TODO legacy encryption uses C20P only, enforce +// createCipher will create and return a new Chacha20Poly1035 cipher for the given nonceSize and symmetric key +func createCipher(nonceSize int, symKey []byte) (cipher.AEAD, error) { + switch nonceSize { + case chacha.NonceSize: + return chacha.New(symKey) + case chacha.NonceSizeX: + return chacha.NewX(symKey) + default: + return nil, errors.New("cipher cannot be created with bad nonce size and shared symmetric Key combo") + } +} + +// buildJSON builds the JSON message following the legacy format +func (c *Crypter) buildJSON(protectedBytes, nonce, cipherText, tag []byte) ([]byte, error) { + + envelope := &Envelope{ + Protected: base64.URLEncoding.EncodeToString(protectedBytes), + IV: base64.URLEncoding.EncodeToString(nonce), + CipherText: base64.URLEncoding.EncodeToString(cipherText), + Tag: base64.URLEncoding.EncodeToString(tag), + } + + jsonOut, err := json.Marshal(envelope) + if err != nil { + return nil, err + } + + return jsonOut, nil +} + +func (c *Crypter) buildProtected(recipients []Recipient) ([]byte, error) { + + var enc string + switch c.alg { + case C20P: + enc = "chacha20poly1305_ietf" + case XC20P: + enc = "xchacha20poly1305_ietf" + } + + protected := Protected{ + Enc: enc, + Typ: "JWM/1.0", + Alg: "Authcrypt", + Recipients: recipients, + } + + protectedBytes, err := json.Marshal(protected) + if err != nil { + return nil, err + } + + return protectedBytes, nil +} + +func (c *Crypter) buildRecipients(cek *[chacha.KeySize]byte) ([]Recipient, error) { + var encodedRecipients []Recipient + + for _, recKey := range c.recipients { + recipient, err := c.buildRecipient(cek, recKey) + if err != nil { + return nil, err + } + + encodedRecipients = append(encodedRecipients, *recipient) + } + + return encodedRecipients, nil +} + +// Note: the spec is incorrect with the parameters for encrypted_key: the CEK is the message to encrypt +// https://github.com/hyperledger/aries-cloudagent-python/blob/master/aries_cloudagent/wallet/crypto.py#L277 + +/* +{ + "encrypted_key": base64URLencode(libsodium.crypto_box(my_key, their_vk, cek, cek_iv)) + ^ this is crypto_box_easy(out, cek, rec_nonce, rec_pub, my_priv) + "header": { + "kid": "base58encode(recipient_verkey)", + "sender" : base64URLencode(libsodium.crypto_box_seal(their_vk, base58encode(sender_vk)), + ^ this is crypto_box_seal(base58encode(my_pub), rec_pub) + "iv" : base64URLencode(cek_iv) + } +} +*/ + +// buildRecipient encodes the necessary data for the recipient to decrypt the message +// encrypting the CEK and sender pub key +func (c *Crypter) buildRecipient(cek *[chacha.KeySize]byte, recKey *publicEd25519) (*Recipient, error) { + + var nonce [24]byte + + _, err := rand.Reader.Read(nonce[:]) + if err != nil { + return nil, err + } + + senderSKCurve, err := secretEd25519toCurve25519(c.sender.priv) + if err != nil { + return nil, err + } + + recPKCurve, err := publicEd25519toCurve25519(recKey) + if err != nil { + return nil, err + } + + encCEK := box.Seal(nil, cek[:], &nonce, (*[32]byte)(recPKCurve), (*[32]byte)(senderSKCurve)) + + var encSender []byte + encSender, err = sodiumBoxSeal([]byte(base58.Encode(c.sender.pub[:])), recPKCurve) + if err != nil { + return nil, err + } + + return &Recipient{ + EncryptedKey: base64.URLEncoding.EncodeToString(encCEK), + Header: RecipientHeader{ + KID: base58.Encode(recKey[:]), // recKey is the Ed25519 pk + Sender: base64.URLEncoding.EncodeToString(encSender), + IV: base64.URLEncoding.EncodeToString(nonce[:]), + }, + }, nil +}