bip39

BIP39 mnemonic codes in Haskell (docs.ppad.tech/bip39).
git clone git://git.ppad.tech/bip39.git
Log | Files | Refs | README | LICENSE

commit a75ff6f1fb1434418b2bec84f02f456a5f023c70
parent a9065f295c08db29e3dbc3aa0a1b9c55af8b5746
Author: Jared Tobin <jared@jtobin.io>
Date:   Wed, 11 Jun 2025 09:49:51 +0400

lib: make functions total

Diffstat:
MREADME.md | 2+-
Mlib/Crypto/KDF/BIP39.hs | 73+++++++++++++++++++++++++++++++++----------------------------------------
Mtest/Main.hs | 13+++++++------
3 files changed, 41 insertions(+), 47 deletions(-)

diff --git a/README.md b/README.md @@ -23,7 +23,7 @@ A sample GHCi session: > let trop = "my ultra secret entropy!" > > -- use 'mnemonic' to create a BIP39 mnemonic - > let mnem = mnemonic trop + > let Just mnem = mnemonic trop > mnem "hope simple bubble suggest elbow correct limb hole gloom nasty fringe dolphin finger demand situate unlock junior autumn" > diff --git a/lib/Crypto/KDF/BIP39.hs b/lib/Crypto/KDF/BIP39.hs @@ -40,6 +40,7 @@ module Crypto.KDF.BIP39 ( , 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,8 +70,7 @@ newtype Wordlist = Wordlist (PA.Array T.Text) -- 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 @@ -78,34 +78,32 @@ newtype Wordlist = Wordlist (PA.Array T.Text) -- "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 @@ words wlist bs = L.unfoldr coalg (bs, 0, 0) where 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 @@ -146,32 +144,29 @@ words wlist bs = L.unfoldr coalg (bs, 0, 0) where 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> _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) + pure $! PBKDF.derive SHA512.hmac norm salt 2048 64 where {-# INLINE _seed #-} -- | Derive a master seed from a provided mnemonic and passphrase. @@ -186,14 +181,12 @@ _seed wlist mnem pass 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) + pure $! PBKDF.derive SHA512.hmac norm salt 2048 64 where -- | Validate a mnemonic against the default English wordlist. -- diff --git a/test/Main.hs b/test/Main.hs @@ -6,6 +6,7 @@ module Main where import qualified Crypto.HDKey.BIP32 as BIP32 import qualified Crypto.KDF.BIP39 as BIP39 import qualified Data.Aeson as A +import qualified Data.Maybe as M import qualified Data.Text.ICU.Normalize2 as ICU import qualified Data.Text.IO as TIO import Test.Tasty @@ -75,9 +76,9 @@ execute wlist V.Bip39Test {..} = do 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_mnem = M.fromJust (BIP39._mnemonic wl entr) + giv_seed = M.fromJust (seed_fn mnem "TREZOR") + out_seed = M.fromJust (seed_fn out_mnem "TREZOR") out_xprv = case BIP32.master out_seed of Just hd -> BIP32.xprv hd Nothing -> error "bang (bip32)" @@ -115,9 +116,9 @@ execute_jp V.JPBip39Test {..} = do 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_mnem = M.fromJust (BIP39._mnemonic BIP39.japanese entr) + giv_seed = M.fromJust (BIP39.seed_unsafe mnem pass) + out_seed = M.fromJust (BIP39.seed_unsafe out_mnem pass) out_xprv = case BIP32.master out_seed of Just hd -> BIP32.xprv hd Nothing -> error "bang (bip32, jp)"