mirror of
https://github.com/muun/recovery.git
synced 2025-11-12 06:50:18 -05:00
Update project structure and build process
This commit is contained in:
445
libwallet/encrypt.go
Executable file
445
libwallet/encrypt.go
Executable file
@@ -0,0 +1,445 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user