muun-recovery/vendor/github.com/muun/libwallet/encrypt.go

446 lines
15 KiB
Go

package libwallet
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"math/big"
"github.com/muun/libwallet/aescbc"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil/base58"
)
const serializedPublicKeyLength = btcec.PubKeyBytesLenCompressed
const PKEncryptionVersion = 1
// maxDerivationPathLen is a safety limit to avoid stupid size allocations
const maxDerivationPathLen = 1000
// maxSignatureLen is a safety limit to avoid giant allocations
const maxSignatureLen = 200
// minNonceLen is the safe minimum we'll set for the nonce. This is the default for golang, but it's not exposed.
const minNonceLen = 12
type Encrypter interface {
// Encrypt the payload and return a string with the necesary information for decryption
Encrypt(payload []byte) (string, error)
}
type Decrypter interface {
// Decrypt a payload generated by Encrypter
Decrypt(payload string) ([]byte, error)
}
type hdPubKeyEncrypter struct {
receiverKey *HDPublicKey
senderKey *HDPrivateKey
}
func addVariableBytes(writer io.Writer, data []byte) error {
if len(data) > math.MaxUint16 {
return fmt.Errorf("data length can't exceeed %v", math.MaxUint16)
}
dataLen := uint16(len(data))
err := binary.Write(writer, binary.BigEndian, &dataLen)
if err != nil {
return fmt.Errorf("failed to write var bytes len: %w", err)
}
n, err := writer.Write(data)
if err != nil || n != len(data) {
return errors.New("failed to write var bytes")
}
return nil
}
func (e *hdPubKeyEncrypter) Encrypt(payload []byte) (string, error) {
// Uses AES128-GCM with associated data. ECDHE is used for key exchange and ECDSA for authentication.
// The goal is to be able to send an arbitrary message to a 3rd party or our future selves via
// an intermediary which has knowledge of public keys for all parties involved.
//
// Conceptually, what we do is:
// 1. Sign the payload using the senders private key so the receiver can check it's authentic
// The signature also covers the receivers public key to avoid payload reuse by the intermediary
// 2. Establish an encryption key via ECDH given the receivers pub key
// 3. Encrypt the payload and signature using AES with a new random nonce
// 4. Add the metadata the receiver will need to decode the message:
// * The derivation path for his pub key
// * The ephemeral key used for ECDH
// * The version code of this scheme
// 5. HMAC the encrypted payload and the metadata so the receiver can check it hasn't been tampered
// 6. Add the nonce to the payload so the receiver can actually decrypt the message.
// The nonce can't be covered by the HMAC since it's used to generate it.
// 7. Profit!
//
// The implementation actually use an AES128-GCM with is an AEAD, so the encryption and HMAC all happen
// at the same time.
signingKey, err := e.senderKey.key.ECPrivKey()
if err != nil {
return "", fmt.Errorf("Encrypt: failed to extract signing key: %w", err)
}
encryptionKey, err := e.receiverKey.key.ECPubKey()
if err != nil {
return "", fmt.Errorf("Encrypt: failed to extract pub key: %w", err)
}
// Sign "payload || encryptionKey" to protect against payload reuse by 3rd parties
signaturePayload := make([]byte, 0, len(payload)+serializedPublicKeyLength)
signaturePayload = append(signaturePayload, payload...)
signaturePayload = append(signaturePayload, encryptionKey.SerializeCompressed()...)
hash := sha256.Sum256(signaturePayload)
senderSignature, err := btcec.SignCompact(btcec.S256(), signingKey, hash[:], false)
if err != nil {
return "", fmt.Errorf("Encrypt: failed to sign payload: %w", err)
}
// plaintext is "senderSignature || payload"
plaintext := bytes.NewBuffer(make([]byte, 0, 2+len(payload)+2+len(senderSignature)))
err = addVariableBytes(plaintext, senderSignature)
if err != nil {
return "", fmt.Errorf("Encrypter: failed to add senderSignature: %w", err)
}
err = addVariableBytes(plaintext, payload)
if err != nil {
return "", fmt.Errorf("Encrypter: failed to add payload: %w", err)
}
pubEph, sharedSecret, err := generateSharedEncryptionSecretForAES(encryptionKey)
if err != nil {
return "", fmt.Errorf("Encrypt: failed to generate shared encryption key: %w", err)
}
blockCipher, err := aes.NewCipher(sharedSecret)
if err != nil {
return "", fmt.Errorf("Encrypt: new aes failed: %w", err)
}
gcm, err := cipher.NewGCM(blockCipher)
if err != nil {
return "", fmt.Errorf("Encrypt: new gcm failed: %w", err)
}
nonce := randomBytes(gcm.NonceSize())
// additionalData is "version || pubEph || receiverKeyPath || nonceLen"
additionalDataLen := 1 + serializedPublicKeyLength + 2 + len(e.receiverKey.Path) + 2
result := bytes.NewBuffer(make([]byte, 0, additionalDataLen))
result.WriteByte(PKEncryptionVersion)
result.Write(pubEph.SerializeCompressed())
err = addVariableBytes(result, []byte(e.receiverKey.Path))
if err != nil {
return "", fmt.Errorf("Encrypt: failed to add receiver path: %w", err)
}
nonceLen := uint16(len(nonce))
err = binary.Write(result, binary.BigEndian, &nonceLen)
if err != nil {
return "", fmt.Errorf("Encrypt: failed to add nonce len: %w", err)
}
ciphertext := gcm.Seal(nil, nonce, plaintext.Bytes(), result.Bytes())
// result is "additionalData || nonce || ciphertext"
n, err := result.Write(nonce)
if err != nil || n != len(nonce) {
return "", errors.New("Encrypt: failed to add nonce")
}
n, err = result.Write(ciphertext)
if err != nil || n != len(ciphertext) {
return "", errors.New("Encrypt: failed to add ciphertext")
}
return base58.Encode(result.Bytes()), nil
}
// hdPrivKeyDecrypter holds the keys for validation and decryption of messages using Muun's scheme
type hdPrivKeyDecrypter struct {
receiverKey *HDPrivateKey
// senderKey optionally holds the pub key used by sender
// If the sender is the same as the receiver, set this to nil and set fromSelf to true.
// If the sender is unknown, set this to nil. If so, the authenticity of the message won't be validated.
senderKey *PublicKey
// fromSelf is true if this message is from yourself
fromSelf bool
}
func extractVariableBytes(reader *bytes.Reader, limit int) ([]byte, error) {
var len uint16
err := binary.Read(reader, binary.BigEndian, &len)
if err != nil || int(len) > limit || int(len) > reader.Len() {
return nil, errors.New("failed to read byte array len")
}
result := make([]byte, len)
n, err := reader.Read(result)
if err != nil || n != int(len) {
return nil, errors.New("failed to extract byte array")
}
return result, nil
}
func extractVariableString(reader *bytes.Reader, limit int) (string, error) {
bytes, err := extractVariableBytes(reader, limit)
return string(bytes), err
}
func (d *hdPrivKeyDecrypter) Decrypt(payload string) ([]byte, error) {
// Uses AES128-GCM with associated data. ECDHE is used for key exchange and ECDSA for authentication.
// See Encrypt further up for an in depth dive into the scheme used
decoded := base58.Decode(payload)
reader := bytes.NewReader(decoded)
version, err := reader.ReadByte()
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to read version byte: %w", err)
}
if version != PKEncryptionVersion {
return nil, fmt.Errorf("Decrypt: found key version %v, expected %v",
version, PKEncryptionVersion)
}
rawPubEph := make([]byte, serializedPublicKeyLength)
n, err := reader.Read(rawPubEph)
if err != nil || n != serializedPublicKeyLength {
return nil, errors.New("Decrypt: failed to read pubeph")
}
receiverPath, err := extractVariableString(reader, maxDerivationPathLen)
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to extract receiver path: %w", err)
}
// additionalDataSize is Whatever I've read so far plus two bytes for the nonce len
additionalDataSize := len(decoded) - reader.Len() + 2
minCiphertextLen := 2 // an empty sig with no plaintext
nonce, err := extractVariableBytes(reader, reader.Len()-minCiphertextLen)
if err != nil || len(nonce) < minNonceLen {
return nil, errors.New("Decrypt: failed to read nonce")
}
// What's left is the ciphertext
ciphertext := make([]byte, reader.Len())
_, err = reader.Read(ciphertext)
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to read ciphertext: %w", err)
}
receiverKey, err := d.receiverKey.DeriveTo(receiverPath)
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to derive receiver key to path %v: %w", receiverPath, err)
}
encryptionKey, err := receiverKey.key.ECPrivKey()
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to extract encryption key: %w", err)
}
var verificationKey *btcec.PublicKey
if d.fromSelf {
// Use the derived receiver key if the sender key is not provided
verificationKey, err = receiverKey.PublicKey().key.ECPubKey()
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to extract verification key: %w", err)
}
} else if d.senderKey != nil {
verificationKey = d.senderKey.key
}
sharedSecret, err := recoverSharedEncryptionSecretForAES(encryptionKey, rawPubEph)
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to recover shared secret: %w", err)
}
blockCipher, err := aes.NewCipher(sharedSecret)
if err != nil {
return nil, fmt.Errorf("Decrypt: new aes failed: %w", err)
}
gcm, err := cipher.NewGCMWithNonceSize(blockCipher, len(nonce))
if err != nil {
return nil, fmt.Errorf("Decrypt: new gcm failed: %w", err)
}
plaintext, err := gcm.Open(nil, nonce, ciphertext, decoded[:additionalDataSize])
if err != nil {
return nil, fmt.Errorf("Decrypt: AEAD failed: %w", err)
}
plaintextReader := bytes.NewReader(plaintext)
sig, err := extractVariableBytes(plaintextReader, maxSignatureLen)
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to read sig: %w", err)
}
data, err := extractVariableBytes(plaintextReader, plaintextReader.Len())
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to extract user data: %w", err)
}
signatureData := make([]byte, 0, len(sig)+serializedPublicKeyLength)
signatureData = append(signatureData, data...)
signatureData = append(signatureData, encryptionKey.PubKey().SerializeCompressed()...)
hash := sha256.Sum256(signatureData)
signatureKey, _, err := btcec.RecoverCompact(btcec.S256(), sig, hash[:])
if err != nil {
return nil, fmt.Errorf("Decrypt: failed to verify signature: %w", err)
}
if verificationKey != nil && !signatureKey.IsEqual(verificationKey) {
return nil, errors.New("Decrypt: signing key mismatch")
}
return data, nil
}
// Assert hdPubKeyEncrypter fulfills Encrypter interface
var _ Encrypter = (*hdPubKeyEncrypter)(nil)
// Assert hdPrivKeyDecrypter fulfills Decrypter interface
var _ Decrypter = (*hdPrivKeyDecrypter)(nil)
// encryptWithPubKey encrypts a message using a pubKey
// It uses ECDHE/AES/CBC leaving padding up to the caller.
func encryptWithPubKey(pubKey *btcec.PublicKey, plaintext []byte) (*btcec.PublicKey, []byte, error) {
// Use deprecated ECDH for compat
pubEph, sharedSecret, err := generateSharedEncryptionSecret(pubKey)
if err != nil {
return nil, nil, err
}
serializedPubkey := pubEph.SerializeCompressed()
iv := serializedPubkey[len(serializedPubkey)-aes.BlockSize:]
ciphertext, err := aescbc.EncryptNoPadding(paddedSerializeBigInt(aescbc.KeySize, sharedSecret), iv, plaintext)
if err != nil {
return nil, nil, fmt.Errorf("encryptWithPubKey: encrypt failed: %w", err)
}
return pubEph, ciphertext, nil
}
// generateSharedEncryptionSecret performs a ECDH with pubKey
// Deprecated: this function is unsafe and generateSharedEncryptionSecretForAES should be used
func generateSharedEncryptionSecret(pubKey *btcec.PublicKey) (*btcec.PublicKey, *big.Int, error) {
privEph, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
return nil, nil, fmt.Errorf("generateSharedEncryptionSecretForAES: failed to generate key: %w", err)
}
sharedSecret, _ := pubKey.ScalarMult(pubKey.X, pubKey.Y, privEph.D.Bytes())
return privEph.PubKey(), sharedSecret, nil
}
// generateSharedEncryptionSecret performs a ECDH with pubKey and produces a secret usable with AES
func generateSharedEncryptionSecretForAES(pubKey *btcec.PublicKey) (*btcec.PublicKey, []byte, error) {
privEph, sharedSecret, err := generateSharedEncryptionSecret(pubKey)
if err != nil {
return nil, nil, err
}
hash := sha256.Sum256(paddedSerializeBigInt(aescbc.KeySize, sharedSecret))
return privEph, hash[:], nil
}
// decryptWithPrivKey decrypts a message encrypted to a pubKey using the corresponding privKey
// It uses ECDHE/AES/CBC leaving padding up to the caller.
func decryptWithPrivKey(privKey *btcec.PrivateKey, rawPubEph []byte, ciphertext []byte) ([]byte, error) {
// Use deprecated ECDH for compat
sharedSecret, err := recoverSharedEncryptionSecret(privKey, rawPubEph)
if err != nil {
return nil, err
}
iv := rawPubEph[len(rawPubEph)-aes.BlockSize:]
plaintext, err := aescbc.DecryptNoPadding(paddedSerializeBigInt(aescbc.KeySize, sharedSecret), iv, ciphertext)
if err != nil {
return nil, fmt.Errorf("decryptWithPrivKey: failed to decrypt: %w", err)
}
return plaintext, nil
}
// recoverSharedEncryptionSecret performs an ECDH to recover the encryption secret meant for privKey from rawPubEph
// Deprecated: this function is unsafe and recoverSharedEncryptionSecretForAES should be used
func recoverSharedEncryptionSecret(privKey *btcec.PrivateKey, rawPubEph []byte) (*big.Int, error) {
pubEph, err := btcec.ParsePubKey(rawPubEph, btcec.S256())
if err != nil {
return nil, fmt.Errorf("recoverSharedEncryptionSecretForAES: failed to parse pub eph: %w", err)
}
sharedSecret, _ := pubEph.ScalarMult(pubEph.X, pubEph.Y, privKey.D.Bytes())
return sharedSecret, nil
}
func recoverSharedEncryptionSecretForAES(privKey *btcec.PrivateKey, rawPubEph []byte) ([]byte, error) {
sharedSecret, err := recoverSharedEncryptionSecret(privKey, rawPubEph)
if err != nil {
return nil, err
}
hash := sha256.Sum256(paddedSerializeBigInt(aescbc.KeySize, sharedSecret))
return hash[:], nil
}
func randomBytes(count int) []byte {
buf := make([]byte, count)
_, err := rand.Read(buf)
if err != nil {
panic("couldn't read random bytes")
}
return buf
}
// What follows are work arounds for https://github.com/golang/go/issues/46893
type DecryptOperation struct {
d Decrypter
payload string
}
func NewDecryptOperation(key *HDPrivateKey, payload string) *DecryptOperation {
return &DecryptOperation{key.Decrypter(), payload}
}
func NewDecryptOperationFrom(sender *PublicKey, key *HDPrivateKey, payload string) *DecryptOperation {
return &DecryptOperation{key.DecrypterFrom(sender), payload}
}
func (o *DecryptOperation) Decrypt() ([]byte, error) {
return o.d.Decrypt(o.payload)
}
type EncryptOperation struct {
e Encrypter
payload []byte
}
func NewEncryptOperation(key *HDPrivateKey, payload []byte) *EncryptOperation {
return &EncryptOperation{key.Encrypter(), append([]byte{}, payload...)}
}
func (o *EncryptOperation) Encrypt() (string, error) {
return o.e.Encrypt(o.payload)
}