Skip to content

Commit

Permalink
Authcrypt Encrypt Using (X)Chach20Poly1035
Browse files Browse the repository at this point in the history
	This change adds support to encrypt agent's payloads
	for the Pack() call at the transport layer

	It follows JWE encryption instructions from Aries
	Issue: hyperledger/aries-rfcs#133

Signed-off-by: Baha Shaaban <[email protected]>
  • Loading branch information
Baha Shaaban committed Aug 16, 2019
1 parent 295e217 commit f606bb5
Show file tree
Hide file tree
Showing 4 changed files with 440 additions and 0 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ module github.com/hyperledger/aries-framework-go

require (
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
github.com/square/go-jose/v3 v3.0.0-20190722231519-723929d55157
github.com/stretchr/testify v1.3.0
github.com/syndtr/goleveldb v1.0.0
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.1.0
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
)
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/square/go-jose/v3 v3.0.0-20190722231519-723929d55157 h1:2gZJx413/VIV3NUbCfGKoB6dHlCxGyTv8SZbtNmuJ8g=
github.com/square/go-jose/v3 v3.0.0-20190722231519-723929d55157/go.mod h1:xxWwA0zGRzuxAFnML4iyQMVPKwv28JDRwmyS2BldbmE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
Expand All @@ -28,12 +30,21 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg=
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
Expand Down
324 changes: 324 additions & 0 deletions pkg/didcomm/crypto/authcrypt/encrypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package authcrypt

import (
"crypto"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"sort"
"strings"

josecipher "github.com/square/go-jose/v3/cipher"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
"golang.org/x/crypto/nacl/secretbox"
errors "golang.org/x/xerrors"

"github.com/btcsuite/btcutil/base58"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
)

// This package deals with Authcrypt encryption for Packing/Unpacking DID Comm exchange
// Using Chacha20Poly1035 encryption/authentication

// C20P Chacha20Poly1035 algorithm
const C20P = "C20P"

// XC20P XChacha20Poly1035 algorithm
const XC20P = "XC20P"

// RandReader is a cryptographically secure random number generator.
var RandReader = rand.Reader

type keyPair struct {
priv *[chacha20poly1305.KeySize]byte
pub *[chacha20poly1305.KeySize]byte
}

// Crypter represents an Authcrypt Encrypter (Decrypter) that outputs/reads JWE envelopes
type Crypter struct {
Sender keyPair
Recipients []*[chacha20poly1305.KeySize]byte
Alg string
NonceSize int
}

// JweEnvelope represents a JWE envelope as per the Aries Encryption envelop specs
type JweEnvelope struct {
Protected jweHeaders `json:"protected,omitempty"`
Recipients []Recipient `json:"recipients,omitempty"`
Aad string `json:"aad,omitempty"`
Iv string `json:"iv,omitempty"`
Tag string `json:"tag,omitempty"`
CipherText string `json:"ciphertext,omitempty"`
}

// jweHeaders are the Protected JWE headers in a map format
type jweHeaders map[string]string

// Recipient is a recipient of an envelop including the shared encryption key
type Recipient struct {
EncryptedKey string `json:"encrypted_key,omitempty"`
Header RecipientHeaders `json:"header,omitempty"`
}

// RecipientHeaders are the recipient headers
type RecipientHeaders struct {
Apu string `json:"apu,omitempty"`
Iv string `json:"iv,omitempty"`
Tag string `json:"tag,omitempty"`
Kid string `json:"kid,omitempty"`
Oid string `json:"oid,omitempty"`
}

// NewAuthCrypter will create an encrypter instance to 'AuthCrypt' payloads for the given sender and recipients arguments
// and the encryption alg argument. Possible algorithms supported are:
// C20P (chacha20-poly1035 ietf)
// XC20P (xchacha20-poly1035 ietf)
func NewAuthCrypter(sender keyPair, recipients [][]byte, alg string) (*Crypter, error) {
var nonceSize int
switch alg {
case C20P:
nonceSize = chacha20poly1305.NonceSize
case XC20P:
nonceSize = chacha20poly1305.NonceSizeX
default:
return nil, errors.New(fmt.Sprintf("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 []*[chacha20poly1305.KeySize]byte
for i, r := range recipients {
if !isKeyValid(r) {
return nil, errors.New(fmt.Sprintf("recipient %d key not supported, it must have %d bytes key", i+1, chacha20poly1305.KeySize))
}
rec := &[chacha20poly1305.KeySize]byte{}
copy(rec[:], r)
recipientsKey = append(recipientsKey, rec)
}

c := &Crypter{
sender,
recipientsKey,
alg,
nonceSize,
}

if !isKeyPairValid(sender) {
return nil, errors.New(fmt.Sprintf("sender keyPair not supported, it must have %d bytes keys", chacha20poly1305.KeySize))
}

return c, nil
}

func isKeyPairValid(kp keyPair) bool {
if kp.priv == nil || kp.pub == nil || len(kp.priv) != chacha20poly1305.KeySize || len(kp.pub) != chacha20poly1305.KeySize {
return false
}
return true
}

func isKeyValid(key []byte) bool {
return len(key) == chacha20poly1305.KeySize
}

// Encrypt will JWE encode the payload argument for the sender and recipients
// Using (X)Chacha20Poly1035 encryption & authentication (Authcrypt)
func (c *Crypter) Encrypt(payload string) (string, error) {
headers := jweHeaders{
"typ": "prs.hyperledger.aries-auth-message",
"alg": "ECDH-SS+" + c.Alg + "KW",
"enc": c.Alg,
}

aad := c.buildAAD()
aadEncoded := base64.URLEncoding.EncodeToString(aad)

encHeaders, err := json.Marshal(headers)
if err != nil {
return "", err
}
pldAad := base64.URLEncoding.EncodeToString(encHeaders) + "." + aadEncoded

nonce := make([]byte, c.NonceSize)
_, err = RandReader.Read(nonce)
if err != nil {
return "", err
}
nonceEncoded := base64.URLEncoding.EncodeToString(nonce)

// generate a symmetric key
sharedKey := keyPair{}
sharedKey.pub, sharedKey.priv, err = box.GenerateKey(RandReader)
if err != nil {
return "", err
}

cipher, err := createCipher(c.NonceSize, sharedKey.priv[:])
if err != nil {
return "", err
}

pld := []byte(payload)

// encrypt payload
symOutput := cipher.Seal(nil, nonce, pld, []byte(pldAad))
tag := symOutput[len(symOutput)-poly1305.TagSize:]
tagEncoded := base64.URLEncoding.EncodeToString(tag)

cipherText := symOutput[0 : len(symOutput)-poly1305.TagSize]
cipherTextEncoded := base64.URLEncoding.EncodeToString(cipherText)

recipients, err := c.encodeRecipients(sharedKey)
if err != nil {
return "", err
}

json, err := c.buildJWE(headers, recipients, aadEncoded, nonceEncoded, tagEncoded, cipherTextEncoded)
if err != nil {
return "", err
}

return string(json), nil
}

func createCipher(nonceSize int, symKey []byte) (cipher.AEAD, error) {
switch nonceSize {
case chacha20poly1305.NonceSize:
return chacha20poly1305.New(symKey)
case chacha20poly1305.NonceSizeX:
return chacha20poly1305.NewX(symKey)
default:
return nil, errors.New("cipher cannot be created with bad nonce size and shared key combo")
}
}

func (c *Crypter) buildJWE(headers jweHeaders, recipients []Recipient, aad string, iv string, tag string, cipherText string) ([]byte, error) {

jwe := JweEnvelope{
Protected: headers,
Recipients: recipients,
Aad: aad,
Iv: iv,
Tag: tag,
CipherText: cipherText,
}

jweBytes, err := json.Marshal(jwe)
if err != nil {
return nil, err
}

return jweBytes, nil
}

func (c *Crypter) buildAAD() []byte {
var kids []string
for _, r := range c.Recipients {
kids = append(kids, base58.Encode(r[:]))
}
sort.Strings(kids)
sha := sha256.Sum256([]byte(strings.Join(kids, ".")))
return sha[:]
}

func (c *Crypter) encodeRecipients(sharedKey keyPair) ([]Recipient, error) {
var encodedRecipients []Recipient
for _, e := range c.Recipients {
rec, err := c.encodeRecipient(sharedKey, e)
if err != nil {
return nil, err
}
encodedRecipients = append(encodedRecipients, *rec)
}
return encodedRecipients, nil
}

func (c *Crypter) encodeRecipient(sharedKey keyPair, recipientKey *[chacha20poly1305.KeySize]byte) (*Recipient, error) {
apu := make([]byte, 64)
_, err := RandReader.Read(apu)
if err != nil {
return nil, err
}

apuEncoded := make([]byte, base64.URLEncoding.EncodedLen(len(apu)))
base64.URLEncoding.Encode(apuEncoded, apu)

kek := c.generateRecipientCek(apuEncoded, recipientKey)

cipher, err := createCipher(c.NonceSize, kek)
if err != nil {
return nil, err
}

nonce := make([]byte, c.NonceSize)
_, err = RandReader.Read(nonce)
if err != nil {
return nil, err
}

// encrypt symmetric shared key using authentication key
kekOutput := cipher.Seal(nil, nonce, sharedKey.priv[:], nil)

tag := kekOutput[len(kekOutput)-poly1305.TagSize:]
symKeyCipher := kekOutput[0 : len(kekOutput)-poly1305.TagSize]

return c.buildRecipient(symKeyCipher, apu, nonce, tag, recipientKey)
}

func (c *Crypter) buildRecipient(symKeyCipher []byte, apu []byte, nonce []byte, tag []byte, recipientKey *[chacha20poly1305.KeySize]byte) (*Recipient, error) {
var keyNonce [24]byte
switch c.NonceSize {
case chacha20poly1305.NonceSize:
copy(keyNonce[:], nonce[:])
copy(keyNonce[chacha20poly1305.NonceSize:], nonce[:]) // duplicate nonce if nonceSize is 12
case chacha20poly1305.NonceSizeX:
}

oid := secretbox.Seal(nil, []byte(base64.URLEncoding.EncodeToString(c.Sender.pub[:])), &keyNonce, recipientKey)

recipientHeaders := RecipientHeaders{
Apu: base64.URLEncoding.EncodeToString(apu),
Iv: base64.URLEncoding.EncodeToString(nonce),
Tag: base64.URLEncoding.EncodeToString(tag),
Kid: base58.Encode(recipientKey[:]),
Oid: base64.URLEncoding.EncodeToString(oid),
}

recipient := &Recipient{
EncryptedKey: base64.URLEncoding.EncodeToString(symKeyCipher),
Header: recipientHeaders,
}

return recipient, nil
}

func (c *Crypter) generateRecipientCek(apu []byte, recipientKey *[chacha20poly1305.KeySize]byte) []byte {
z := &[chacha20poly1305.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}
curve25519.ScalarMult(z, c.Sender.priv, recipientKey)

// suppPubInfo is the encoded length of the recipient shared key output size in bits
supPubInfo := make([]byte, 4)
binary.BigEndian.PutUint32(supPubInfo, uint32(chacha20poly1305.KeySize)*8)

reader := josecipher.NewConcatKDF(crypto.SHA256, z[:], []byte(c.Alg), apu, nil, supPubInfo, []byte{})
// kek is the recipient specific encryption key used to encrypt the sharedKey
kek := make([]byte, chacha20poly1305.KeySize)

// Read on the KDF will never fail
_, _ = reader.Read(kek)

return kek
}
Loading

0 comments on commit f606bb5

Please sign in to comment.