forked from hyperledger-archives/aries-framework-go
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Authcrypt Encrypt Using (X)Chach20Poly1035
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
Showing
4 changed files
with
440 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.