mirror of
https://github.com/muun/recovery.git
synced 2025-02-23 11:32:33 -05:00
779 lines
27 KiB
Go
779 lines
27 KiB
Go
package sphinx
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcutil"
|
|
)
|
|
|
|
const (
|
|
// addressSize is the length of the serialized address used to uniquely
|
|
// identify the next hop to forward the onion to. BOLT 04 defines this
|
|
// as 8 byte channel_id.
|
|
AddressSize = 8
|
|
|
|
// RealmByteSize is the number of bytes that the realm byte occupies.
|
|
RealmByteSize = 1
|
|
|
|
// AmtForwardSize is the number of bytes that the amount to forward
|
|
// occupies.
|
|
AmtForwardSize = 8
|
|
|
|
// OutgoingCLTVSize is the number of bytes that the outgoing CLTV value
|
|
// occupies.
|
|
OutgoingCLTVSize = 4
|
|
|
|
// NumPaddingBytes is the number of padding bytes in the hopData. These
|
|
// bytes are currently unused within the protocol, and are reserved for
|
|
// future use. However, if a hop contains extra data, then we'll
|
|
// utilize this space to pack in the unrolled bytes.
|
|
NumPaddingBytes = 12
|
|
|
|
// LegacyHopDataSize is the fixed size of hop_data. BOLT 04 currently
|
|
// specifies this to be 1 byte realm, 8 byte channel_id, 8 byte amount
|
|
// to forward, 4 byte outgoing CLTV value, 12 bytes padding and 32 bytes
|
|
// HMAC for a total of 65 bytes per hop.
|
|
LegacyHopDataSize = (RealmByteSize + AddressSize + AmtForwardSize +
|
|
OutgoingCLTVSize + NumPaddingBytes + HMACSize)
|
|
|
|
// MaxPayloadSize is the maximum size a payload for a single hop can be.
|
|
// This is the worst case scenario of a single hop, consuming all
|
|
// available space. We need to know this in order to generate a
|
|
// sufficiently long stream of pseudo-random bytes when
|
|
// encrypting/decrypting the payload.
|
|
MaxPayloadSize = routingInfoSize
|
|
|
|
// routingInfoSize is the fixed size of the the routing info. This
|
|
// consists of a addressSize byte address and a HMACSize byte HMAC for
|
|
// each hop of the route, the first pair in cleartext and the following
|
|
// pairs increasingly obfuscated. If not all space is used up, the
|
|
// remainder is padded with null-bytes, also obfuscated.
|
|
routingInfoSize = 1300
|
|
|
|
// numStreamBytes is the number of bytes produced by our CSPRG for the
|
|
// key stream implementing our stream cipher to encrypt/decrypt the mix
|
|
// header. The MaxPayloadSize bytes at the end are used to
|
|
// encrypt/decrypt the fillers when processing the packet of generating
|
|
// the HMACs when creating the packet.
|
|
numStreamBytes = routingInfoSize * 2
|
|
|
|
// keyLen is the length of the keys used to generate cipher streams and
|
|
// encrypt payloads. Since we use SHA256 to generate the keys, the
|
|
// maximum length currently is 32 bytes.
|
|
keyLen = 32
|
|
|
|
// baseVersion represent the current supported version of onion packet.
|
|
baseVersion = 0
|
|
)
|
|
|
|
var (
|
|
ErrMaxRoutingInfoSizeExceeded = fmt.Errorf(
|
|
"max routing info size of %v bytes exceeded", routingInfoSize)
|
|
)
|
|
|
|
// OnionPacket is the onion wrapped hop-to-hop routing information necessary to
|
|
// propagate a message through the mix-net without intermediate nodes having
|
|
// knowledge of their position within the route, the source, the destination,
|
|
// and finally the identities of the past/future nodes in the route. At each
|
|
// hop the ephemeral key is used by the node to perform ECDH between itself and
|
|
// the source node. This derived secret key is used to check the MAC of the
|
|
// entire mix header, decrypt the next set of routing information, and
|
|
// re-randomize the ephemeral key for the next node in the path. This per-hop
|
|
// re-randomization allows us to only propagate a single group element through
|
|
// the onion route.
|
|
type OnionPacket struct {
|
|
// Version denotes the version of this onion packet. The version
|
|
// indicates how a receiver of the packet should interpret the bytes
|
|
// following this version byte. Currently, a version of 0x00 is the
|
|
// only defined version type.
|
|
Version byte
|
|
|
|
// EphemeralKey is the public key that each hop will used in
|
|
// combination with the private key in an ECDH to derive the shared
|
|
// secret used to check the HMAC on the packet and also decrypted the
|
|
// routing information.
|
|
EphemeralKey *btcec.PublicKey
|
|
|
|
// RoutingInfo is the full routing information for this onion packet.
|
|
// This encodes all the forwarding instructions for this current hop
|
|
// and all the hops in the route.
|
|
RoutingInfo [routingInfoSize]byte
|
|
|
|
// HeaderMAC is an HMAC computed with the shared secret of the routing
|
|
// data and the associated data for this route. Including the
|
|
// associated data lets each hop authenticate higher-level data that is
|
|
// critical for the forwarding of this HTLC.
|
|
HeaderMAC [HMACSize]byte
|
|
}
|
|
|
|
// generateSharedSecrets by the given nodes pubkeys, generates the shared
|
|
// secrets.
|
|
func generateSharedSecrets(paymentPath []*btcec.PublicKey,
|
|
sessionKey *btcec.PrivateKey) []Hash256 {
|
|
|
|
// Each hop performs ECDH with our ephemeral key pair to arrive at a
|
|
// shared secret. Additionally, each hop randomizes the group element
|
|
// for the next hop by multiplying it by the blinding factor. This way
|
|
// we only need to transmit a single group element, and hops can't link
|
|
// a session back to us if they have several nodes in the path.
|
|
numHops := len(paymentPath)
|
|
hopSharedSecrets := make([]Hash256, numHops)
|
|
|
|
// Compute the triplet for the first hop outside of the main loop.
|
|
// Within the loop each new triplet will be computed recursively based
|
|
// off of the blinding factor of the last hop.
|
|
lastEphemeralPubKey := sessionKey.PubKey()
|
|
hopSharedSecrets[0] = generateSharedSecret(paymentPath[0], sessionKey)
|
|
lastBlindingFactor := computeBlindingFactor(lastEphemeralPubKey, hopSharedSecrets[0][:])
|
|
|
|
// The cached blinding factor will contain the running product of the
|
|
// session private key x and blinding factors b_i, computed as
|
|
// c_0 = x
|
|
// c_i = c_{i-1} * b_{i-1} (mod |F(G)|).
|
|
// = x * b_0 * b_1 * ... * b_{i-1} (mod |F(G)|).
|
|
//
|
|
// We begin with just the session private key x, so that base case
|
|
// c_0 = x. At the beginning of each iteration, the previous blinding
|
|
// factor is aggregated into the modular product, and used as the scalar
|
|
// value in deriving the hop ephemeral keys and shared secrets.
|
|
var cachedBlindingFactor big.Int
|
|
cachedBlindingFactor.SetBytes(sessionKey.D.Bytes())
|
|
|
|
// Now recursively compute the cached blinding factor, ephemeral ECDH
|
|
// pub keys, and shared secret for each hop.
|
|
var nextBlindingFactor big.Int
|
|
for i := 1; i <= numHops-1; i++ {
|
|
// Update the cached blinding factor with b_{i-1}.
|
|
nextBlindingFactor.SetBytes(lastBlindingFactor[:])
|
|
cachedBlindingFactor.Mul(&cachedBlindingFactor, &nextBlindingFactor)
|
|
cachedBlindingFactor.Mod(&cachedBlindingFactor, btcec.S256().Params().N)
|
|
|
|
// a_i = g ^ c_i
|
|
// = g^( x * b_0 * ... * b_{i-1} )
|
|
// = X^( b_0 * ... * b_{i-1} )
|
|
// X_our_session_pub_key x all prev blinding factors
|
|
lastEphemeralPubKey = blindBaseElement(cachedBlindingFactor.Bytes())
|
|
|
|
// e_i = Y_i ^ c_i
|
|
// = ( Y_i ^ x )^( b_0 * ... * b_{i-1} )
|
|
// (Y_their_pub_key x x_our_priv) x all prev blinding factors
|
|
hopBlindedPubKey := blindGroupElement(
|
|
paymentPath[i], cachedBlindingFactor.Bytes(),
|
|
)
|
|
|
|
// s_i = sha256( e_i )
|
|
// = sha256( Y_i ^ (x * b_0 * ... * b_{i-1} )
|
|
hopSharedSecrets[i] = sha256.Sum256(hopBlindedPubKey.SerializeCompressed())
|
|
|
|
// Only need to evaluate up to the penultimate blinding factor.
|
|
if i >= numHops-1 {
|
|
break
|
|
}
|
|
|
|
// b_i = sha256( a_i || s_i )
|
|
lastBlindingFactor = computeBlindingFactor(
|
|
lastEphemeralPubKey, hopSharedSecrets[i][:],
|
|
)
|
|
}
|
|
|
|
return hopSharedSecrets
|
|
}
|
|
|
|
// NewOnionPacket creates a new onion packet which is capable of obliviously
|
|
// routing a message through the mix-net path outline by 'paymentPath'.
|
|
func NewOnionPacket(paymentPath *PaymentPath, sessionKey *btcec.PrivateKey,
|
|
assocData []byte, pktFiller PacketFiller) (*OnionPacket, error) {
|
|
|
|
// Check whether total payload size doesn't exceed the hard maximum.
|
|
if paymentPath.TotalPayloadSize() > routingInfoSize {
|
|
return nil, ErrMaxRoutingInfoSizeExceeded
|
|
}
|
|
|
|
// If we don't actually have a partially populated route, then we'll
|
|
// exit early.
|
|
numHops := paymentPath.TrueRouteLength()
|
|
if numHops == 0 {
|
|
return nil, fmt.Errorf("route of length zero passed in")
|
|
}
|
|
|
|
// We'll force the caller to provide a packet filler, as otherwise we
|
|
// may default to an insecure filling method (which should only really
|
|
// be used to generate test vectors).
|
|
if pktFiller == nil {
|
|
return nil, fmt.Errorf("packet filler must be specified")
|
|
}
|
|
|
|
hopSharedSecrets := generateSharedSecrets(
|
|
paymentPath.NodeKeys(), sessionKey,
|
|
)
|
|
|
|
// Generate the padding, called "filler strings" in the paper.
|
|
filler := generateHeaderPadding("rho", paymentPath, hopSharedSecrets)
|
|
|
|
// Allocate zero'd out byte slices to store the final mix header packet
|
|
// and the hmac for each hop.
|
|
var (
|
|
mixHeader [routingInfoSize]byte
|
|
nextHmac [HMACSize]byte
|
|
hopPayloadBuf bytes.Buffer
|
|
)
|
|
|
|
// Fill the packet using the caller specified methodology.
|
|
if err := pktFiller(sessionKey, &mixHeader); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Now we compute the routing information for each hop, along with a
|
|
// MAC of the routing info using the shared key for that hop.
|
|
for i := numHops - 1; i >= 0; i-- {
|
|
// We'll derive the two keys we need for each hop in order to:
|
|
// generate our stream cipher bytes for the mixHeader, and
|
|
// calculate the MAC over the entire constructed packet.
|
|
rhoKey := generateKey("rho", &hopSharedSecrets[i])
|
|
muKey := generateKey("mu", &hopSharedSecrets[i])
|
|
|
|
// The HMAC for the final hop is simply zeroes. This allows the
|
|
// last hop to recognize that it is the destination for a
|
|
// particular payment.
|
|
paymentPath[i].HopPayload.HMAC = nextHmac
|
|
|
|
// Next, using the key dedicated for our stream cipher, we'll
|
|
// generate enough bytes to obfuscate this layer of the onion
|
|
// packet.
|
|
streamBytes := generateCipherStream(rhoKey, routingInfoSize)
|
|
payload := paymentPath[i].HopPayload
|
|
|
|
// Before we assemble the packet, we'll shift the current
|
|
// mix-header to the right in order to make room for this next
|
|
// per-hop data.
|
|
shiftSize := payload.NumBytes()
|
|
rightShift(mixHeader[:], shiftSize)
|
|
|
|
err := payload.Encode(&hopPayloadBuf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
copy(mixHeader[:], hopPayloadBuf.Bytes())
|
|
|
|
// Once the packet for this hop has been assembled, we'll
|
|
// re-encrypt the packet by XOR'ing with a stream of bytes
|
|
// generated using our shared secret.
|
|
xor(mixHeader[:], mixHeader[:], streamBytes[:])
|
|
|
|
// If this is the "last" hop, then we'll override the tail of
|
|
// the hop data.
|
|
if i == numHops-1 {
|
|
copy(mixHeader[len(mixHeader)-len(filler):], filler)
|
|
}
|
|
|
|
// The packet for this hop consists of: mixHeader. When
|
|
// calculating the MAC, we'll also include the optional
|
|
// associated data which can allow higher level applications to
|
|
// prevent replay attacks.
|
|
packet := append(mixHeader[:], assocData...)
|
|
nextHmac = calcMac(muKey, packet)
|
|
|
|
hopPayloadBuf.Reset()
|
|
}
|
|
|
|
return &OnionPacket{
|
|
Version: baseVersion,
|
|
EphemeralKey: sessionKey.PubKey(),
|
|
RoutingInfo: mixHeader,
|
|
HeaderMAC: nextHmac,
|
|
}, nil
|
|
}
|
|
|
|
// rightShift shifts the byte-slice by the given number of bytes to the right
|
|
// and 0-fill the resulting gap.
|
|
func rightShift(slice []byte, num int) {
|
|
for i := len(slice) - num - 1; i >= 0; i-- {
|
|
slice[num+i] = slice[i]
|
|
}
|
|
|
|
for i := 0; i < num; i++ {
|
|
slice[i] = 0
|
|
}
|
|
}
|
|
|
|
// generateHeaderPadding derives the bytes for padding the mix header to ensure
|
|
// it remains fixed sized throughout route transit. At each step, we add
|
|
// 'frameSize*frames' padding of zeroes, concatenate it to the previous filler,
|
|
// then decrypt it (XOR) with the secret key of the current hop. When
|
|
// encrypting the mix header we essentially do the reverse of this operation:
|
|
// we "encrypt" the padding, and drop 'frameSize*frames' number of zeroes. As
|
|
// nodes process the mix header they add the padding ('frameSize*frames') in
|
|
// order to check the MAC and decrypt the next routing information eventually
|
|
// leaving only the original "filler" bytes produced by this function at the
|
|
// last hop. Using this methodology, the size of the field stays constant at
|
|
// each hop.
|
|
func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash256) []byte {
|
|
numHops := path.TrueRouteLength()
|
|
|
|
// We have to generate a filler that matches all but the last hop (the
|
|
// last hop won't generate an HMAC)
|
|
fillerSize := path.TotalPayloadSize() - path[numHops-1].HopPayload.NumBytes()
|
|
filler := make([]byte, fillerSize)
|
|
|
|
for i := 0; i < numHops-1; i++ {
|
|
// Sum up how many frames were used by prior hops.
|
|
fillerStart := routingInfoSize
|
|
for _, p := range path[:i] {
|
|
fillerStart -= p.HopPayload.NumBytes()
|
|
}
|
|
|
|
// The filler is the part dangling off of the end of the
|
|
// routingInfo, so offset it from there, and use the current
|
|
// hop's frame count as its size.
|
|
fillerEnd := routingInfoSize + path[i].HopPayload.NumBytes()
|
|
|
|
streamKey := generateKey(key, &sharedSecrets[i])
|
|
streamBytes := generateCipherStream(streamKey, numStreamBytes)
|
|
|
|
xor(filler, filler, streamBytes[fillerStart:fillerEnd])
|
|
}
|
|
|
|
return filler
|
|
}
|
|
|
|
// Encode serializes the raw bytes of the onion packet into the passed
|
|
// io.Writer. The form encoded within the passed io.Writer is suitable for
|
|
// either storing on disk, or sending over the network.
|
|
func (f *OnionPacket) Encode(w io.Writer) error {
|
|
ephemeral := f.EphemeralKey.SerializeCompressed()
|
|
|
|
if _, err := w.Write([]byte{f.Version}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := w.Write(ephemeral); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := w.Write(f.RoutingInfo[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := w.Write(f.HeaderMAC[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Decode fully populates the target ForwardingMessage from the raw bytes
|
|
// encoded within the io.Reader. In the case of any decoding errors, an error
|
|
// will be returned. If the method success, then the new OnionPacket is ready
|
|
// to be processed by an instance of SphinxNode.
|
|
func (f *OnionPacket) Decode(r io.Reader) error {
|
|
var err error
|
|
|
|
var buf [1]byte
|
|
if _, err := io.ReadFull(r, buf[:]); err != nil {
|
|
return err
|
|
}
|
|
f.Version = buf[0]
|
|
|
|
// If version of the onion packet protocol unknown for us than in might
|
|
// lead to improperly decoded data.
|
|
if f.Version != baseVersion {
|
|
return ErrInvalidOnionVersion
|
|
}
|
|
|
|
var ephemeral [33]byte
|
|
if _, err := io.ReadFull(r, ephemeral[:]); err != nil {
|
|
return err
|
|
}
|
|
f.EphemeralKey, err = btcec.ParsePubKey(ephemeral[:], btcec.S256())
|
|
if err != nil {
|
|
return ErrInvalidOnionKey
|
|
}
|
|
|
|
if _, err := io.ReadFull(r, f.RoutingInfo[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := io.ReadFull(r, f.HeaderMAC[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ProcessCode is an enum-like type which describes to the high-level package
|
|
// user which action should be taken after processing a Sphinx packet.
|
|
type ProcessCode int
|
|
|
|
const (
|
|
// ExitNode indicates that the node which processed the Sphinx packet
|
|
// is the destination hop in the route.
|
|
ExitNode = iota
|
|
|
|
// MoreHops indicates that there are additional hops left within the
|
|
// route. Therefore the caller should forward the packet to the node
|
|
// denoted as the "NextHop".
|
|
MoreHops
|
|
|
|
// Failure indicates that a failure occurred during packet processing.
|
|
Failure
|
|
)
|
|
|
|
// String returns a human readable string for each of the ProcessCodes.
|
|
func (p ProcessCode) String() string {
|
|
switch p {
|
|
case ExitNode:
|
|
return "ExitNode"
|
|
case MoreHops:
|
|
return "MoreHops"
|
|
case Failure:
|
|
return "Failure"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
// ProcessedPacket encapsulates the resulting state generated after processing
|
|
// an OnionPacket. A processed packet communicates to the caller what action
|
|
// should be taken after processing.
|
|
type ProcessedPacket struct {
|
|
// Action represents the action the caller should take after processing
|
|
// the packet.
|
|
Action ProcessCode
|
|
|
|
// ForwardingInstructions is the per-hop payload recovered from the
|
|
// initial encrypted onion packet. It details how the packet should be
|
|
// forwarded and also includes information that allows the processor of
|
|
// the packet to authenticate the information passed within the HTLC.
|
|
//
|
|
// NOTE: This field will only be populated iff the above Action is
|
|
// MoreHops.
|
|
ForwardingInstructions *HopData
|
|
|
|
// Payload is the raw payload as extracted from the packet. If the
|
|
// ForwardingInstructions field above is nil, then this is a modern TLV
|
|
// payload. As a result, the caller should parse the contents to obtain
|
|
// the new set of forwarding instructions.
|
|
Payload HopPayload
|
|
|
|
// NextPacket is the onion packet that should be forwarded to the next
|
|
// hop as denoted by the ForwardingInstructions field.
|
|
//
|
|
// NOTE: This field will only be populated iff the above Action is
|
|
// MoreHops.
|
|
NextPacket *OnionPacket
|
|
}
|
|
|
|
// Router is an onion router within the Sphinx network. The router is capable
|
|
// of processing incoming Sphinx onion packets thereby "peeling" a layer off
|
|
// the onion encryption which the packet is wrapped with.
|
|
type Router struct {
|
|
nodeID [AddressSize]byte
|
|
nodeAddr *btcutil.AddressPubKeyHash
|
|
|
|
onionKey *btcec.PrivateKey
|
|
|
|
log ReplayLog
|
|
}
|
|
|
|
// NewRouter creates a new instance of a Sphinx onion Router given the node's
|
|
// currently advertised onion private key, and the target Bitcoin network.
|
|
func NewRouter(nodeKey *btcec.PrivateKey, net *chaincfg.Params, log ReplayLog) *Router {
|
|
var nodeID [AddressSize]byte
|
|
copy(nodeID[:], btcutil.Hash160(nodeKey.PubKey().SerializeCompressed()))
|
|
|
|
// Safe to ignore the error here, nodeID is 20 bytes.
|
|
nodeAddr, _ := btcutil.NewAddressPubKeyHash(nodeID[:], net)
|
|
|
|
return &Router{
|
|
nodeID: nodeID,
|
|
nodeAddr: nodeAddr,
|
|
onionKey: &btcec.PrivateKey{
|
|
PublicKey: ecdsa.PublicKey{
|
|
Curve: btcec.S256(),
|
|
X: nodeKey.X,
|
|
Y: nodeKey.Y,
|
|
},
|
|
D: nodeKey.D,
|
|
},
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
// Start starts / opens the ReplayLog's channeldb and its accompanying
|
|
// garbage collector goroutine.
|
|
func (r *Router) Start() error {
|
|
return r.log.Start()
|
|
}
|
|
|
|
// Stop stops / closes the ReplayLog's channeldb and its accompanying
|
|
// garbage collector goroutine.
|
|
func (r *Router) Stop() {
|
|
r.log.Stop()
|
|
}
|
|
|
|
// ProcessOnionPacket processes an incoming onion packet which has been forward
|
|
// to the target Sphinx router. If the encoded ephemeral key isn't on the
|
|
// target Elliptic Curve, then the packet is rejected. Similarly, if the
|
|
// derived shared secret has been seen before the packet is rejected. Finally
|
|
// if the MAC doesn't check the packet is again rejected.
|
|
//
|
|
// In the case of a successful packet processing, and ProcessedPacket struct is
|
|
// returned which houses the newly parsed packet, along with instructions on
|
|
// what to do next.
|
|
func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket,
|
|
assocData []byte, incomingCltv uint32) (*ProcessedPacket, error) {
|
|
|
|
// Compute the shared secret for this onion packet.
|
|
sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Additionally, compute the hash prefix of the shared secret, which
|
|
// will serve as an identifier for detecting replayed packets.
|
|
hashPrefix := hashSharedSecret(&sharedSecret)
|
|
|
|
// Continue to optimistically process this packet, deferring replay
|
|
// protection until the end to reduce the penalty of multiple IO
|
|
// operations.
|
|
packet, err := processOnionPacket(onionPkt, &sharedSecret, assocData, r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Atomically compare this hash prefix with the contents of the on-disk
|
|
// log, persisting it only if this entry was not detected as a replay.
|
|
if err := r.log.Put(hashPrefix, incomingCltv); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return packet, nil
|
|
}
|
|
|
|
// ReconstructOnionPacket rederives the subsequent onion packet.
|
|
//
|
|
// NOTE: This method does not do any sort of replay protection, and should only
|
|
// be used to reconstruct packets that were successfully processed previously.
|
|
func (r *Router) ReconstructOnionPacket(onionPkt *OnionPacket,
|
|
assocData []byte) (*ProcessedPacket, error) {
|
|
|
|
// Compute the shared secret for this onion packet.
|
|
sharedSecret, err := r.generateSharedSecret(onionPkt.EphemeralKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return processOnionPacket(onionPkt, &sharedSecret, assocData, r)
|
|
}
|
|
|
|
// unwrapPacket wraps a layer of the passed onion packet using the specified
|
|
// shared secret and associated data. The associated data will be used to check
|
|
// the HMAC at each hop to ensure the same data is passed along with the onion
|
|
// packet. This function returns the next inner onion packet layer, along with
|
|
// the hop data extracted from the outer onion packet.
|
|
func unwrapPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
|
|
assocData []byte) (*OnionPacket, *HopPayload, error) {
|
|
|
|
dhKey := onionPkt.EphemeralKey
|
|
routeInfo := onionPkt.RoutingInfo
|
|
headerMac := onionPkt.HeaderMAC
|
|
|
|
// Using the derived shared secret, ensure the integrity of the routing
|
|
// information by checking the attached MAC without leaking timing
|
|
// information.
|
|
message := append(routeInfo[:], assocData...)
|
|
calculatedMac := calcMac(generateKey("mu", sharedSecret), message)
|
|
if !hmac.Equal(headerMac[:], calculatedMac[:]) {
|
|
return nil, nil, ErrInvalidOnionHMAC
|
|
}
|
|
|
|
// Attach the padding zeroes in order to properly strip an encryption
|
|
// layer off the routing info revealing the routing information for the
|
|
// next hop.
|
|
streamBytes := generateCipherStream(
|
|
generateKey("rho", sharedSecret), numStreamBytes,
|
|
)
|
|
zeroBytes := bytes.Repeat([]byte{0}, MaxPayloadSize)
|
|
headerWithPadding := append(routeInfo[:], zeroBytes...)
|
|
|
|
var hopInfo [numStreamBytes]byte
|
|
xor(hopInfo[:], headerWithPadding, streamBytes)
|
|
|
|
// Randomize the DH group element for the next hop using the
|
|
// deterministic blinding factor.
|
|
blindingFactor := computeBlindingFactor(dhKey, sharedSecret[:])
|
|
nextDHKey := blindGroupElement(dhKey, blindingFactor[:])
|
|
|
|
// With the MAC checked, and the payload decrypted, we can now parse
|
|
// out the payload so we can derive the specified forwarding
|
|
// instructions.
|
|
var hopPayload HopPayload
|
|
if err := hopPayload.Decode(bytes.NewReader(hopInfo[:])); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// With the necessary items extracted, we'll copy of the onion packet
|
|
// for the next node, snipping off our per-hop data.
|
|
var nextMixHeader [routingInfoSize]byte
|
|
copy(nextMixHeader[:], hopInfo[hopPayload.NumBytes():])
|
|
innerPkt := &OnionPacket{
|
|
Version: onionPkt.Version,
|
|
EphemeralKey: nextDHKey,
|
|
RoutingInfo: nextMixHeader,
|
|
HeaderMAC: hopPayload.HMAC,
|
|
}
|
|
|
|
return innerPkt, &hopPayload, nil
|
|
}
|
|
|
|
// processOnionPacket performs the primary key derivation and handling of onion
|
|
// packets. The processed packets returned from this method should only be used
|
|
// if the packet was not flagged as a replayed packet.
|
|
func processOnionPacket(onionPkt *OnionPacket, sharedSecret *Hash256,
|
|
assocData []byte,
|
|
sharedSecretGen sharedSecretGenerator) (*ProcessedPacket, error) {
|
|
|
|
// First, we'll unwrap an initial layer of the onion packet. Typically,
|
|
// we'll only have a single layer to unwrap, However, if the sender has
|
|
// additional data for us within the Extra Onion Blobs (EOBs), then we
|
|
// may have to unwrap additional layers. By default, the inner most
|
|
// mix header is the one that we'll want to pass onto the next hop so
|
|
// they can properly check the HMAC and unwrap a layer for their
|
|
// handoff hop.
|
|
innerPkt, outerHopPayload, err := unwrapPacket(
|
|
onionPkt, sharedSecret, assocData,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// By default we'll assume that there are additional hops in the route.
|
|
// However if the uncovered 'nextMac' is all zeroes, then this
|
|
// indicates that we're the final hop in the route.
|
|
var action ProcessCode = MoreHops
|
|
if bytes.Compare(zeroHMAC[:], outerHopPayload.HMAC[:]) == 0 {
|
|
action = ExitNode
|
|
}
|
|
|
|
hopData, err := outerHopPayload.HopData()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Finally, we'll return a fully processed packet with the outer most
|
|
// hop data (where the primary forwarding instructions lie) and the
|
|
// inner most onion packet that we unwrapped.
|
|
return &ProcessedPacket{
|
|
Action: action,
|
|
ForwardingInstructions: hopData,
|
|
Payload: *outerHopPayload,
|
|
NextPacket: innerPkt,
|
|
}, nil
|
|
}
|
|
|
|
// Tx is a transaction consisting of a number of sphinx packets to be atomically
|
|
// written to the replay log. This structure helps to coordinate construction of
|
|
// the underlying Batch object, and to ensure that the result of the processing
|
|
// is idempotent.
|
|
type Tx struct {
|
|
// batch is the set of packets to be incrementally processed and
|
|
// ultimately committed in this transaction
|
|
batch *Batch
|
|
|
|
// router is a reference to the sphinx router that created this
|
|
// transaction. Committing this transaction will utilize this router's
|
|
// replay log.
|
|
router *Router
|
|
|
|
// packets contains a potentially sparse list of optimistically processed
|
|
// packets for this batch. The contents of a particular index should
|
|
// only be accessed if the index is *not* included in the replay set, or
|
|
// otherwise failed any other stage of the processing.
|
|
packets []ProcessedPacket
|
|
}
|
|
|
|
// BeginTxn creates a new transaction that can later be committed back to the
|
|
// sphinx router's replay log.
|
|
//
|
|
// NOTE: The nels parameter should represent the maximum number of that could
|
|
// be added to the batch, using sequence numbers that match or exceed this
|
|
// value could result in an out-of-bounds panic.
|
|
func (r *Router) BeginTxn(id []byte, nels int) *Tx {
|
|
return &Tx{
|
|
batch: NewBatch(id),
|
|
router: r,
|
|
packets: make([]ProcessedPacket, nels),
|
|
}
|
|
}
|
|
|
|
// ProcessOnionPacket processes an incoming onion packet which has been forward
|
|
// to the target Sphinx router. If the encoded ephemeral key isn't on the
|
|
// target Elliptic Curve, then the packet is rejected. Similarly, if the
|
|
// derived shared secret has been seen before the packet is rejected. Finally
|
|
// if the MAC doesn't check the packet is again rejected.
|
|
//
|
|
// In the case of a successful packet processing, and ProcessedPacket struct is
|
|
// returned which houses the newly parsed packet, along with instructions on
|
|
// what to do next.
|
|
func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,
|
|
assocData []byte, incomingCltv uint32) error {
|
|
|
|
// Compute the shared secret for this onion packet.
|
|
sharedSecret, err := t.router.generateSharedSecret(
|
|
onionPkt.EphemeralKey,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Additionally, compute the hash prefix of the shared secret, which
|
|
// will serve as an identifier for detecting replayed packets.
|
|
hashPrefix := hashSharedSecret(&sharedSecret)
|
|
|
|
// Continue to optimistically process this packet, deferring replay
|
|
// protection until the end to reduce the penalty of multiple IO
|
|
// operations.
|
|
packet, err := processOnionPacket(
|
|
onionPkt, &sharedSecret, assocData, t.router,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add the hash prefix to pending batch of shared secrets that will be
|
|
// written later via Commit().
|
|
err = t.batch.Put(seqNum, hashPrefix, incomingCltv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If we successfully added this packet to the batch, cache the
|
|
// processed packet within the Tx which can be accessed after
|
|
// committing if this sequence number does not appear in the replay
|
|
// set.
|
|
t.packets[seqNum] = *packet
|
|
|
|
return nil
|
|
}
|
|
|
|
// Commit writes this transaction's batch of sphinx packets to the replay log,
|
|
// performing a final check against the log for replays.
|
|
func (t *Tx) Commit() ([]ProcessedPacket, *ReplaySet, error) {
|
|
if t.batch.IsCommitted {
|
|
return t.packets, t.batch.ReplaySet, nil
|
|
}
|
|
|
|
rs, err := t.router.log.PutBatch(t.batch)
|
|
|
|
return t.packets, rs, err
|
|
}
|