mirror of
https://github.com/muun/recovery.git
synced 2025-02-23 11:32:33 -05:00
290 lines
9.4 KiB
Go
290 lines
9.4 KiB
Go
package sphinx
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/aead/chacha20"
|
|
"github.com/btcsuite/btcd/btcec"
|
|
)
|
|
|
|
const (
|
|
// HMACSize is the length of the HMACs used to verify the integrity of
|
|
// the onion. Any value lower than 32 will truncate the HMAC both
|
|
// during onion creation as well as during the verification.
|
|
HMACSize = 32
|
|
)
|
|
|
|
// Hash256 is a statically sized, 32-byte array, typically containing
|
|
// the output of a SHA256 hash.
|
|
type Hash256 [sha256.Size]byte
|
|
|
|
// DecryptedError contains the decrypted error message and its sender.
|
|
type DecryptedError struct {
|
|
// Sender is the node that sent the error. Note that a node may occur in
|
|
// the path multiple times. If that is the case, the sender pubkey does
|
|
// not tell the caller on which visit the error occurred.
|
|
Sender *btcec.PublicKey
|
|
|
|
// SenderIdx is the position of the error sending node in the path.
|
|
// Index zero is the self node. SenderIdx allows to distinguish between
|
|
// errors from nodes that occur in the path multiple times.
|
|
SenderIdx int
|
|
|
|
// Message is the decrypted error message.
|
|
Message []byte
|
|
}
|
|
|
|
// zeroHMAC is the special HMAC value that allows the final node to determine
|
|
// if it is the payment destination or not.
|
|
var zeroHMAC [HMACSize]byte
|
|
|
|
// calcMac calculates HMAC-SHA-256 over the message using the passed secret key
|
|
// as input to the HMAC.
|
|
func calcMac(key [keyLen]byte, msg []byte) [HMACSize]byte {
|
|
hmac := hmac.New(sha256.New, key[:])
|
|
hmac.Write(msg)
|
|
h := hmac.Sum(nil)
|
|
|
|
var mac [HMACSize]byte
|
|
copy(mac[:], h[:HMACSize])
|
|
|
|
return mac
|
|
}
|
|
|
|
// xor computes the byte wise XOR of a and b, storing the result in dst. Only
|
|
// the frist `min(len(a), len(b))` bytes will be xor'd.
|
|
func xor(dst, a, b []byte) int {
|
|
n := len(a)
|
|
if len(b) < n {
|
|
n = len(b)
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
dst[i] = a[i] ^ b[i]
|
|
}
|
|
return n
|
|
}
|
|
|
|
// generateKey generates a new key for usage in Sphinx packet
|
|
// construction/processing based off of the denoted keyType. Within Sphinx
|
|
// various keys are used within the same onion packet for padding generation,
|
|
// MAC generation, and encryption/decryption.
|
|
func generateKey(keyType string, sharedKey *Hash256) [keyLen]byte {
|
|
mac := hmac.New(sha256.New, []byte(keyType))
|
|
mac.Write(sharedKey[:])
|
|
h := mac.Sum(nil)
|
|
|
|
var key [keyLen]byte
|
|
copy(key[:], h[:keyLen])
|
|
|
|
return key
|
|
}
|
|
|
|
// generateCipherStream generates a stream of cryptographic psuedo-random bytes
|
|
// intended to be used to encrypt a message using a one-time-pad like
|
|
// construction.
|
|
func generateCipherStream(key [keyLen]byte, numBytes uint) []byte {
|
|
var (
|
|
nonce [8]byte
|
|
)
|
|
cipher, err := chacha20.NewCipher(nonce[:], key[:])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
output := make([]byte, numBytes)
|
|
cipher.XORKeyStream(output, output)
|
|
|
|
return output
|
|
}
|
|
|
|
// computeBlindingFactor for the next hop given the ephemeral pubKey and
|
|
// sharedSecret for this hop. The blinding factor is computed as the
|
|
// sha-256(pubkey || sharedSecret).
|
|
func computeBlindingFactor(hopPubKey *btcec.PublicKey,
|
|
hopSharedSecret []byte) Hash256 {
|
|
|
|
sha := sha256.New()
|
|
sha.Write(hopPubKey.SerializeCompressed())
|
|
sha.Write(hopSharedSecret)
|
|
|
|
var hash Hash256
|
|
copy(hash[:], sha.Sum(nil))
|
|
return hash
|
|
}
|
|
|
|
// blindGroupElement blinds the group element P by performing scalar
|
|
// multiplication of the group element by blindingFactor: blindingFactor * P.
|
|
func blindGroupElement(hopPubKey *btcec.PublicKey, blindingFactor []byte) *btcec.PublicKey {
|
|
newX, newY := btcec.S256().ScalarMult(hopPubKey.X, hopPubKey.Y, blindingFactor[:])
|
|
return &btcec.PublicKey{btcec.S256(), newX, newY}
|
|
}
|
|
|
|
// blindBaseElement blinds the groups's generator G by performing scalar base
|
|
// multiplication using the blindingFactor: blindingFactor * G.
|
|
func blindBaseElement(blindingFactor []byte) *btcec.PublicKey {
|
|
newX, newY := btcec.S256().ScalarBaseMult(blindingFactor)
|
|
return &btcec.PublicKey{btcec.S256(), newX, newY}
|
|
}
|
|
|
|
// sharedSecretGenerator is an interface that abstracts away exactly *how* the
|
|
// shared secret for each hop is generated.
|
|
//
|
|
// TODO(roasbef): rename?
|
|
type sharedSecretGenerator interface {
|
|
// generateSharedSecret given a public key, generates a shared secret
|
|
// using private data of the underlying sharedSecretGenerator.
|
|
generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error)
|
|
}
|
|
|
|
// generateSharedSecret generates the shared secret by given ephemeral key.
|
|
func (r *Router) generateSharedSecret(dhKey *btcec.PublicKey) (Hash256, error) {
|
|
var sharedSecret Hash256
|
|
|
|
// Ensure that the public key is on our curve.
|
|
if !btcec.S256().IsOnCurve(dhKey.X, dhKey.Y) {
|
|
return sharedSecret, ErrInvalidOnionKey
|
|
}
|
|
|
|
// Compute our shared secret.
|
|
sharedSecret = generateSharedSecret(dhKey, r.onionKey)
|
|
return sharedSecret, nil
|
|
}
|
|
|
|
// generateSharedSecret generates the shared secret for a particular hop. The
|
|
// shared secret is generated by taking the group element contained in the
|
|
// mix-header, and performing an ECDH operation with the node's long term onion
|
|
// key. We then take the _entire_ point generated by the ECDH operation,
|
|
// serialize that using a compressed format, then feed the raw bytes through a
|
|
// single SHA256 invocation. The resulting value is the shared secret.
|
|
func generateSharedSecret(pub *btcec.PublicKey, priv *btcec.PrivateKey) Hash256 {
|
|
s := &btcec.PublicKey{}
|
|
s.X, s.Y = btcec.S256().ScalarMult(pub.X, pub.Y, priv.D.Bytes())
|
|
|
|
return sha256.Sum256(s.SerializeCompressed())
|
|
}
|
|
|
|
// onionEncrypt obfuscates the data with compliance with BOLT#4. As we use a
|
|
// stream cipher, calling onionEncrypt on an already encrypted piece of data
|
|
// will decrypt it.
|
|
func onionEncrypt(sharedSecret *Hash256, data []byte) []byte {
|
|
p := make([]byte, len(data))
|
|
|
|
ammagKey := generateKey("ammag", sharedSecret)
|
|
streamBytes := generateCipherStream(ammagKey, uint(len(data)))
|
|
xor(p, data, streamBytes)
|
|
|
|
return p
|
|
}
|
|
|
|
// onionErrorLength is the expected length of the onion error message.
|
|
// Including padding, all messages on the wire should be 256 bytes. We then add
|
|
// the size of the sha256 HMAC as well.
|
|
const onionErrorLength = 2 + 2 + 256 + sha256.Size
|
|
|
|
// DecryptError attempts to decrypt the passed encrypted error response. The
|
|
// onion failure is encrypted in backward manner, starting from the node where
|
|
// error have occurred. As a result, in order to decrypt the error we need get
|
|
// all shared secret and apply decryption in the reverse order. A structure is
|
|
// returned that contains the decrypted error message and information on the
|
|
// sender.
|
|
func (o *OnionErrorDecrypter) DecryptError(encryptedData []byte) (
|
|
*DecryptedError, error) {
|
|
|
|
// Ensure the error message length is as expected.
|
|
if len(encryptedData) != onionErrorLength {
|
|
return nil, fmt.Errorf("invalid error length: "+
|
|
"expected %v got %v", onionErrorLength,
|
|
len(encryptedData))
|
|
}
|
|
|
|
sharedSecrets := generateSharedSecrets(
|
|
o.circuit.PaymentPath,
|
|
o.circuit.SessionKey,
|
|
)
|
|
|
|
var (
|
|
sender int
|
|
msg []byte
|
|
dummySecret Hash256
|
|
)
|
|
copy(dummySecret[:], bytes.Repeat([]byte{1}, 32))
|
|
|
|
// We'll iterate a constant amount of hops to ensure that we don't give
|
|
// away an timing information pertaining to the position in the route
|
|
// that the error emanated from.
|
|
for i := 0; i < NumMaxHops; i++ {
|
|
var sharedSecret Hash256
|
|
|
|
// If we've already found the sender, then we'll use our dummy
|
|
// secret to continue decryption attempts to fill out the rest
|
|
// of the loop. Otherwise, we'll use the next shared secret in
|
|
// line.
|
|
if sender != 0 || i > len(sharedSecrets)-1 {
|
|
sharedSecret = dummySecret
|
|
} else {
|
|
sharedSecret = sharedSecrets[i]
|
|
}
|
|
|
|
// With the shared secret, we'll now strip off a layer of
|
|
// encryption from the encrypted error payload.
|
|
encryptedData = onionEncrypt(&sharedSecret, encryptedData)
|
|
|
|
// Next, we'll need to separate the data, from the MAC itself
|
|
// so we can reconstruct and verify it.
|
|
expectedMac := encryptedData[:sha256.Size]
|
|
data := encryptedData[sha256.Size:]
|
|
|
|
// With the data split, we'll now re-generate the MAC using its
|
|
// specified key.
|
|
umKey := generateKey("um", &sharedSecret)
|
|
h := hmac.New(sha256.New, umKey[:])
|
|
h.Write(data)
|
|
|
|
// If the MAC matches up, then we've found the sender of the
|
|
// error and have also obtained the fully decrypted message.
|
|
realMac := h.Sum(nil)
|
|
if hmac.Equal(realMac, expectedMac) && sender == 0 {
|
|
sender = i + 1
|
|
msg = data
|
|
}
|
|
}
|
|
|
|
// If the sender index is still zero, then we haven't found the sender,
|
|
// meaning we've failed to decrypt.
|
|
if sender == 0 {
|
|
return nil, errors.New("unable to retrieve onion failure")
|
|
}
|
|
|
|
return &DecryptedError{
|
|
SenderIdx: sender,
|
|
Sender: o.circuit.PaymentPath[sender-1],
|
|
Message: msg,
|
|
}, nil
|
|
}
|
|
|
|
// EncryptError is used to make data obfuscation using the generated shared
|
|
// secret.
|
|
//
|
|
// In context of Lightning Network is either used by the nodes in order to make
|
|
// initial obfuscation with the creation of the hmac or by the forwarding nodes
|
|
// for backward failure obfuscation of the onion failure blob. By obfuscating
|
|
// the onion failure on every node in the path we are adding additional step of
|
|
// the security and barrier for malware nodes to retrieve valuable information.
|
|
// The reason for using onion obfuscation is to not give
|
|
// away to the nodes in the payment path the information about the exact
|
|
// failure and its origin.
|
|
func (o *OnionErrorEncrypter) EncryptError(initial bool, data []byte) []byte {
|
|
if initial {
|
|
umKey := generateKey("um", &o.sharedSecret)
|
|
hash := hmac.New(sha256.New, umKey[:])
|
|
hash.Write(data)
|
|
h := hash.Sum(nil)
|
|
data = append(h, data...)
|
|
}
|
|
|
|
return onionEncrypt(&o.sharedSecret, data)
|
|
}
|