diff --git a/src/Simplex/Messaging/Crypto/BIP32.hs b/src/Simplex/Messaging/Crypto/BIP32.hs index 7b7d7d60d..52c222b84 100644 --- a/src/Simplex/Messaging/Crypto/BIP32.hs +++ b/src/Simplex/Messaging/Crypto/BIP32.hs @@ -22,8 +22,10 @@ module Simplex.Messaging.Crypto.BIP32 ) where +import Control.Applicative (optional, (<|>)) import qualified Crypto.Hash as H import qualified Crypto.MAC.HMAC as HMAC +import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteArray as BA import Data.Bits (shiftR, (.&.)) import Data.ByteString (ByteString) @@ -32,13 +34,14 @@ import qualified Data.ByteString.Char8 as BC import Data.List (intercalate) import Data.Word (Word32) import qualified Simplex.Messaging.Crypto.Secp256k1 as S +import Simplex.Messaging.Parsers (parseAll) -- | An extended private key: the key plus its chain code. -- -- 'Show' is redacting — the chain code plus one child key is enough to derive -- siblings, so it is secret material too. data ExtendedKey = ExtendedKey - { xkKey :: S.PrivateKey, + { xkKey :: S.Secp256k1PrivateKey, xkChainCode :: ByteString } deriving (Eq) @@ -88,7 +91,7 @@ deriveChild xk i = where dat | isHardened i = B.singleton 0 <> S.unPrivateKey (xkKey xk) <> ser32 i - | otherwise = S.serializePublicKey S.Compressed (S.publicKey (xkKey xk)) <> ser32 i + | otherwise = S.serializePublicKey S.Compressed (S.secp256k1PublicKey (xkKey xk)) <> ser32 i hm = hmacSHA512 (xkChainCode xk) dat il = B.take 32 hm ir = B.drop 32 hm @@ -99,31 +102,19 @@ derivePath = foldl (\acc i -> acc >>= (`deriveChild` i)) . Right -- | Parse a path such as @m\/44'\/60'\/0'\/0\/0@. A leading @m@ or @M@ is -- optional; both @'@ and @h@ mark a hardened index. parsePath :: ByteString -> Either String [Word32] -parsePath s = case BC.split '/' (BC.filter (/= ' ') s) of - [] -> Right [] - (h : rest) - | h == "m" || h == "M" || B.null h -> traverse element rest - | otherwise -> traverse element (h : rest) +parsePath = parseAll pathP + +pathP :: A.Parser [Word32] +pathP = + optional (A.satisfy $ \c -> c == 'm' || c == 'M') + *> ((A.char '/' *> (indexP `A.sepBy'` A.char '/')) <|> (indexP `A.sepBy1'` A.char '/') <|> pure []) where - element e - | B.null e = Left "path: empty component" - | otherwise = - let (digits, suffix) = BC.span (`elem` ("0123456789" :: String)) e - mark - | suffix == "'" || suffix == "h" || suffix == "H" = Right True - | B.null suffix = Right False - | otherwise = Left $ "path: bad component " <> BC.unpack e - in if B.null digits - then Left $ "path: bad component " <> BC.unpack e - else do - h' <- mark - n <- readIndex digits - if h' then Right (n + hardenedOffset) else Right n - readIndex digits = - let n = BC.foldl' (\acc c -> acc * 10 + toInteger (fromEnum c - fromEnum '0')) 0 digits - in if n >= toInteger hardenedOffset - then Left $ "path: index out of range: " <> BC.unpack digits - else Right (fromInteger n) + indexP = do + n <- A.decimal :: A.Parser Integer + hard <- (True <$ A.satisfy (\c -> c == '\'' || c == 'h' || c == 'H')) <|> pure False + if n >= toInteger hardenedOffset + then fail $ "path: index out of range: " <> show n + else pure $ fromInteger n + if hard then hardenedOffset else 0 renderPath :: [Word32] -> ByteString renderPath is = BC.pack $ intercalate "/" ("m" : map component is) diff --git a/src/Simplex/Messaging/Crypto/BIP39.hs b/src/Simplex/Messaging/Crypto/BIP39.hs index bfe693379..03388265c 100644 --- a/src/Simplex/Messaging/Crypto/BIP39.hs +++ b/src/Simplex/Messaging/Crypto/BIP39.hs @@ -33,18 +33,20 @@ import Control.Concurrent.STM import Crypto.Hash (Digest, SHA256, SHA512 (..), hash) import qualified Crypto.KDF.PBKDF2 as PBKDF2 import Crypto.Random (ChaChaDRG, randomBytesGenerate) +import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteArray as BA import Data.Bits (shiftL, shiftR, (.&.), (.|.)) import Data.ByteString (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BC -import Data.Char (toLower) +import Data.Char (isSpace, toLower) import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IM import Data.List (foldl') import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Simplex.Messaging.Crypto.BIP39.English (englishWordList) +import Simplex.Messaging.Parsers (parseAll) -- | A validated BIP-39 mnemonic. Indexes and words are always consistent -- because the constructor is private and every index is in @[0, 2047]@. @@ -142,22 +144,25 @@ mnemonicToEntropy m = integerToBE (entBits `div` 8) (combined `shiftR` csBits) -- seed: 'mnemonicPhrase' always rebuilds the canonical lowercase sentence from -- the wordlist, and that is what 'mnemonicToSeed' hashes. parseMnemonic :: ByteString -> Either String Mnemonic -parseMnemonic phrase - | n `notElem` validWordCounts = - Left $ "mnemonic: expected 12, 15, 18, 21 or 24 words, got " <> show n - | otherwise = do +parseMnemonic = parseAll mnemonicP + +mnemonicP :: A.Parser Mnemonic +mnemonicP = do + ws <- A.skipSpace *> (wordP `A.sepBy1'` A.takeWhile1 isSpace) <* A.skipSpace + let n = length ws + if n `notElem` validWordCounts + then fail $ "mnemonic: expected 12, 15, 18, 21 or 24 words, got " <> show n + else do idxs <- traverse lookupWord ws let m = mnemonicFromIndexes idxs -- Stripping the checksum bits and recomputing them is the checksum check: -- if the supplied bits were wrong, the round trip cannot reproduce them. if entropyToIndexes (mnemonicToEntropy m) == idxs - then Right m - else Left "mnemonic: checksum mismatch" + then pure m + else fail "mnemonic: checksum mismatch" where - ws = BC.words $ BC.map toLower phrase - n = length ws - lookupWord w = - maybe (Left $ "mnemonic: not in wordlist: " <> BC.unpack w) Right $ M.lookup w indexByWord + wordP = BC.map toLower <$> A.takeWhile1 A.isAlpha_ascii + lookupWord w = maybe (fail $ "mnemonic: not in wordlist: " <> BC.unpack w) pure $ M.lookup w indexByWord -- | PBKDF2-HMAC-SHA512, 2048 iterations, salt @\"mnemonic\" <> passphrase@. -- Pass an empty passphrase for the common case. diff --git a/src/Simplex/Messaging/Crypto/Secp256k1.hs b/src/Simplex/Messaging/Crypto/Secp256k1.hs index dcdff54b4..5c6e2374f 100644 --- a/src/Simplex/Messaging/Crypto/Secp256k1.hs +++ b/src/Simplex/Messaging/Crypto/Secp256k1.hs @@ -18,13 +18,13 @@ -- trust it. Note there is deliberately no normalization entry point: we never -- accept a foreign signature, we only produce our own. module Simplex.Messaging.Crypto.Secp256k1 - ( PrivateKey, - PublicKey, + ( Secp256k1PrivateKey, + Secp256k1PublicKey, RecoverableSignature (..), PubKeyFormat (..), mkPrivateKey, unPrivateKey, - publicKey, + secp256k1PublicKey, parsePublicKey, serializePublicKey, privateKeyTweakAdd, @@ -43,10 +43,8 @@ where import Control.Monad (when) import Crypto.Random (drgNew, randomBytesGenerate) import qualified Data.ByteArray as BA -import qualified Data.ByteArray.Encoding as BAE import Data.ByteString (ByteString) import qualified Data.ByteString as B -import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Unsafe as BU import Foreign import Foreign.C @@ -85,27 +83,17 @@ compactSize = 64 -- | A validated secp256k1 private key: 32 bytes, in @[1, n-1]@. -- --- 'Show' is redacting and 'Eq' is constant-time, both deliberately: this key --- authorises transfers of assets with monetary value, so it must not reach a --- log through a derived 'Show' and must not leak through comparison timing. -newtype PrivateKey = PrivateKey ByteString +-- There is no 'Show' instance, so the key cannot reach a log, and 'Eq' is +-- constant-time: this key authorises transfers of assets with monetary value. +newtype Secp256k1PrivateKey = Secp256k1PrivateKey ByteString -instance Show PrivateKey where - show _ = "PrivateKey " - -instance Eq PrivateKey where - PrivateKey a == PrivateKey b = BA.constEq a b +instance Eq Secp256k1PrivateKey where + Secp256k1PrivateKey a == Secp256k1PrivateKey b = BA.constEq a b -- | A parsed public key, held in libsecp256k1's opaque 64-byte internal form. -- Use 'serializePublicKey' to get the SEC1 bytes. -newtype PublicKey = PublicKey ByteString - deriving newtype (Eq) - -instance Show PublicKey where - show pk = "PublicKey " <> BC.unpack (hex $ serializePublicKey Compressed pk) - -hex :: ByteString -> ByteString -hex = BAE.convertToBase BAE.Base16 +newtype Secp256k1PublicKey = Secp256k1PublicKey ByteString + deriving newtype (Eq, Show) -- | SEC1 output format for 'serializePublicKey'. data PubKeyFormat = Compressed | Uncompressed @@ -202,8 +190,8 @@ packPtr :: Ptr Word8 -> Int -> IO ByteString packPtr p n = B.packCStringLen (castPtr p, n) -- | Marshal a 'PublicKey' back into its opaque C representation. -withPubKeyRaw :: PublicKey -> (Ptr PubKeyRaw -> IO a) -> IO a -withPubKeyRaw (PublicKey bs) f = withBS bs $ f . castPtr +withPubKeyRaw :: Secp256k1PublicKey -> (Ptr PubKeyRaw -> IO a) -> IO a +withPubKeyRaw (Secp256k1PublicKey bs) f = withBS bs $ f . castPtr -- | Marshal a 'RecoverableSignature' into the opaque C representation, failing -- if libsecp256k1 rejects it. @@ -221,28 +209,28 @@ withRecSigRaw (RecoverableSignature compact recId) f -- | Validate 32 bytes as a private key. Rejects zero and anything at or above -- the group order, which is what makes 'publicKey' and 'signRecoverable' total. -mkPrivateKey :: ByteString -> Either String PrivateKey +mkPrivateKey :: ByteString -> Either String Secp256k1PrivateKey mkPrivateKey bs | B.length bs /= privateKeySize = Left $ "private key: expected 32 bytes, got " <> show (B.length bs) | otherwise = unsafePerformIO $ withBS bs $ \p -> do rc <- c_ec_seckey_verify secp256k1Ctx p - pure $ if rc == 1 then Right (PrivateKey bs) else Left "private key: not in [1, n-1]" + pure $ if rc == 1 then Right (Secp256k1PrivateKey bs) else Left "private key: not in [1, n-1]" -unPrivateKey :: PrivateKey -> ByteString -unPrivateKey (PrivateKey bs) = bs +unPrivateKey :: Secp256k1PrivateKey -> ByteString +unPrivateKey (Secp256k1PrivateKey bs) = bs -- | Derive the public key. Total, because 'PrivateKey' is validated. -publicKey :: PrivateKey -> PublicKey -publicKey (PrivateKey sk) = unsafePerformIO $ +secp256k1PublicKey :: Secp256k1PrivateKey -> Secp256k1PublicKey +secp256k1PublicKey (Secp256k1PrivateKey sk) = unsafePerformIO $ allocaBytes pubKeyInternalSize $ \pkPtr -> withBS sk $ \skPtr -> do rc <- c_ec_pubkey_create secp256k1Ctx pkPtr skPtr -- Cannot fail: the key was verified by mkPrivateKey. when (rc /= 1) $ ioError (userError "secp256k1_ec_pubkey_create failed on a validated key") - PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize + Secp256k1PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize -- | Parse a SEC1 point, compressed (33 bytes) or uncompressed (65 bytes). -parsePublicKey :: ByteString -> Either String PublicKey +parsePublicKey :: ByteString -> Either String Secp256k1PublicKey parsePublicKey bs | len /= compressedSize && len /= uncompressedSize = Left $ "public key: expected 33 or 65 bytes, got " <> show len @@ -251,12 +239,12 @@ parsePublicKey bs withBS bs $ \inPtr -> do rc <- c_ec_pubkey_parse secp256k1Ctx pkPtr inPtr (fromIntegral len) if rc == 1 - then Right . PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize + then Right . Secp256k1PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize else pure $ Left "public key: not a valid curve point" where len = B.length bs -serializePublicKey :: PubKeyFormat -> PublicKey -> ByteString +serializePublicKey :: PubKeyFormat -> Secp256k1PublicKey -> ByteString serializePublicKey fmt pk = unsafePerformIO $ allocaBytes outLen $ \outPtr -> alloca $ \lenPtr -> @@ -275,8 +263,8 @@ serializePublicKey fmt pk = unsafePerformIO $ -- -- 'Nothing' when the result is zero or the tweak is out of range — BIP-32 -- requires the caller to skip to the next child index in that case. -privateKeyTweakAdd :: PrivateKey -> ByteString -> Maybe PrivateKey -privateKeyTweakAdd (PrivateKey sk) tweak +privateKeyTweakAdd :: Secp256k1PrivateKey -> ByteString -> Maybe Secp256k1PrivateKey +privateKeyTweakAdd (Secp256k1PrivateKey sk) tweak | B.length tweak /= privateKeySize = Nothing | otherwise = unsafePerformIO $ allocaBytes privateKeySize $ \skPtr -> @@ -284,7 +272,7 @@ privateKeyTweakAdd (PrivateKey sk) tweak withBS sk $ \src -> copyBytes skPtr src privateKeySize rc <- c_ec_seckey_tweak_add secp256k1Ctx skPtr twPtr if rc == 1 - then Just . PrivateKey <$> packPtr skPtr privateKeySize + then Just . Secp256k1PrivateKey <$> packPtr skPtr privateKeySize else pure Nothing -- | @tweak * P@. The scalar multiplication behind an ECDH shared secret. @@ -295,17 +283,17 @@ privateKeyTweakAdd (PrivateKey sk) tweak -- caller. -- -- 'Nothing' when the tweak is zero or out of range. -publicKeyTweakMul :: PublicKey -> ByteString -> Maybe PublicKey +publicKeyTweakMul :: Secp256k1PublicKey -> ByteString -> Maybe Secp256k1PublicKey publicKeyTweakMul = tweakPubKey c_ec_pubkey_tweak_mul -- | @P + tweak * G@, the point addition stealth address derivation needs. -- -- 'Nothing' when the tweak is out of range or the result is the point at -- infinity. -publicKeyTweakAdd :: PublicKey -> ByteString -> Maybe PublicKey +publicKeyTweakAdd :: Secp256k1PublicKey -> ByteString -> Maybe Secp256k1PublicKey publicKeyTweakAdd = tweakPubKey c_ec_pubkey_tweak_add -tweakPubKey :: (Ptr Ctx -> Ptr PubKeyRaw -> Ptr Word8 -> IO CInt) -> PublicKey -> ByteString -> Maybe PublicKey +tweakPubKey :: (Ptr Ctx -> Ptr PubKeyRaw -> Ptr Word8 -> IO CInt) -> Secp256k1PublicKey -> ByteString -> Maybe Secp256k1PublicKey tweakPubKey f pk tweak | B.length tweak /= privateKeySize = Nothing | otherwise = unsafePerformIO $ @@ -314,12 +302,12 @@ tweakPubKey f pk tweak withPubKeyRaw pk $ \src -> copyBytes (castPtr pkPtr) (castPtr src) pubKeyInternalSize rc <- f secp256k1Ctx pkPtr twPtr if rc == 1 - then Just . PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize + then Just . Secp256k1PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize else pure Nothing -- | Sign a 32-byte digest. Deterministic (RFC 6979) and always low-@s@. -signRecoverable :: PrivateKey -> ByteString -> Either String RecoverableSignature -signRecoverable (PrivateKey sk) digest +signRecoverable :: Secp256k1PrivateKey -> ByteString -> Either String RecoverableSignature +signRecoverable (Secp256k1PrivateKey sk) digest | B.length digest /= digestSize = Left $ "digest: expected 32 bytes, got " <> show (B.length digest) | otherwise = unsafePerformIO $ @@ -340,7 +328,7 @@ signRecoverable (PrivateKey sk) digest pure $ Right RecoverableSignature {rsCompact = compact, rsRecId = fromIntegral recId} -- | Recover the signing public key from a signature and the digest it signed. -recoverPublicKey :: RecoverableSignature -> ByteString -> Either String PublicKey +recoverPublicKey :: RecoverableSignature -> ByteString -> Either String Secp256k1PublicKey recoverPublicKey sig digest | B.length digest /= digestSize = Left $ "digest: expected 32 bytes, got " <> show (B.length digest) @@ -350,7 +338,7 @@ recoverPublicKey sig digest withBS digest $ \msgPtr -> do rc <- c_ecdsa_recover secp256k1Ctx pkPtr sigPtr msgPtr if rc == 1 - then Right . PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize + then Right . Secp256k1PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize else pure $ Left "secp256k1_ecdsa_recover failed" -- | Whether @s <= n/2@, i.e. the signature is in the canonical form EIP-2 diff --git a/src/Simplex/Messaging/Eth/Address.hs b/src/Simplex/Messaging/Eth/Address.hs index 8acb5f9e1..9646cadc6 100644 --- a/src/Simplex/Messaging/Eth/Address.hs +++ b/src/Simplex/Messaging/Eth/Address.hs @@ -16,6 +16,7 @@ module Simplex.Messaging.Eth.Address ) where +import Control.Applicative (optional, (<|>)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bits (shiftR, (.&.)) @@ -42,9 +43,21 @@ instance Show Address where -- Parsing accepts bare or @0x@-prefixed hex and verifies a mixed-case checksum. instance StrEncoding Address where strEncode = checksumAddress - strP = either fail pure . parseAddress =<< A.takeWhile1 isHexOr0x + strP = do + _ <- optional $ A.string "0x" <|> A.string "0X" + body <- A.takeWhile1 isHexDigit + if B.length body /= addressSize * 2 + then fail $ "address: expected 40 hex digits, got " <> show (B.length body) + else case fromHex (BC.map toLower body) of + Left _ -> fail "address: not hexadecimal" + Right bs + | mixedCase body && checksumAddress (Address bs) /= "0x" <> body -> + fail "address: EIP-55 checksum mismatch" + | otherwise -> pure $ Address bs where - isHexOr0x c = isHexDigit c || c == 'x' || c == 'X' + mixedCase body = BC.any isUpper letters && BC.any isLower letters + where + letters = BC.filter (not . isDigit) body instance ToJSON Address where toEncoding = strToJEncoding @@ -66,12 +79,12 @@ mkAddress bs -- | The low 20 bytes of @keccak256@ of the uncompressed public key with its -- @0x04@ SEC1 prefix removed. -addressFromPublicKey :: S.PublicKey -> Address +addressFromPublicKey :: S.Secp256k1PublicKey -> Address addressFromPublicKey pk = Address . B.drop 12 . keccak256 . B.drop 1 $ S.serializePublicKey S.Uncompressed pk -addressFromPrivateKey :: S.PrivateKey -> Address -addressFromPrivateKey = addressFromPublicKey . S.publicKey +addressFromPrivateKey :: S.Secp256k1PrivateKey -> Address +addressFromPrivateKey = addressFromPublicKey . S.secp256k1PublicKey -- | EIP-55: @0x@ followed by 40 hex digits whose case encodes a checksum over -- the lowercase hex form. @@ -94,19 +107,7 @@ checksumAddress (Address bs) = "0x" <> B.pack (zipWith adjust [0 ..] lowerHex) -- its EIP-55 checksum; an all-lowercase or all-uppercase one carries no -- checksum and is accepted as-is, which is what every Ethereum client does. parseAddress :: ByteString -> Either String Address -parseAddress s - | B.length body /= 40 = Left $ "address: expected 40 hex digits, got " <> show (B.length body) - | otherwise = case fromHex (BC.map toLower body) of - Left _ -> Left "address: not hexadecimal" - Right bs - | mixedCase && checksumAddress (Address bs) /= "0x" <> body -> - Left "address: EIP-55 checksum mismatch" - | otherwise -> Right (Address bs) - where - body = if "0x" `B.isPrefixOf` s || "0X" `B.isPrefixOf` s then B.drop 2 s else s - bodyC = BC.unpack body - letters = filter (not . isDigit) bodyC - mixedCase = any isUpper letters && any isLower letters +parseAddress = strDecode -- | BIP-44 path for Ethereum account @i@, address @k@: @m\/44'\/60'\/i'\/0\/k@. -- The account must be below 'hardenedOffset', as 'hardened' returns anything diff --git a/src/Simplex/Messaging/Eth/EIP712.hs b/src/Simplex/Messaging/Eth/EIP712.hs index b09e6ac4e..a2ba2a81e 100644 --- a/src/Simplex/Messaging/Eth/EIP712.hs +++ b/src/Simplex/Messaging/Eth/EIP712.hs @@ -19,7 +19,7 @@ -- > "TransferName(address from,address to,uint256 tokenId,uint256 nonce,uint256 deadline)" module Simplex.Messaging.Eth.EIP712 ( Eip712Domain (..), - Value (..), + Eip712Value (..), typeHash, encodeValue, encodeData, @@ -50,7 +50,7 @@ data Eip712Domain = Eip712Domain -- -- 'VStruct' takes an already-computed 'hashStruct' result, which is how nested -- structs are encoded; 'VArray' hashes the concatenation of its members. -data Value +data Eip712Value = VUint Integer | VInt Integer | VBool Bool @@ -61,7 +61,7 @@ data Value VBytes ByteString | -- | @string@; the caller supplies UTF-8 bytes. VString ByteString - | VArray [Value] + | VArray [Eip712Value] | -- | A nested struct, given as its 32-byte @hashStruct@. VStruct ByteString deriving (Eq, Show) @@ -71,7 +71,7 @@ typeHash :: ByteString -> ByteString typeHash = keccak256 -- | Encode one member as exactly 32 bytes. -encodeValue :: Value -> Either String ByteString +encodeValue :: Eip712Value -> Either String ByteString encodeValue = \case VUint n | n < 0 || n >= two256 -> Left $ "eip712: uint256 out of range: " <> show n @@ -94,11 +94,11 @@ encodeValue = \case two256 = 2 ^ (256 :: Int) :: Integer two255 = 2 ^ (255 :: Int) :: Integer -encodeData :: [Value] -> Either String ByteString +encodeData :: [Eip712Value] -> Either String ByteString encodeData vs = B.concat <$> traverse encodeValue vs -- | @keccak256(typeHash ‖ encodeData(members))@. -hashStruct :: ByteString -> [Value] -> Either String ByteString +hashStruct :: ByteString -> [Eip712Value] -> Either String ByteString hashStruct typeString members = keccak256 . (typeHash typeString <>) <$> encodeData members domainSeparator :: Eip712Domain -> Either String ByteString @@ -112,7 +112,7 @@ domainSeparator d = ] -- | The final digest to sign: @keccak256(0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct)@. -hashTypedData :: Eip712Domain -> ByteString -> [Value] -> Either String ByteString +hashTypedData :: Eip712Domain -> ByteString -> [Eip712Value] -> Either String ByteString hashTypedData d typeString members = do ds <- domainSeparator d hs <- hashStruct typeString members diff --git a/src/Simplex/Messaging/Eth/Stealth.hs b/src/Simplex/Messaging/Eth/Stealth.hs index 5e896803d..daf0dc216 100644 --- a/src/Simplex/Messaging/Eth/Stealth.hs +++ b/src/Simplex/Messaging/Eth/Stealth.hs @@ -52,8 +52,8 @@ import qualified Simplex.Messaging.Crypto.Secp256k1 as S -- | A recipient's published key pair: spending key, then viewing key. data StealthMetaAddress = StealthMetaAddress - { smaSpend :: S.PublicKey, - smaView :: S.PublicKey + { smaSpend :: S.Secp256k1PublicKey, + smaView :: S.Secp256k1PublicKey } deriving (Eq, Show) @@ -74,9 +74,9 @@ data StealthDestination = StealthDestination } deriving (Eq, Show) -metaAddress :: S.PrivateKey -> S.PrivateKey -> StealthMetaAddress +metaAddress :: S.Secp256k1PrivateKey -> S.Secp256k1PrivateKey -> StealthMetaAddress metaAddress spend view = - StealthMetaAddress {smaSpend = S.publicKey spend, smaView = S.publicKey view} + StealthMetaAddress {smaSpend = S.secp256k1PublicKey spend, smaView = S.secp256k1PublicKey view} metaAddressSize :: Int metaAddressSize = 2 * S.compressedSize @@ -97,7 +97,7 @@ parseMetaAddress bs -- | @keccak256(x || y)@ of @sk * P@ — the value both sides arrive at, the -- sender from the ephemeral key and the recipient from the viewing key. -sharedSecretHash :: S.PrivateKey -> S.PublicKey -> Either String ByteString +sharedSecretHash :: S.Secp256k1PrivateKey -> S.Secp256k1PublicKey -> Either String ByteString sharedSecretHash sk pk = case S.publicKeyTweakMul pk (S.unPrivateKey sk) of Nothing -> Left "stealth: shared secret is not a valid point" @@ -106,14 +106,14 @@ sharedSecretHash sk pk = -- | Sender side. @ephemeral@ must be freshly random and used once: reusing it -- across recipients lets them link the destinations, and reusing it for one -- recipient produces the same address twice. -stealthDestination :: S.PrivateKey -> StealthMetaAddress -> Either String StealthDestination +stealthDestination :: S.Secp256k1PrivateKey -> StealthMetaAddress -> Either String StealthDestination stealthDestination ephemeral ma = do sh <- sharedSecretHash ephemeral (smaView ma) stealthPub <- tweakSpend (smaSpend ma) sh pure StealthDestination { sdAddress = addressFromPublicKey stealthPub, - sdEphemeralPubKey = S.serializePublicKey S.Compressed (S.publicKey ephemeral), + sdEphemeralPubKey = S.serializePublicKey S.Compressed (S.secp256k1PublicKey ephemeral), sdViewTag = B.head sh } @@ -121,7 +121,7 @@ stealthDestination ephemeral ma = do -- -- The view tag is checked before the point addition, which is the whole reason -- it exists — a non-match costs one multiplication and one hash. -stealthMatch :: S.PrivateKey -> S.PublicKey -> ByteString -> ViewTag -> Either String (Maybe Address) +stealthMatch :: S.Secp256k1PrivateKey -> S.Secp256k1PublicKey -> ByteString -> ViewTag -> Either String (Maybe Address) stealthMatch view spend ephemeralPub tag = do eph <- S.parsePublicKey ephemeralPub sh <- sharedSecretHash view eph @@ -133,7 +133,7 @@ stealthMatch view spend ephemeralPub tag = do -- -- Needs the spending key, which is why a viewing key can be delegated for -- scanning without granting the ability to spend. -stealthPrivateKey :: S.PrivateKey -> S.PrivateKey -> ByteString -> Either String S.PrivateKey +stealthPrivateKey :: S.Secp256k1PrivateKey -> S.Secp256k1PrivateKey -> ByteString -> Either String S.Secp256k1PrivateKey stealthPrivateKey spend view ephemeralPub = do eph <- S.parsePublicKey ephemeralPub sh <- sharedSecretHash view eph @@ -141,7 +141,7 @@ stealthPrivateKey spend view ephemeralPub = do Nothing -> Left "stealth: derived key out of range" Just sk -> Right sk -tweakSpend :: S.PublicKey -> ByteString -> Either String S.PublicKey +tweakSpend :: S.Secp256k1PublicKey -> ByteString -> Either String S.Secp256k1PublicKey tweakSpend spend sh = case S.publicKeyTweakAdd spend sh of Nothing -> Left "stealth: derived point out of range" Just p -> Right p diff --git a/tests/CoreTests/EthCryptoTests.hs b/tests/CoreTests/EthCryptoTests.hs index aaa3aeaa1..b146666b1 100644 --- a/tests/CoreTests/EthCryptoTests.hs +++ b/tests/CoreTests/EthCryptoTests.hs @@ -76,31 +76,29 @@ secp256k1Tests = do it "produces low-s signatures (EIP-2)" $ S.isLowS testSig `shouldBe` True it "recovers the signing key" $ - right (S.recoverPublicKey testSig testDigest) `shouldBe` S.publicKey testKey + right (S.recoverPublicKey testSig testDigest) `shouldBe` S.secp256k1PublicKey testKey it "does not recover the signing key from another digest" $ - S.recoverPublicKey testSig (keccak256 "SimpleX names ") `shouldNotBe` Right (S.publicKey testKey) + S.recoverPublicKey testSig (keccak256 "SimpleX names ") `shouldNotBe` Right (S.secp256k1PublicKey testKey) it "round-trips a compressed public key" $ do - let pk = S.publicKey testKey + let pk = S.secp256k1PublicKey testKey ser = S.serializePublicKey S.Compressed pk B.length ser `shouldBe` 33 S.parsePublicKey ser `shouldBe` Right pk it "round-trips an uncompressed public key" $ do - let pk = S.publicKey testKey + let pk = S.secp256k1PublicKey testKey ser = S.serializePublicKey S.Uncompressed pk B.length ser `shouldBe` 65 S.parsePublicKey ser `shouldBe` Right pk it "rejects a zero private key" $ - S.mkPrivateKey (B.replicate 32 0) `shouldSatisfy` isLeft + isLeft (S.mkPrivateKey (B.replicate 32 0)) `shouldBe` True it "rejects a private key at the group order" $ - S.mkPrivateKey (hx "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141") `shouldSatisfy` isLeft + isLeft (S.mkPrivateKey (hx "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")) `shouldBe` True it "rejects a short private key" $ - S.mkPrivateKey (B.replicate 31 1) `shouldSatisfy` isLeft + isLeft (S.mkPrivateKey (B.replicate 31 1)) `shouldBe` True it "rejects a digest that is not 32 bytes" $ S.signRecoverable testKey (B.replicate 31 0) `shouldSatisfy` isLeft it "rejects a malformed public key" $ S.parsePublicKey (B.replicate 33 0) `shouldSatisfy` isLeft - it "redacts the private key in Show" $ - show testKey `shouldBe` "PrivateKey " it "adds a tweak to a private key" $ (toHex . S.unPrivateKey <$> S.privateKeyTweakAdd testKey (B.replicate 31 0 <> B.singleton 1)) `shouldBe` Just "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362319" @@ -506,7 +504,7 @@ stealthTests = do it "rejects points not on the curve" $ parseMetaAddress (B.replicate 66 0xAA) `shouldSatisfy` isLeft -aliceSpend, aliceView, bobSpend, bobView, ephemeralKey, ephemeralKey2 :: S.PrivateKey +aliceSpend, aliceView, bobSpend, bobView, ephemeralKey, ephemeralKey2 :: S.Secp256k1PrivateKey aliceSpend = right $ S.mkPrivateKey (hx "1111111111111111111111111111111111111111111111111111111111111111") aliceView = right $ S.mkPrivateKey (hx "2222222222222222222222222222222222222222222222222222222222222222") bobSpend = right $ S.mkPrivateKey (hx "3333333333333333333333333333333333333333333333333333333333333333") @@ -519,5 +517,5 @@ aliceMeta = metaAddress aliceSpend aliceView bobMeta = metaAddress bobSpend bobView -- Distinct ephemeral keys for batch tests. -ephemeralN :: Int -> S.PrivateKey +ephemeralN :: Int -> S.Secp256k1PrivateKey ephemeralN i = right . S.mkPrivateKey . keccak256 . BC.pack $ "ephemeral " <> show i