bolt4

Onion routing protocol, per BOLT #4 (docs.ppad.tech/bolt4).
git clone git://git.ppad.tech/bolt4.git
Log | Files | Refs | README | LICENSE

Blinding.hs (5242B)


      1 {-# OPTIONS_HADDOCK hide #-}
      2 {-# LANGUAGE DeriveGeneric #-}
      3 
      4 -- |
      5 -- Module: Lightning.Protocol.BOLT4.Blinding
      6 -- Copyright: (c) 2025 Jared Tobin
      7 -- License: MIT
      8 -- Maintainer: Jared Tobin <jared@ppad.tech>
      9 --
     10 -- Route blinding.
     11 
     12 module Lightning.Protocol.BOLT4.Blinding (
     13     BlindingError(..)
     14   , create_blinded_path
     15   , decrypt_recipient_data
     16   , unblind
     17   ) where
     18 
     19 import Control.DeepSeq (NFData)
     20 import qualified Crypto.AEAD.ChaCha20Poly1305 as AEAD
     21 import qualified Crypto.Curve.Secp256k1 as Secp256k1
     22 import qualified Data.ByteString as BS
     23 import GHC.Generics (Generic)
     24 import qualified Lightning.Protocol.BOLT1 as BOLT1
     25 import Lightning.Protocol.BOLT4.Codec
     26 import Lightning.Protocol.BOLT4.Prim
     27 import Lightning.Protocol.BOLT4.Types
     28 
     29 -- | Why a blinded route could not be created.
     30 data BlindingError
     31   = EmptyPath
     32     -- ^ the route has no hops
     33   | InvalidNodeId {-# UNPACK #-} !Int
     34     -- ^ the node id of the hop with this index is not a valid point
     35   | InvalidHopData {-# UNPACK #-} !Int !EncodeError
     36     -- ^ the data of the hop with this index can't be encoded
     37   deriving (Eq, Show, Generic)
     38 
     39 instance NFData BlindingError
     40 
     41 -- | Create a blinded route to oneself, given a seed for the route's
     42 --   ephemeral keys (which must be fresh and random for every route) and
     43 --   each node's id and data, from the introduction node to oneself.
     44 --
     45 --   >>> let Just seed = secret_key (BS.replicate 32 0x01)
     46 --   >>> let Just me = secret_key (BS.replicate 32 0x45)
     47 --   >>> let dat = empty_blinded_hop_data { bhd_path_id = Just "deadbeef" }
     48 --   >>> let Right path = create_blinded_path seed [(public_key me, dat)]
     49 --   >>> bp_first_node_id path == public_key me
     50 --   True
     51 --   >>> length (bp_hops path)
     52 --   1
     53 create_blinded_path
     54   :: SecretKey
     55   -> [(BOLT1.Point, BlindedHopData)]
     56   -> Either BlindingError BlindedPath
     57 create_blinded_path seed nodes = case nodes of
     58   [] -> Left EmptyPath
     59   (intro, _) : _ -> do
     60     hops <- go (sk_bytes seed) (sk_pub seed) (zip [0 ..] nodes)
     61     pure (BlindedPath intro (public_key seed) hops)
     62   where
     63     go _ _ [] = Right []
     64     go e epub ((i, (nid, dat)) : rest) = do
     65       let note = maybe (Left (InvalidNodeId i)) Right
     66       node <- note (from_point nid)
     67       ss <- note (ecdh e node)
     68       bid <- note (blind_pub node (blinded_node_tweak ss) >>= to_point)
     69       plain <- either (Left . InvalidHopData i) Right
     70                  (encode_blinded_hop_data dat)
     71       enc <- maybe (Left (InvalidHopData i FieldTooLong)) Right
     72                (encrypt (derive_rho ss) plain)
     73       let hop = BlindedHop bid enc
     74       case rest of
     75         [] -> Right [hop]
     76         _  -> do
     77           let bf = blinding_factor epub ss
     78           e' <- note (blind_scalar e bf)
     79           epub' <- note (blind_pub epub bf)
     80           (hop :) <$> go e' epub' rest
     81 
     82 -- | Decrypt the @encrypted_recipient_data@ received by a node in a
     83 --   blinded route, given the node's private key and the path key it
     84 --   received.
     85 --
     86 --   Returns the decrypted data and the path key for the next node (the
     87 --   data's @next_path_key_override@, if present).
     88 --   'Lightning.Protocol.BOLT4.process' does this for payment onions.
     89 --
     90 --   >>> let Just seed = secret_key (BS.replicate 32 0x01)
     91 --   >>> let Just me = secret_key (BS.replicate 32 0x45)
     92 --   >>> let dat = empty_blinded_hop_data { bhd_path_id = Just "deadbeef" }
     93 --   >>> let Right path = create_blinded_path seed [(public_key me, dat)]
     94 --   >>> let [hop] = bp_hops path
     95 --   >>> let pk = bp_first_path_key path
     96 --   >>> let Right info = decrypt_recipient_data me pk (bh_encrypted_data hop)
     97 --   >>> bhd_path_id (bi_data info)
     98 --   Just "deadbeef"
     99 decrypt_recipient_data
    100   :: SecretKey
    101   -> BOLT1.Point
    102   -> BS.ByteString
    103   -> Either ProcessError BlindedInfo
    104 decrypt_recipient_data sk pk enc = do
    105   e <- maybe (Left InvalidPathKey) Right (from_point pk)
    106   ss <- maybe (Left InvalidPathKey) Right (ecdh (sk_bytes sk) e)
    107   unblind ss e enc
    108 
    109 -- Decrypt encrypted_recipient_data, given the shared secret with the path
    110 -- key, and the path key.
    111 unblind
    112   :: SharedSecret
    113   -> Secp256k1.Projective
    114   -> BS.ByteString
    115   -> Either ProcessError BlindedInfo
    116 unblind ss e enc = do
    117   plain <- maybe (Left InvalidRecipientData) Right
    118              (decrypt (derive_rho ss) enc)
    119   dat <- either (const (Left InvalidRecipientData)) Right
    120            (decode_blinded_hop_data plain)
    121   next <- case bhd_next_path_key_override dat of
    122     Just o  -> Right o
    123     Nothing -> maybe (Left InvalidPathKey) Right
    124                  (blind_pub e (blinding_factor e ss) >>= to_point)
    125   pure (BlindedInfo dat next)
    126 
    127 -- ChaCha20-Poly1305 with an all-zero nonce and no associated data; the
    128 -- tag follows the ciphertext. Fails only on counter overflow.
    129 encrypt :: DerivedKey -> BS.ByteString -> Maybe BS.ByteString
    130 encrypt k plain =
    131   case AEAD.encrypt BS.empty (un_derived_key k) (BS.replicate 12 0) plain of
    132     Right (c, t) -> Just (c <> t)
    133     Left _       -> Nothing
    134 
    135 decrypt :: DerivedKey -> BS.ByteString -> Maybe BS.ByteString
    136 decrypt k enc
    137   | BS.length enc < 16 = Nothing
    138   | otherwise =
    139       let (c, t) = BS.splitAt (BS.length enc - 16) enc
    140       in  case AEAD.decrypt BS.empty (un_derived_key k) (BS.replicate 12 0)
    141                  (c, t) of
    142             Right p -> Just p
    143             Left _  -> Nothing