ppad-bip39 0.2.1 → 0.3.0
raw patch · 4 files changed
+53/−59 lines, 4 filesdep ~ppad-pbkdfPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: ppad-pbkdf
API changes (from Hackage documentation)
- Crypto.KDF.BIP39: _mnemonic :: Wordlist -> ByteString -> Text
+ Crypto.KDF.BIP39: _mnemonic :: Wordlist -> ByteString -> Maybe Text
- Crypto.KDF.BIP39: _seed :: Wordlist -> Text -> Text -> ByteString
+ Crypto.KDF.BIP39: _seed :: Wordlist -> Text -> Text -> Maybe ByteString
- Crypto.KDF.BIP39: mnemonic :: ByteString -> Text
+ Crypto.KDF.BIP39: mnemonic :: ByteString -> Maybe Text
- Crypto.KDF.BIP39: seed :: Text -> Text -> ByteString
+ Crypto.KDF.BIP39: seed :: Text -> Text -> Maybe ByteString
- Crypto.KDF.BIP39: seed_unsafe :: Text -> Text -> ByteString
+ Crypto.KDF.BIP39: seed_unsafe :: Text -> Text -> Maybe ByteString
Files
- CHANGELOG +4/−0
- lib/Crypto/KDF/BIP39.hs +38/−45
- ppad-bip39.cabal +2/−2
- test/Main.hs +9/−12
CHANGELOG view
@@ -1,5 +1,9 @@ # Changelog +- 0.3.0 (2025-06-21)+ * The 'mnemonic' and 'seed' function families are now total, returning+ 'Nothing' when supplied with bad inputs.+ - 0.2.1 (2025-03-02) * Various documentation improvements.
lib/Crypto/KDF/BIP39.hs view
@@ -3,7 +3,7 @@ {-# LANGUAGE OverloadedStrings #-} -- |--- Module: Crypto.HDKey.BIP39+-- Module: Crypto.KDF.BIP39 -- Copyright: (c) 2025 Jared Tobin -- License: MIT -- Maintainer: Jared Tobin <jared@ppad.tech>@@ -40,6 +40,7 @@ , spanish ) where +import Control.Monad (guard) import qualified Crypto.KDF.PBKDF as PBKDF import qualified Crypto.Hash.SHA256 as SHA256 import qualified Crypto.Hash.SHA512 as SHA512@@ -69,43 +70,40 @@ -- wordlist. -- -- The entropy must be at least 128 bits long and at most 256 bits--- long. Providing invalid entropy will result in an 'ErrorCall'--- exception.+-- long. Providing invalid entropy will result in a 'Nothing' value. -- -- >>> import qualified System.Entropy as E -- >>> trop <- E.getEntropy 16 -- >>> mnemonic trop--- "coral maze mimic half fat breeze thought club give brass bone snake"+-- Just "coral maze mimic half fat breeze thought club give brass bone snake" mnemonic :: BS.ByteString -- ^ 128-256 bits of entropy- -> T.Text+ -> Maybe T.Text mnemonic = _mnemonic english -- | Generate a BIP39 mnemonic from some entropy, using the provided -- wordlist. -- -- The entropy must be at least 128 bits long and at most 256 bits--- long. Providing invalid entropy will result in an 'ErrorCall'--- exception.+-- long. Providing invalid entropy will result in a value of+-- 'Nothing'. -- -- >>> import qualified System.Entropy as E -- >>> trop <- E.getEntropy 16 -- >>> _mnemonic czech trop--- "naslepo lysina dikobraz slupka beseda rorejs ostraha kobliha napevno blahobyt kazivost jiskra"+-- Just "naslepo lysina dikobraz slupka beseda rorejs ostraha kobliha napevno blahobyt kazivost jiskra" _mnemonic :: Wordlist -> BS.ByteString -- ^ 128-256 bits of entropy- -> T.Text-_mnemonic (Wordlist wlist) entropy@(BI.PS _ _ l)- | l < 16 = error "ppad-bip39 (mnemonic): invalid entropy length"- | l > 32 = error "ppad-bip39 (mnemonic): invalid entropy length"- | otherwise =- let has = SHA256.hash entropy- h = BU.unsafeHead has- n = l `quot` 4- kek = h .&. (0b1111_1111 .<<. (8 - n)) -- top n bits- cat = entropy <> BS.singleton kek- in T.intercalate " " (words wlist cat)+ -> Maybe T.Text+_mnemonic (Wordlist wlist) entropy@(BI.PS _ _ l) = do+ guard (l >= 16 && l <= 32)+ let has = SHA256.hash entropy+ h = BU.unsafeHead has+ n = l `quot` 4+ kek = h .&. (0b1111_1111 .<<. (8 - n)) -- top n bits+ cat = entropy <> BS.singleton kek+ pure $! T.intercalate " " (words wlist cat) {-# INLINE _mnemonic #-} -- remaining, bits pool, number of bits in pool@@ -121,7 +119,7 @@ nacc = acc .&. ((1 .<<. (len - 11)) - 1) -- adjust pool nlen = len - 11 -- track less bits word = PA.indexArray wlist w11- in Just (word, (etc, nacc, nlen))+ in pure (word, (etc, nacc, nlen)) | not (BS.null etc) = let next = BU.unsafeHead etc rest = BU.unsafeTail etc@@ -142,36 +140,33 @@ -- >>> let mnem = "coral maze mimic half fat breeze thought club give brass bone snake" -- >> let pass = "hunter2" -- >>> seed mnem pass--- <512-bit long seed>+-- Just <512-bit long seed> seed :: T.Text -- ^ mnemonic -> T.Text -- ^ passphrase (use e.g. "" or 'mempty' if not required)- -> BS.ByteString -- ^ seed+ -> Maybe BS.ByteString -- ^ seed seed = _seed english -- | Derive a master seed from a provided mnemonic and passphrase, where the -- mnemonic has been generated from an arbitrary wordlist. -- -- The provided mnemonic is checked for validity using '_valid'.--- Providing an invalid mnemonic will result in an 'ErrorCall'--- exception.+-- Providing an invalid mnemonic will result in a 'Nothing' value. -- -- >>> let mnem = "coral maze mimic half fat breeze thought club give brass bone snake" -- >> let pass = "hunter2" -- >>> _seed english mnem pass--- <512-bit long seed>+-- Just <512-bit long seed> _seed- :: Wordlist -- ^ wordlist- -> T.Text -- ^ mnemonic- -> T.Text -- ^ passphrase (use e.g. "" or 'mempty' if not required)- -> BS.ByteString -- ^ seed-_seed wlist mnem pass- | not (_valid wlist mnem) =- error "ppad-bip39 (seed): invalid mnemonic"- | otherwise =- let salt = TE.encodeUtf8 ("mnemonic" <> ICU.nfkd pass)- norm = TE.encodeUtf8 (ICU.nfkd mnem)- in PBKDF.derive SHA512.hmac norm salt 2048 64 where+ :: Wordlist -- ^ wordlist+ -> T.Text -- ^ mnemonic+ -> T.Text -- ^ passphrase (use e.g. "" or 'mempty' if not required)+ -> Maybe BS.ByteString -- ^ seed+_seed wlist mnem pass = do+ guard (_valid wlist mnem)+ let salt = TE.encodeUtf8 ("mnemonic" <> ICU.nfkd pass)+ norm = TE.encodeUtf8 (ICU.nfkd mnem)+ PBKDF.derive SHA512.hmac norm salt 2048 64 {-# INLINE _seed #-} -- | Derive a master seed from a provided mnemonic and passphrase.@@ -182,18 +177,16 @@ -- >>> let mnem = "coral maze mimic half fat breeze thought club give brass bone snake" -- >> let pass = "hunter2" -- >>> seed_unsafe mnem pass--- <512-bit long seed>+-- Just <512-bit long seed> seed_unsafe :: T.Text -- ^ mnemonic -> T.Text -- ^ passphrase (use e.g. "" or 'mempty' if not required)- -> BS.ByteString -- ^ seed-seed_unsafe mnem pass- | length (T.words mnem) `notElem` [12, 15, 18, 21, 24] =- error "ppad-bip39 (seed_unsafe): invalid mnemonic"- | otherwise =- let salt = TE.encodeUtf8 ("mnemonic" <> ICU.nfkd pass)- norm = TE.encodeUtf8 (ICU.nfkd mnem)- in PBKDF.derive SHA512.hmac norm salt 2048 64 where+ -> Maybe BS.ByteString -- ^ seed+seed_unsafe mnem pass = do+ guard (length (T.words mnem) `elem` [12, 15, 18, 21, 24])+ let salt = TE.encodeUtf8 ("mnemonic" <> ICU.nfkd pass)+ norm = TE.encodeUtf8 (ICU.nfkd mnem)+ PBKDF.derive SHA512.hmac norm salt 2048 64 -- | Validate a mnemonic against the default English wordlist. --
ppad-bip39.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: ppad-bip39-version: 0.2.1+version: 0.3.0 synopsis: BIP39 mnemonic codes. license: MIT license-file: LICENSE@@ -40,7 +40,7 @@ build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13- , ppad-pbkdf >= 0.1 && < 0.2+ , ppad-pbkdf >= 0.2 && < 0.3 , ppad-sha256 >= 0.2.3 && < 0.3 , ppad-sha512 >= 0.1.3 && < 0.2 , primitive >= 0.8 && < 0.10
test/Main.hs view
@@ -1,3 +1,4 @@+{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -75,12 +76,10 @@ mnem = bt_mnemonic seed = bt_seed xprv = bt_xprv- out_mnem = BIP39._mnemonic wl entr- giv_seed = seed_fn mnem "TREZOR"- out_seed = seed_fn out_mnem "TREZOR"- out_xprv = case BIP32.master out_seed of- Just hd -> BIP32.xprv hd- Nothing -> error "bang (bip32)"+ Just out_mnem = BIP39._mnemonic wl entr+ Just giv_seed = seed_fn mnem "TREZOR"+ Just out_seed = seed_fn out_mnem "TREZOR"+ Just out_xprv = BIP32.master out_seed >>= BIP32.xprv t_msg = mempty testGroup t_msg [ -- we always output (NFKD) normalized UTF8, but test inputs may not be@@ -115,12 +114,10 @@ pass = jp_passphrase seed = jp_seed xprv = jp_xprv- out_mnem = BIP39._mnemonic BIP39.japanese entr- giv_seed = BIP39.seed_unsafe mnem pass- out_seed = BIP39.seed_unsafe out_mnem pass- out_xprv = case BIP32.master out_seed of- Just hd -> BIP32.xprv hd- Nothing -> error "bang (bip32, jp)"+ Just out_mnem = BIP39._mnemonic BIP39.japanese entr+ Just giv_seed = BIP39.seed_unsafe mnem pass+ Just out_seed = BIP39.seed_unsafe out_mnem pass+ Just out_xprv = BIP32.master out_seed >>= BIP32.xprv testGroup mempty [ testCase "mnemonic" $ assertEqual mempty (ICU.nfkd mnem) out_mnem , testCase "seed (from given mnemonic)" $ assertEqual mempty seed giv_seed