From 0354cecb58acb74b881bb05583b8d6a20ecb0d39 Mon Sep 17 00:00:00 2001 From: Baha Shaaban Date: Mon, 9 Sep 2019 14:18:55 -0400 Subject: [PATCH] Authcrypt Decrypt Using (X)Chacha20Poly1035 Using RawURLEncoding for Base64 encoding to allow interoperability with non Go agents Signed-off-by: Baha Shaaban --- pkg/didcomm/crypto/jwe/authcrypt/authcrypt.go | 47 ++- .../crypto/jwe/authcrypt/authcrypt_test.go | 358 ++++++++++++++++-- pkg/didcomm/crypto/jwe/authcrypt/decrypt.go | 179 +++++++-- pkg/didcomm/crypto/jwe/authcrypt/encrypt.go | 127 ++++--- 4 files changed, 589 insertions(+), 122 deletions(-) diff --git a/pkg/didcomm/crypto/jwe/authcrypt/authcrypt.go b/pkg/didcomm/crypto/jwe/authcrypt/authcrypt.go index d7263de40..5b5f3b21a 100644 --- a/pkg/didcomm/crypto/jwe/authcrypt/authcrypt.go +++ b/pkg/didcomm/crypto/jwe/authcrypt/authcrypt.go @@ -9,9 +9,8 @@ package authcrypt import ( "crypto/rand" "errors" - "fmt" - "golang.org/x/crypto/chacha20poly1305" + chacha "golang.org/x/crypto/chacha20poly1305" ) // This package deals with Authcrypt encryption for Packing/Unpacking DID Comm exchange @@ -31,17 +30,27 @@ const XC20P = ContentEncryption("XC20P") // XChacha20 encryption + Poly1035 auth //nolint:gochecknoglobals var randReader = rand.Reader +// errEmptyRecipients is used when recipients list is empty +var errEmptyRecipients = errors.New("empty recipients") + +// errInvalidKeypair is used when a keypair is invalid +var errInvalidKeypair = errors.New("invalid keypair") + +// errRecipientNotFound is used when a recipient is not found +var errRecipientNotFound = errors.New("recipient not found") + +// errUnsupportedAlg is used when a bad encryption algorithm is used +var errUnsupportedAlg = errors.New("algorithm not supported") + type keyPair struct { - priv *[chacha20poly1305.KeySize]byte - pub *[chacha20poly1305.KeySize]byte + priv *[chacha.KeySize]byte + pub *[chacha.KeySize]byte } // Crypter represents an Authcrypt Encrypter (Decrypter) that outputs/reads JWE envelopes type Crypter struct { - sender keyPair - recipients []*[chacha20poly1305.KeySize]byte - alg ContentEncryption - nonceSize int + alg ContentEncryption + nonceSize int } // Envelope represents a JWE envelope as per the Aries Encryption envelope specs @@ -77,37 +86,27 @@ type RecipientHeaders struct { // C20P (chacha20-poly1035 ietf) // XC20P (xchacha20-poly1035 ietf) // The returned crypter contains all the information required to encrypt payloads. -func New(sender keyPair, recipients []*[chacha20poly1305.KeySize]byte, alg ContentEncryption) (*Crypter, error) { +func New(alg ContentEncryption) (*Crypter, error) { var nonceSize int switch alg { case C20P: - nonceSize = chacha20poly1305.NonceSize + nonceSize = chacha.NonceSize case XC20P: - nonceSize = chacha20poly1305.NonceSizeX + nonceSize = chacha.NonceSizeX default: - return nil, fmt.Errorf("encryption algorithm '%s' not supported", alg) + return nil, errUnsupportedAlg } - if len(recipients) == 0 { - return nil, errors.New("empty recipients keys, must have at least one recipient") - } - var recipientsKey []*[chacha20poly1305.KeySize]byte - recipientsKey = append(recipientsKey, recipients...) c := &Crypter{ - sender, - recipientsKey, alg, nonceSize, } - if !isKeyPairValid(sender) { - return nil, fmt.Errorf("sender keyPair not supported, it must have %d bytes keys", chacha20poly1305.KeySize) - } - return c, nil } -func isKeyPairValid(kp keyPair) bool { +// IsKeyPairValid is a utility function that validates a KeyPair +func IsKeyPairValid(kp keyPair) bool { if kp.priv == nil || kp.pub == nil { return false } diff --git a/pkg/didcomm/crypto/jwe/authcrypt/authcrypt_test.go b/pkg/didcomm/crypto/jwe/authcrypt/authcrypt_test.go index dac771108..5b493793e 100644 --- a/pkg/didcomm/crypto/jwe/authcrypt/authcrypt_test.go +++ b/pkg/didcomm/crypto/jwe/authcrypt/authcrypt_test.go @@ -8,7 +8,9 @@ package authcrypt import ( "bytes" + "encoding/base64" "encoding/json" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -22,43 +24,63 @@ func TestEncrypt(t *testing.T) { sendEcKey := keyPair{} sendEcKey.pub, sendEcKey.priv, err = box.GenerateKey(randReader) require.NoError(t, err) + t.Logf("sender key pub: %v", base64.RawURLEncoding.EncodeToString(sendEcKey.pub[:])) + t.Logf("sender key priv: %v", base64.RawURLEncoding.EncodeToString(sendEcKey.priv[:])) recipient1Key := keyPair{} recipient1Key.pub, recipient1Key.priv, err = box.GenerateKey(randReader) require.NoError(t, err) + t.Logf("recipient1Key pub: %v", base64.RawURLEncoding.EncodeToString(recipient1Key.pub[:])) + t.Logf("recipient1Key priv: %v", base64.RawURLEncoding.EncodeToString(recipient1Key.priv[:])) recipient2Key := keyPair{} recipient2Key.pub, recipient2Key.priv, err = box.GenerateKey(randReader) require.NoError(t, err) + t.Logf("recipient2Key pub: %v", base64.RawURLEncoding.EncodeToString(recipient2Key.pub[:])) + t.Logf("recipient2Key priv: %v", base64.RawURLEncoding.EncodeToString(recipient2Key.priv[:])) recipient3Key := keyPair{} recipient3Key.pub, recipient3Key.priv, err = box.GenerateKey(randReader) require.NoError(t, err) + t.Logf("recipient3Key pub: %v", base64.RawURLEncoding.EncodeToString(recipient3Key.pub[:])) + t.Logf("recipient3Key priv: %v", base64.RawURLEncoding.EncodeToString(recipient3Key.priv[:])) badKey := keyPair{ pub: nil, priv: nil, } + t.Run("Error test case: Create a new AuthCrypter with bad encryption algorithm", func(t *testing.T) { - _, e := New(sendEcKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}, "BAD") + _, e := New("BAD") require.Error(t, e) + require.EqualError(t, e, errUnsupportedAlg.Error()) }) - t.Run("Error test case: Create a new AuthCrypter with bad sender key", func(t *testing.T) { - _, e := New(badKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}, XC20P) + t.Run("Error test case: Create a new AuthCrypter and use a bad sender key for encryption", func(t *testing.T) { + crypter, e := New(XC20P) + require.NoError(t, e) + require.NotEmpty(t, crypter) + enc, e := crypter.Encrypt([]byte("lorem ipsum dolor sit amet"), + badKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}) require.Error(t, e) + require.Empty(t, enc) }) - t.Run("Error test case: Create a new AuthCrypter with bad recipient key", func(t *testing.T) { - _, e := New(sendEcKey, []*[chacha.KeySize]byte{}, "XC20P") + t.Run("Error test case: Create a new AuthCrypter and use an empty recipient keys list for encryption", func(t *testing.T) { //nolint:lll + crypter, e := New("XC20P") + require.NoError(t, e) + require.NotEmpty(t, crypter) + enc, e := crypter.Encrypt([]byte("lorem ipsum dolor sit amet"), sendEcKey, []*[chacha.KeySize]byte{}) require.Error(t, e) + require.Empty(t, enc) }) t.Run("Success test case: Create a valid AuthCrypter for ChachaPoly1035 encryption (alg: C20P)", func(t *testing.T) { - crypter, e := New(sendEcKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}, C20P) + crypter, e := New(C20P) require.NoError(t, e) require.NotEmpty(t, crypter) - enc, e := crypter.Encrypt([]byte("lorem ipsum dolor sit amet")) + enc, e := crypter.Encrypt([]byte("lorem ipsum dolor sit amet"), + sendEcKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}) require.NoError(t, e) require.NotEmpty(t, enc) @@ -68,10 +90,11 @@ func TestEncrypt(t *testing.T) { }) t.Run("Success test case: Create a valid AuthCrypter for XChachaPoly1035 encryption (alg: XC20P)", func(t *testing.T) { - crypter, e := New(sendEcKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}, XC20P) + crypter, e := New(XC20P) require.NoError(t, e) require.NotEmpty(t, crypter) - enc, e := crypter.Encrypt([]byte("lorem ipsum dolor sit amet")) + enc, e := crypter.Encrypt([]byte("lorem ipsum dolor sit amet"), + sendEcKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}) require.NoError(t, e) require.NotEmpty(t, enc) @@ -81,29 +104,274 @@ func TestEncrypt(t *testing.T) { t.Run("Error test Case: use a valid AuthCrypter but scramble the nonce size", func(t *testing.T) { crypter.nonceSize = 0 - _, err = crypter.Encrypt([]byte("lorem ipsum dolor sit amet")) + _, err = crypter.Encrypt([]byte("lorem ipsum dolor sit amet"), + sendEcKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}) require.Error(t, err) }) }) - // TODO add Decrypt test cases once implemented - t.Run("Error test Case [INCOMPLETE]: Test Decrypting a message should fail as it's not implemented yet", func(t *testing.T) { //nolint:lll - crypter, e := New(sendEcKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}, XC20P) + t.Run("Success test case: Decrypting a message (with the same crypter)", func(t *testing.T) { + crypter, e := New(XC20P) + require.NoError(t, e) + require.NotEmpty(t, crypter) + pld := []byte("lorem ipsum dolor sit amet") + enc, e := crypter.Encrypt(pld, sendEcKey, + []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}) + require.NoError(t, e) + require.NotEmpty(t, enc) + + m, e := prettyPrint(enc) + require.NoError(t, e) + t.Logf("Encryption with unescaped XC20P: %s", enc) + t.Logf("Encryption with XC20P: %s", m) + + // decrypt for recipient1 + dec, e := crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.NoError(t, e) + require.NotEmpty(t, dec) + require.EqualValues(t, dec, pld) + }) + + t.Run("Success test case: Decrypting a message with two Crypter instances to simulate two agents", func(t *testing.T) { + crypter, e := New(XC20P) + require.NoError(t, e) + require.NotEmpty(t, crypter) + pld := []byte("lorem ipsum dolor sit amet") + enc, e := crypter.Encrypt(pld, sendEcKey, + []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}) + require.NoError(t, e) + require.NotEmpty(t, enc) + + m, e := prettyPrint(enc) + require.NoError(t, e) + t.Logf("Encryption with unescaped XC20P: %s", enc) + t.Logf("Encryption with XC20P: %s", m) + + // now decrypt with recipient3 + crypter1, e := New(XC20P) + require.NoError(t, e) + dec, e := crypter1.Decrypt(enc, recipient3Key.priv, []*[chacha.KeySize]byte{recipient3Key.pub}) + require.NoError(t, e) + require.NotEmpty(t, dec) + require.EqualValues(t, dec, pld) + + // now try decrypting with recipient2 + crypter2, e := New(XC20P) + require.NoError(t, e) + dec, e = crypter2.Decrypt(enc, recipient2Key.priv, []*[chacha.KeySize]byte{recipient2Key.pub}) + require.NoError(t, e) + require.NotEmpty(t, dec) + require.EqualValues(t, dec, pld) + t.Logf("Decryption Payload with XC20P: %s", pld) + }) + + t.Run("Failure test case: Decrypting a message with an unauthorized (recipient2) agent", func(t *testing.T) { + crypter, e := New(XC20P) require.NoError(t, e) require.NotEmpty(t, crypter) - enc, e := crypter.Encrypt([]byte("lorem ipsum dolor sit amet")) + pld := []byte("lorem ipsum dolor sit amet") + enc, e := crypter.Encrypt(pld, sendEcKey, []*[chacha.KeySize]byte{recipient1Key.pub, recipient3Key.pub}) require.NoError(t, e) require.NotEmpty(t, enc) m, e := prettyPrint(enc) require.NoError(t, e) + t.Logf("Encryption with unescaped XC20P: %s", enc) t.Logf("Encryption with XC20P: %s", m) - dec, e := crypter.Decrypt(enc, recipient1Key.priv) + // decrypting for recipient 2 (unauthorized) + crypter1, e := New(XC20P) + require.NoError(t, e) + dec, e := crypter1.Decrypt(enc, recipient2Key.priv, []*[chacha.KeySize]byte{recipient2Key.pub}) + require.Error(t, e) + require.Empty(t, dec) + + // now try to decrypt with an invalid recipient who's trying to use another agent's key + crypter1, e = New(XC20P) + require.NoError(t, e) + dec, e = crypter1.Decrypt(enc, recipient2Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) require.Error(t, e) require.Empty(t, dec) }) + t.Run("Failure test case: Decrypting a message but scramble JWE beforehand", func(t *testing.T) { + crypter, e := New(XC20P) + require.NoError(t, e) + require.NotEmpty(t, crypter) + pld := []byte("lorem ipsum dolor sit amet") + enc, e := crypter.Encrypt(pld, sendEcKey, + []*[chacha.KeySize]byte{recipient1Key.pub, recipient2Key.pub, recipient3Key.pub}) + require.NoError(t, e) + require.NotEmpty(t, enc) + + var validJwe *Envelope + e = json.Unmarshal(enc, &validJwe) + require.NoError(t, e) + + // make a jwe copy to test with scrambling its values + jwe := &Envelope{} + deepCopy(jwe, validJwe) + + // test decrypting with empty recipients + dec, e := crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{}) + require.EqualError(t, e, errEmptyRecipients.Error()) // nolint:lll + require.Empty(t, dec) + + // test bad jwe format + enc = []byte("{badJWE}") + + // update jwe with bad cipherText format + jwe.CipherText = "badCipherText" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad nonce format + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt message: illegal base64 data at input byte 12") + require.Empty(t, dec) + jwe.CipherText = validJwe.CipherText + + // update jwe with bad nonce format + jwe.IV = "badIV!" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad nonce format + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt message: illegal base64 data at input byte 5") + require.Empty(t, dec) + jwe.IV = validJwe.IV + + // update jwe with bad tag format + jwe.Tag = "badTag!" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag format + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt message: illegal base64 data at input byte 6") + require.Empty(t, dec) + jwe.Tag = validJwe.Tag + + // update jwe with bad aad format + jwe.AAD = "badAAD" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt message: failed to decrypt message - invalid AAD in envelope") + require.Empty(t, dec) + jwe.AAD = validJwe.AAD + + // update jwe with bad recipient oid format + jwe.Recipients[0].Header.OID = "badOID!" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt message: illegal base64 data at input byte 6") + require.Empty(t, dec) + jwe.Recipients[0].Header.OID = validJwe.Recipients[0].Header.OID + + // update jwe with bad recipient tag format + jwe.Recipients[0].Header.Tag = "badTag!" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt shared key: illegal base64 data at input byte 6") + require.Empty(t, dec) + jwe.Recipients[0].Header.Tag = validJwe.Recipients[0].Header.Tag + + // update jwe with bad recipient nonce format + jwe.Recipients[0].Header.IV = "badIV!" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt shared key: illegal base64 data at input byte 5") + require.Empty(t, dec) + jwe.Recipients[0].Header.IV = validJwe.Recipients[0].Header.IV + + // update jwe with bad recipient apu format + jwe.Recipients[0].Header.APU = "badAPU!" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt shared key: illegal base64 data at input byte 6") + require.Empty(t, dec) + jwe.Recipients[0].Header.APU = validJwe.Recipients[0].Header.APU + + // update jwe with bad recipient kid (sender) format + jwe.Recipients[0].Header.KID = "badKID!" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, fmt.Sprintf("failed to decrypt message: %s", errRecipientNotFound.Error())) + require.Empty(t, dec) + jwe.Recipients[0].Header.KID = validJwe.Recipients[0].Header.KID + + // update jwe with bad recipient CEK (encrypted key) format + jwe.Recipients[0].EncryptedKey = "badEncryptedKey!" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt shared key: illegal base64 data at input byte 15") + require.Empty(t, dec) + jwe.Recipients[0].EncryptedKey = validJwe.Recipients[0].EncryptedKey + + // update jwe with bad recipient CEK (encrypted key) value + jwe.Recipients[0].EncryptedKey = "Np2ZIsTdsM190yv_v3FkfjVshGqAUvH4KfWOnQE8wl4" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt shared key: chacha20poly1305: message authentication failed") + require.Empty(t, dec) + jwe.Recipients[0].EncryptedKey = validJwe.Recipients[0].EncryptedKey + + // now try bad nonce size + jwe.IV = "ouaN1Qm8cUzNr1IC" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad nonce value + require.PanicsWithValue(t, "chacha20poly1305: bad nonce length passed to Open", func() { + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + }) + require.Empty(t, dec) + jwe.IV = validJwe.IV + + // now try bad nonce value + jwe.Recipients[0].Header.IV = "dZY1WrG0IeIOfLJG8FMLkf3BUqUCe0xI" + enc, e = json.Marshal(jwe) + require.NoError(t, e) + // decrypt with bad tag + dec, e = crypter.Decrypt(enc, recipient1Key.priv, []*[chacha.KeySize]byte{recipient1Key.pub}) + require.EqualError(t, e, "failed to decrypt shared key: chacha20poly1305: message authentication failed") + require.Empty(t, dec) + jwe.Recipients[0].Header.IV = validJwe.Recipients[0].Header.IV + }) +} + +func deepCopy(envelope, envelope2 *Envelope) { + for _, r := range envelope2.Recipients { + newRe := Recipient{ + EncryptedKey: r.EncryptedKey, + Header: RecipientHeaders{ + APU: r.Header.APU, + KID: r.Header.KID, + IV: r.Header.IV, + OID: r.Header.OID, + Tag: r.Header.Tag, + }, + } + envelope.Recipients = append(envelope.Recipients, newRe) + } + envelope.CipherText = envelope2.CipherText + envelope.IV = envelope2.IV + envelope.Protected = envelope2.Protected + envelope.AAD = envelope2.AAD + envelope.Tag = envelope2.Tag } func prettyPrint(msg []byte) (string, error) { @@ -122,32 +390,45 @@ func TestBadCreateCipher(t *testing.T) { } func TestRefEncrypt(t *testing.T) { - // reference from + // reference php crypto material similar to // https://github.com/hyperledger/aries-rfcs/issues/133#issuecomment-518922447 - var senderPriv = "6tsNPgZAg-NWM3s4S0VOOWM2yrcOfwsCrN0JGFEWaWw" - var senderPub = "QbvqozxGQ3U8FDLmlKOx8Hd5GiozMRO2pwrevZ5ZFTM" - var recipientPub = "-u0zk9iY_ZS2wP2z4zuLjR7_kz_kxVU0anRz8_A66T0" - var payload = []byte("SGVsbG8gV29ybGQh") + var senderPrivStr = "XZOBB1M7ikDoFR86rSgAuVt1ACJDMJ9alxHUsND6MBo" + senderPriv, err := base64.RawURLEncoding.DecodeString(senderPrivStr) + require.NoError(t, err) + var senderPubStr = "qdXzr6z28ck-RfTEiaBZmHOwH11ow-CBfLo97Qe31j4" + senderPub, err := base64.RawURLEncoding.DecodeString(senderPubStr) + require.NoError(t, err) + var recipientPrivStr = "kE3RDpviO_lVI3hdi6CKfT2BbuPph4WjC2DnkX7fBz4" + recipientPriv, err := base64.RawURLEncoding.DecodeString(recipientPrivStr) + require.NoError(t, err) + var recipientPubStr = "800RcOPc9M8vFElpaHGkl-p9SpmY2Efm2MZW5tikf1c" + recipientPub, err := base64.RawURLEncoding.DecodeString(recipientPubStr) + require.NoError(t, err) + var payloadStr = "SGVsbG8gV29ybGQh" + payload, err := base64.RawURLEncoding.DecodeString(payloadStr) + require.NoError(t, err) + // refJWE created by executing PHP test code at: + // https://github.com/gamringer/php-authcrypt/blob/master/examples/1-crypt.php //nolint:lll const refJWE = `{ "protected": "eyJ0eXAiOiJwcnMuaHlwZXJsZWRnZXIuYXJpZXMtYXV0aC1tZXNzYWdlIiwiYWxnIjoiRUNESC1TUytYQzIwUEtXIiwiZW5jIjoiWEMyMFAifQ", "recipients": [ { - "encrypted_key": "Y6eYXyrOWj67oHh4hla_MS024oPtgWeM4LOPqwyrXTM", + "encrypted_key": "-MXYFTdqmcmw11apipgtcr-E365Yvk6_4d9cVxRs89U", "header": { - "apu": "MMapegFCsTTrygbuC80X0NeHjrtJ7Fh5d9CIl6pq4HVgYDAtjIS7dyQKXO-Vgan8ho33ZglRJCfW4Wx2pH3cNg", - "iv": "eBuzpjLTU16gmJvZKV3JShzvibJM6h7_", - "tag": "PKg4RLQ5hikKQ2Vq2SCqGg", - "kid": "HtWhz6QevaQF39Gv3Hvf7K6xo2FTViAkY22rhZpQrdWc", - "oid": "cOvRYUooq-y1TDjK3Lt3wCA3H-w9E6PJXNPDdIPLDDZlpE7QJvJuLIwJzSPqDhRvQUMaOYrXyLGAgdriGpKbKjWcLtQapjExq8sesL5bax68J46vv-2-GuDVbQ" + "apu": "cJn0DhdOCZAmuLCFuOM1R9v26aPVl-EMW5V_Y81zgkddrzw2WmAvdSbhrS0BHjAmRdsZ52fPYoveQZeQIIFqPw", + "iv": "s2rbhR-abOcLdJuZpvKLa_aLfhIqRyGL", + "tag": "vx1JrepSfW90QIbG7vRBWg", + "kid": "HNkELiimfV5S3VyMWVqCd6H77cE3FMhYgp2Gq4tvZRJn", + "oid": "KStwoVefDEH4gsdTQay0QyLkPMtXdJPJhMrO9hk-A0-cKE5sqBZxAIc4_iw7VOaSRLCMipZgYWD1epH1hQJbUMESQtuGUBCxVZCAJJYQNML7PtZz1wooCYyBfa4" } } ], - "aad": "FeI0LXy7m0-orE0VwiQU-2RjQyYMsnIvSEzpduiB7sY", - "iv": "9AL-EASXKfuonBKKxsPHSccrX2zy7j2l", - "tag": "IG6L99-sFnq3Cfz29Z-jDg", - "ciphertext": "IX7EQSrqhxL61YjE" + "aad": "xuT9nzr1gf7k9IlS2936LFnUoDb-Tu1cBa8fhfUgxGk", + "iv": "nM0UDDCERek5syITAHwzDPGiEVErTtpo", + "tag": "gu1ZvF35-JYMd1JITD0qeg", + "ciphertext": "ntZwQokGaZhnQ8L2" }` senderPrivK := &[32]byte{} @@ -161,11 +442,11 @@ func TestRefEncrypt(t *testing.T) { recipientPubK := &[32]byte{} copy(recipientPubK[:], recipientPub) - crypter, err := New(senderKp, []*[32]byte{recipientPubK}, XC20P) + crypter, err := New(XC20P) require.NoError(t, err) require.NotNil(t, crypter) - pld, err := crypter.Encrypt(payload) + pld, err := crypter.Encrypt(payload, senderKp, []*[32]byte{recipientPubK}) require.NoError(t, err) refPld, err := prettyPrint([]byte(refJWE)) @@ -182,4 +463,17 @@ func TestRefEncrypt(t *testing.T) { require.NoError(t, err) t.Logf("Reference JWE Ummarshalled: %s", refPldUnmarshalled) t.Logf("Encrypted JWE Ummarshalled: %s", encryptedPldUmarshalled) + + // try to decrypt the encrypted payload + recipientPrivK := &[32]byte{} + copy(recipientPrivK[:], recipientPriv) + dec, err := crypter.Decrypt(pld, recipientPrivK, []*[32]byte{recipientPubK}) + require.NoError(t, err) + require.NotEmpty(t, dec) + require.Equal(t, dec, payload) + + // TODO fix try to decrypt the reference payload + // dec, err = crypter.Decrypt([]byte(refJWE), recipientPrivK, []*[32]byte{recipientPubK}) + // require.NoError(t, err) + // require.NotEmpty(t, dec) } diff --git a/pkg/didcomm/crypto/jwe/authcrypt/decrypt.go b/pkg/didcomm/crypto/jwe/authcrypt/decrypt.go index 544740d6c..34665d825 100644 --- a/pkg/didcomm/crypto/jwe/authcrypt/decrypt.go +++ b/pkg/didcomm/crypto/jwe/authcrypt/decrypt.go @@ -7,54 +7,189 @@ SPDX-License-Identifier: Apache-2.0 package authcrypt import ( + "bytes" + "encoding/base64" "encoding/json" "errors" + "fmt" - "golang.org/x/crypto/blake2b" - "golang.org/x/crypto/chacha20poly1305" + "github.com/btcsuite/btcutil/base58" + chacha "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/nacl/box" ) -// Decrypt will JWE decode the envelope argument for the sender and recipients -// Using (X)Chacha20 encryption algorithm and Poly1035 authenticator -func (c *Crypter) Decrypt(envelope []byte, recipientPrivKey *[chacha20poly1305.KeySize]byte) ([]byte, error) { - // TODO implement decryption and call decryptOID for the recipient's OID +// Decrypt will JWE decode the envelope argument for the recipientPrivKey and validates +// the envelope's recipients has a match in recipients argument (usually 1, or if multiple +// then it uses the first one found). +// Using (X)Chacha20 cipher and Poly1035 authenticator for the encrypted payload and +// encrypted CEK +// And Using (x)Salsa20 cipher with 25519 keys (libsodium equivalent) for decrypting +// the sender's public key in the current recipient's header. +// The current recipient is the one with the sender's encrypted key that successfully +// decrypts with recipientPrivKey. +func (c *Crypter) Decrypt(envelope []byte, recipientPrivKey *[chacha.KeySize]byte, recipients []*[chacha.KeySize]byte) ([]byte, error) { //nolint:lll,funlen + if len(recipients) == 0 { + return nil, errEmptyRecipients + } + jwe := &Envelope{} err := json.Unmarshal(envelope, jwe) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt message: %w", err) + } + recipient, err := c.findRecipient(jwe.Recipients, recipients) + if err != nil { + return nil, fmt.Errorf("failed to decrypt message: %w", err) } var oid []byte - for _, recipient := range jwe.Recipients { - oid, err = decryptOID(recipientPrivKey, c.sender.pub, []byte(recipient.Header.OID)) - if oid != nil { - break + // TODO replace oid with JWK wrapped in spk recipient header + cryptedOID, err := base64.RawURLEncoding.DecodeString(recipient.Header.OID) + if err != nil { + return nil, fmt.Errorf("failed to decrypt message: %w", err) + } + recPubKey := base58.Decode(recipient.Header.KID) + var recipientPubKey [chacha.KeySize]byte + copy(recipientPubKey[:], recPubKey) + + oid, err = decryptOID(recipientPrivKey, &recipientPubKey, cryptedOID) + if err != nil { + return nil, fmt.Errorf("failed to decrypt sender key: %w", err) + } + // if oid is found, it means decrypting the sender's public key with this recipient is successful + // proceed with decrypting the recipient's shared key and use it to decrypt the JWE's real payload + if oid != nil { + senderKey := base58.Decode(string(oid)) + var senderPubKey [chacha.KeySize]byte + copy(senderPubKey[:], senderKey) + + sharedKey, er := c.decryptSharedKey(keyPair{priv: recipientPrivKey, pub: &recipientPubKey}, &senderPubKey, recipient) + if er != nil { + return nil, fmt.Errorf("failed to decrypt shared key: %w", er) + } + + symOutput, er := c.decryptPayload(sharedKey, jwe) + if er != nil { + return nil, fmt.Errorf("failed to decrypt message: %w", er) + } + + return symOutput, nil + } + + return nil, errors.New("failed to decrypt message - invalid sender key in envelope") +} + +func (c *Crypter) decryptPayload(cek []byte, jwe *Envelope) ([]byte, error) { + aad := retrieveAAD(jwe.Recipients) + aadEncoded := base64.RawURLEncoding.EncodeToString(aad) + if jwe.AAD != aadEncoded { + return nil, errors.New("failed to decrypt message - invalid AAD in envelope") + } + + crypter, er := createCipher(c.nonceSize, cek) + if er != nil { + return nil, er + } + + pldAAD := jwe.Protected + "." + aadEncoded + payload, er := base64.RawURLEncoding.DecodeString(jwe.CipherText) + if er != nil { + return nil, er + } + tag, er := base64.RawURLEncoding.DecodeString(jwe.Tag) + if er != nil { + return nil, er + } + nonce, er := base64.RawURLEncoding.DecodeString(jwe.IV) + if er != nil { + return nil, er + } + payload = append(payload, tag...) + return crypter.Open(nil, nonce, payload, []byte(pldAAD)) +} + +func retrieveAAD(recipients []Recipient) []byte { + var keys []string + for _, rec := range recipients { + keys = append(keys, rec.Header.KID) + } + return hashAAD(keys) +} + +// findRecipient will loop through jweRecipients and returns the first matching key from recipients +func (c *Crypter) findRecipient(jweRecipients []Recipient, recipients []*[chacha.KeySize]byte) (*Recipient, error) { + for _, recipient := range jweRecipients { + recipient := recipient // pin! + for _, rec := range recipients { + if bytes.Equal(rec[:], base58.Decode(recipient.Header.KID)) { + return &recipient, nil + } } } + return nil, errRecipientNotFound +} + +func (c *Crypter) decryptSharedKey(recipientKp keyPair, senderPubKey *[chacha.KeySize]byte, recipient *Recipient) ([]byte, error) { //nolint:lll + apu, err := base64.RawURLEncoding.DecodeString(recipient.Header.APU) + if err != nil { + return nil, err + } + + nonce, err := base64.RawURLEncoding.DecodeString(recipient.Header.IV) + if err != nil { + return nil, err + } + if len(nonce) != c.nonceSize { + return nil, errors.New("bad nonce size") + } + + tag, err := base64.RawURLEncoding.DecodeString(recipient.Header.Tag) + if err != nil { + return nil, err + } + + sharedEncryptedKey, err := base64.RawURLEncoding.DecodeString(recipient.EncryptedKey) + if err != nil { + return nil, err + } + + // create a new ephemeral key for the recipient and return its APU + kek, err := c.generateRecipientCEK(apu, recipientKp.priv, senderPubKey) + if err != nil { + return nil, err + } + + // create a new (chacha20poly1035) cipher with this new key to encrypt the shared key (cek) + cipher, err := createCipher(c.nonceSize, kek) + if err != nil { + return nil, err + } + + cipherText := sharedEncryptedKey + cipherText = append(cipherText, tag...) - return oid, err + return cipher.Open(nil, nonce, cipherText, nil) } // decryptOID will decrypt a recipient's encrypted OID (in the case of this package, it is represented as // ephemeral key concatenated with the sender's public key) using the recipient's privKey/pubKey keypair, -// this is equivalent to libsodium's C function: crypto_box_seal() +// this is equivalent to libsodium's C function: crypto_box_seal_open() // https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes#usage -func decryptOID(privKey, pubKey *[chacha20poly1305.KeySize]byte, encrypted []byte) ([]byte, error) { - var epk [32]byte - var nonce [24]byte - copy(epk[:], encrypted[:chacha20poly1305.KeySize]) +// the returned decrypted value is the sender's public key base58 encoded +// TODO replace 'OID' to 'SPK' recipient header which should represent the key in JWK encoded in a compact JWE format +func decryptOID(recipientPrivKey, recipientPubKey *[chacha.KeySize]byte, encrypted []byte) ([]byte, error) { + var epk [chacha.KeySize]byte + copy(epk[:], encrypted[:chacha.KeySize]) - nonceWriter, err := blake2b.New(24, nil) + // generate an equivalent nonce to libsodium's (see link above) + nonce, err := generateLibsodiumNonce(epk[:], recipientPubKey[:]) if err != nil { return nil, err } - nonceSlice := nonceWriter.Sum(append(epk[:], pubKey[:]...)) - copy(nonce[:], nonceSlice) - decrypted, ok := box.Open(nil, encrypted[32:], &nonce, &epk, privKey) + decrypted, ok := box.Open(nil, encrypted[chacha.KeySize:], nonce, &epk, recipientPrivKey) if !ok { - return nil, errors.New("decryption error") + return nil, errors.New("sender public key decryption error") } return decrypted, nil } diff --git a/pkg/didcomm/crypto/jwe/authcrypt/encrypt.go b/pkg/didcomm/crypto/jwe/authcrypt/encrypt.go index 5bf4562fe..6b0a52b18 100644 --- a/pkg/didcomm/crypto/jwe/authcrypt/encrypt.go +++ b/pkg/didcomm/crypto/jwe/authcrypt/encrypt.go @@ -28,22 +28,32 @@ import ( // Encrypt will JWE encode the payload argument for the sender and recipients // Using (X)Chacha20 encryption algorithm and Poly1035 authenticator -func (c *Crypter) Encrypt(payload []byte) ([]byte, error) { +// It will encrypt using the sender's keypair and the list of recipients arguments +func (c *Crypter) Encrypt(payload []byte, sender keyPair, recipients []*[chacha.KeySize]byte) ([]byte, error) { //nolint:lll,funlen + if len(recipients) == 0 { + return nil, errEmptyRecipients + } + + if !IsKeyPairValid(sender) { + return nil, errInvalidKeypair + } + headers := jweHeaders{ "typ": "prs.hyperledger.aries-auth-message", "alg": "ECDH-SS+" + string(c.alg) + "KW", "enc": string(c.alg), } - aad := c.buildAAD() - aadEncoded := base64.URLEncoding.EncodeToString(aad) + aad := buildAAD(recipients) + aadEncoded := base64.RawURLEncoding.EncodeToString(aad) - encHeaders, err := json.Marshal(headers) + h, err := json.Marshal(headers) if err != nil { return nil, err } + encHeaders := base64.RawURLEncoding.EncodeToString(h) // build the Payload's AAD string - pldAad := base64.URLEncoding.EncodeToString(encHeaders) + "." + aadEncoded + pldAAD := encHeaders + "." + aadEncoded // generate a new nonce for this encryption nonce := make([]byte, c.nonceSize) @@ -51,7 +61,7 @@ func (c *Crypter) Encrypt(payload []byte) ([]byte, error) { if err != nil { return nil, err } - nonceEncoded := base64.URLEncoding.EncodeToString(nonce) + nonceEncoded := base64.RawURLEncoding.EncodeToString(nonce) cek := &[chacha.KeySize]byte{} @@ -69,18 +79,18 @@ func (c *Crypter) Encrypt(payload []byte) ([]byte, error) { // encrypt payload using generated nonce, payload and its AAD // the output is a []byte containing the cipherText + tag - symOutput := crypter.Seal(nil, nonce, payload, []byte(pldAad)) + symOutput := crypter.Seal(nil, nonce, payload, []byte(pldAAD)) tagEncoded := extractTag(symOutput) cipherTextEncoded := extractCipherText(symOutput) // now build, encode recipients and include the encrypted cek (with a recipient's ephemeral key) - recipients, err := c.encodeRecipients(cek) + encRec, err := c.encodeRecipients(cek, recipients, sender) if err != nil { return nil, err } - jwe, err := c.buildJWE(headers, recipients, aadEncoded, nonceEncoded, tagEncoded, cipherTextEncoded) + jwe, err := c.buildJWE(encHeaders, encRec, aadEncoded, nonceEncoded, tagEncoded, cipherTextEncoded) if err != nil { return nil, err } @@ -93,16 +103,18 @@ func extractTag(symOutput []byte) string { // symOutput has a length of len(clear msg) + poly1035.TagSize // fetch the tag from the tail of symOutput tag := symOutput[len(symOutput)-poly1305.TagSize:] + // base64 encode the tag - return base64.URLEncoding.EncodeToString(tag) + return base64.RawURLEncoding.EncodeToString(tag) } // extractCipherText extracts the base64UrlEncoded cipherText sub slice from symOutput returned by cipher.Seal func extractCipherText(symOutput []byte) string { // fetch the cipherText from the head of symOutput (0:up to the trailing tag) cipherText := symOutput[0 : len(symOutput)-poly1305.TagSize] + // base64 encode the cipherText - return base64.URLEncoding.EncodeToString(cipherText) + return base64.RawURLEncoding.EncodeToString(cipherText) } // createCipher will create and return a new Chacha20Poly1035 cipher for the given nonceSize and symmetric key @@ -119,13 +131,9 @@ func createCipher(nonceSize int, symKey []byte) (cipher.AEAD, error) { // buildJWE builds the JSON object representing the JWE output of the encryption // and returns its marshaled []byte representation -func (c *Crypter) buildJWE(hdrs jweHeaders, recipients []Recipient, aad, iv, tag, cipherText string) ([]byte, error) { - h, err := json.Marshal(hdrs) - if err != nil { - return nil, err - } +func (c *Crypter) buildJWE(headers string, recipients []Recipient, aad, iv, tag, cipherText string) ([]byte, error) { jwe := Envelope{ - Protected: base64.URLEncoding.EncodeToString(h), + Protected: headers, Recipients: recipients, AAD: aad, IV: iv, @@ -144,11 +152,17 @@ func (c *Crypter) buildJWE(hdrs jweHeaders, recipients []Recipient, aad, iv, tag // buildAAD to build the Additional Authentication Data for the AEAD (chach20poly1035) cipher. // the build takes the list of recipients keys base58 encoded and sorted then SHA256 hash // the concatenation of these keys with a '.' separator -func (c *Crypter) buildAAD() []byte { +func buildAAD(recipients []*[chacha.KeySize]byte) []byte { var keys []string - for _, r := range c.recipients { + for _, r := range recipients { keys = append(keys, base58.Encode(r[:])) } + return hashAAD(keys) +} + +// hashAAD will string sort keys and return sha256 hash of the string representation +// of keys concatenated by '.' +func hashAAD(keys []string) []byte { sort.Strings(keys) sha := sha256.Sum256([]byte(strings.Join(keys, "."))) return sha[:] @@ -156,10 +170,10 @@ func (c *Crypter) buildAAD() []byte { // encodeRecipients will encode the sharedKey (cek) for each recipient // and return a list of encoded recipient keys -func (c *Crypter) encodeRecipients(sharedSymKey *[chacha.KeySize]byte) ([]Recipient, error) { +func (c *Crypter) encodeRecipients(sharedSymKey *[chacha.KeySize]byte, recipients []*[chacha.KeySize]byte, senderKp keyPair) ([]Recipient, error) { //nolint:lll var encodedRecipients []Recipient - for _, e := range c.recipients { - rec, err := c.encodeRecipient(sharedSymKey, e) + for _, e := range recipients { + rec, err := c.encodeRecipient(sharedSymKey, e, senderKp) if err != nil { return nil, err } @@ -170,7 +184,7 @@ func (c *Crypter) encodeRecipients(sharedSymKey *[chacha.KeySize]byte) ([]Recipi // encodeRecipient will encode the sharedKey (cek) with recipientKey // by generating a new ephemeral key to be used by the recipient to decrypt the cek -func (c *Crypter) encodeRecipient(sharedSymKey, recipientKey *[chacha.KeySize]byte) (*Recipient, error) { +func (c *Crypter) encodeRecipient(sharedSymKey, recipientKey *[chacha.KeySize]byte, senderKp keyPair) (*Recipient, error) { //nolint:lll // generate a random APU value (Agreement PartyUInfo: https://tools.ietf.org/html/rfc7518#section-4.6.1.2) apu := make([]byte, 64) _, err := randReader.Read(apu) @@ -179,7 +193,7 @@ func (c *Crypter) encodeRecipient(sharedSymKey, recipientKey *[chacha.KeySize]by } // create a new ephemeral key for the recipient and return its APU - kek, err := c.generateRecipientCEK(apu, recipientKey) + kek, err := c.generateRecipientCEK(apu, senderKp.priv, recipientKey) if err != nil { return nil, err } @@ -203,22 +217,22 @@ func (c *Crypter) encodeRecipient(sharedSymKey, recipientKey *[chacha.KeySize]by tag := extractTag(kekOutput) sharedKeyCipher := extractCipherText(kekOutput) - return c.buildRecipient(sharedKeyCipher, apu, nonce, tag, recipientKey) + return c.buildRecipient(sharedKeyCipher, apu, nonce, tag, senderKp.pub, recipientKey) } // buildRecipient will build a proper JSON formatted Recipient -func (c *Crypter) buildRecipient(key string, apu, nonce []byte, tag string, recipientKey *[chacha.KeySize]byte) (*Recipient, error) { //nolint:lll - oid, err := encryptOID(recipientKey, []byte(base58.Encode(c.sender.pub[:]))) +func (c *Crypter) buildRecipient(key string, apu, nonce []byte, tag string, senderPubKey, recipientKey *[chacha.KeySize]byte) (*Recipient, error) { //nolint:lll + oid, err := encryptOID(recipientKey, []byte(base58.Encode(senderPubKey[:]))) if err != nil { return nil, err } recipientHeaders := RecipientHeaders{ - APU: base64.URLEncoding.EncodeToString(apu), - IV: base64.URLEncoding.EncodeToString(nonce), + APU: base64.RawURLEncoding.EncodeToString(apu), + IV: base64.RawURLEncoding.EncodeToString(nonce), Tag: tag, KID: base58.Encode(recipientKey[:]), - OID: base64.URLEncoding.EncodeToString(oid), + OID: base64.RawURLEncoding.EncodeToString(oid), } recipient := &Recipient{ @@ -232,10 +246,10 @@ func (c *Crypter) buildRecipient(key string, apu, nonce []byte, tag string, reci // generateRecipientCEK will generate an ephemeral symmetric key for the recipientKey to // be used for encrypting the cek. // it will return this new key along with the corresponding APU or an error if it fails. -func (c *Crypter) generateRecipientCEK(apu []byte, recipientKey *[chacha.KeySize]byte) ([]byte, error) { +func (c *Crypter) generateRecipientCEK(apu []byte, privKey, pubKey *[chacha.KeySize]byte) ([]byte, error) { // base64 encode the APU - apuEncoded := make([]byte, base64.URLEncoding.EncodedLen(len(apu))) - base64.URLEncoding.Encode(apuEncoded, apu) + apuEncoded := make([]byte, base64.RawURLEncoding.EncodedLen(len(apu))) + base64.RawURLEncoding.Encode(apuEncoded, apu) // generating Z is inspired by sodium_crypto_scalarmult() // https://github.com/gamringer/php-authcrypt/blob/master/src/Crypt.php#L80 @@ -244,7 +258,7 @@ func (c *Crypter) generateRecipientCEK(apu []byte, recipientKey *[chacha.KeySize z := &[chacha.KeySize]byte{9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} //nolint:lll // do ScalarMult of the sender's private key with the recipient key to get a derived Z point // ( equivalent to derive an EC key ) - curve25519.ScalarMult(z, c.sender.priv, recipientKey) + curve25519.ScalarMult(z, privKey, pubKey) // inspired by: github.com/square/go-jose/v3@v3.0.0-20190722231519-723929d55157/cipher/ecdh_es.go // -> DeriveECDHES() call @@ -270,28 +284,53 @@ func (c *Crypter) generateRecipientCEK(apu []byte, recipientKey *[chacha.KeySize } // encryptOID 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() +// 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 // TODO add testing to ensure interoperability of encryptOID with libsodium's function above // the main difference between libsodium and below implementation is libsodium hides -// the ephemeral key and the nonce creation from the caller while box.Seal require these +// the ephemeral key and the nonce creation from the caller while box.Seal requires these // to be prebuilt and passed as arguments. -func encryptOID(pubKey *[chacha.KeySize]byte, msg []byte) ([]byte, error) { - var nonce [24]byte +// TODO replace 'OID' to 'SPK' recipient header which should represent the key in JWK encoded in a compact JWE format +func encryptOID(recipientPubKey *[chacha.KeySize]byte, msg []byte) ([]byte, error) { // generate ephemeral asymmetric keys epk, esk, err := box.GenerateKey(randReader) if err != nil { return nil, err } - // generate an equivalent nonce to libsodium's (see link above) + // generate an equivalent nonce to libsodium's (see link in comment above) + nonce, err := generateLibsodiumNonce(epk[:], recipientPubKey[:]) + if err != nil { + return nil, err + } + + var out = make([]byte, len(epk)) + copy(out, epk[:]) + + // now seal msg with the ephemeral key, nonce and recipientPubKey (which is the recipient's publicKey) + return box.Seal(out, msg, nonce, recipientPubKey, esk), nil +} + +// generateLibsodiumNonce will generate a nonce that is equivalent to libsodium's +// nonce format ie: black2b generation using concatenation of pub1 with pub2 +func generateLibsodiumNonce(pub1, pub2 []byte) (*[24]byte, error) { + var nonce [24]byte + // generate an equivalent nonce to libsodium's nonceWriter, err := blake2b.New(24, nil) if err != nil { return nil, err } - nonceSlice := nonceWriter.Sum(append(epk[:], pubKey[:]...)) - copy(nonce[:], nonceSlice) + _, 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) - // now seal the msg with the ephemeral key, nonce and pubKey (which is recipient's publicKey) - return box.Seal(epk[:], msg, &nonce, pubKey, esk), nil + return &nonce, nil }