cardano-crypto-1.3.0: src/Crypto/Encoding/BIP39.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Crypto.Encoding.BIP39
( -- * Entropy
Entropy
, ValidEntropySize
, Checksum
, ValidChecksumSize
, MnemonicWords
, EntropySize
, toEntropy
, entropyRaw
, entropyChecksum
, entropyToWords
, wordsToEntropy
, -- * Seed
Seed
, Passphrase
, sentenceToSeed
, phraseToSeed
, -- * Mnemonic Sentence
MnemonicSentence
, MnemonicPhrase
, ValidMnemonicSentence
, mnemonicPhrase
, checkMnemonicPhrase
, mnemonicPhraseToMnemonicSentence
, mnemonicSentenceToMnemonicPhrase
, mnemonicSentenceToString
, mnemonicSentenceToListN
, mnemonicPhraseToString
, translateTo
, -- ** Dictionary
Dictionary(..)
, WordIndex
, wordIndex
, unWordIndex
, -- * helpers
ConsistentEntropy
, CheckSumBits
, Elem
, -- * Errors
DictionaryError(..)
, EntropyError(..)
, MnemonicWordsError(..)
) where
import Prelude ((-), (*), (+), div, divMod, (^), fromIntegral)
import qualified Basement.String as String
import Basement.Nat
import qualified Basement.Sized.List as ListN
import Basement.Sized.List (ListN)
import Basement.NormalForm
import Basement.Compat.Typeable
import Basement.Numerical.Number (IsIntegral(..))
import Basement.Imports
import Foundation.Check
import Control.Monad (replicateM, (<=<))
import Data.Bits
import Data.Maybe (fromMaybe)
import Data.List (reverse, intersperse, length)
import Data.Kind (Constraint)
import Data.ByteArray (ByteArrayAccess, ByteArray)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Proxy
import GHC.TypeLits
import Crypto.Hash (hashWith, SHA256(..))
import Crypto.Number.Serialize (os2ip, i2ospOf_)
import qualified Crypto.KDF.PBKDF2 as PBKDF2
import Crypto.Encoding.BIP39.Dictionary
import Cardano.Internal.Compat (fromRight)
-- -------------------------------------------------------------------------- --
-- Entropy
-- -------------------------------------------------------------------------- --
-- | this is the `Checksum` of a given 'Entropy'
--
-- the 'Nat' type parameter represent the size, in bits, of this checksum.
newtype Checksum (bits :: Nat) = Checksum Word8
deriving (Show, Eq, Typeable, NormalForm)
checksum :: forall csz ba . (KnownNat csz, ByteArrayAccess ba)
=> ba -> Checksum csz
checksum bs = Checksum $ (hashWith SHA256 bs `BA.index` 0) `shiftR` (8 - csz)
where
csz = fromInteger $ natVal (Proxy @csz)
type ValidChecksumSize (ent :: Nat) (csz :: Nat) =
( KnownNat csz, NatWithinBound Int csz
, Elem csz '[3, 4, 5, 6, 7, 8]
, CheckSumBits ent ~ csz
)
-- | Number of bits of checksum related to a specific entropy size in bits
type family CheckSumBits (n :: Nat) :: Nat where
CheckSumBits 96 = 3
CheckSumBits 128 = 4
CheckSumBits 160 = 5
CheckSumBits 192 = 6
CheckSumBits 224 = 7
CheckSumBits 256 = 8
-- | BIP39's entropy is a byte array of a given size (in bits, see
-- 'ValidEntropySize' for the valid size).
--
-- To it is associated
data Entropy (n :: Nat) = Entropy
{ entropyRaw :: !ByteString
-- ^ Get the raw binary associated with the entropy
, entropyChecksum :: !(Checksum (CheckSumBits n))
-- ^ Get the checksum of the Entropy
}
deriving (Show, Eq, Typeable)
instance NormalForm (Entropy n) where
toNormalForm (Entropy !_ cs) = toNormalForm cs
instance Arbitrary (Entropy 96) where
arbitrary = fromRight (error "arbitrary (Entropy 96)") . toEntropy . BS.pack <$> replicateM 12 arbitrary
instance Arbitrary (Entropy 128) where
arbitrary = fromRight (error "arbitrary (Entropy 128)") . toEntropy . BS.pack <$> replicateM 16 arbitrary
instance Arbitrary (Entropy 160) where
arbitrary = fromRight (error "arbitrary (Entropy 160)") . toEntropy . BS.pack <$> replicateM 20 arbitrary
instance Arbitrary (Entropy 192) where
arbitrary = fromRight (error "arbitrary (Entropy 192)") . toEntropy . BS.pack <$> replicateM 24 arbitrary
instance Arbitrary (Entropy 224) where
arbitrary = fromRight (error "arbitrary (Entropy 224)") . toEntropy . BS.pack <$> replicateM 28 arbitrary
instance Arbitrary (Entropy 256) where
arbitrary = fromRight (error "arbitrary (Entropy 256)") . toEntropy . BS.pack <$> replicateM 32 arbitrary
-- | Type Constraint Alias to check a given 'Nat' is valid for an entropy size
--
-- i.e. it must be one of the following: 96, 128, 160, 192, 224, 256.
--
type ValidEntropySize (n :: Nat) =
( KnownNat n, NatWithinBound Int n
, Elem n '[96, 128, 160, 192, 224, 256]
)
-- | Create a specific entropy type of known size from a raw bytestring
toEntropy :: forall n csz ba
. (ValidEntropySize n, ValidChecksumSize n csz, ByteArrayAccess ba)
=> ba
-> Either (EntropyError csz) (Entropy n)
toEntropy bs
| actual == expected = Right $ Entropy (BA.convert bs) (checksum @csz bs)
| otherwise = Left $ ErrInvalidEntropyLength actual expected
where
actual = BA.length bs*8
expected = natValInt (Proxy @n)
toEntropyCheck :: forall n csz ba
. (ValidEntropySize n, ValidChecksumSize n csz, ByteArrayAccess ba)
=> ba
-> Checksum csz
-> Either (EntropyError csz) (Entropy n)
toEntropyCheck bs s = case toEntropy bs of
Left err -> Left err
Right e@(Entropy _ cs) | cs == s -> Right e
| otherwise -> Left $ ErrInvalidEntropyChecksum cs s
-- | Number of Words related to a specific entropy size in bits
type family MnemonicWords (n :: Nat) :: Nat where
MnemonicWords 96 = 9
MnemonicWords 128 = 12
MnemonicWords 160 = 15
MnemonicWords 192 = 18
MnemonicWords 224 = 21
MnemonicWords 256 = 24
-- | Corresponding entropy size in bits for a given number of words
type family EntropySize (n :: Nat) :: Nat where
EntropySize 9 = 96
EntropySize 12 = 128
EntropySize 15 = 160
EntropySize 18 = 192
EntropySize 21 = 224
EntropySize 24 = 256
-- | Type Constraint Alias to check the entropy size, the number of mnemonic
-- words and the checksum size is consistent. i.e. that the following is true:
--
-- | entropysize | checksumsize | entropysize + checksumsize | mnemonicsize |
-- +---------------+--------------+----------------------------+--------------+
-- | 96 | 3 | 99 | 9 |
-- | 128 | 4 | 132 | 12 |
-- | 160 | 5 | 165 | 15 |
-- | 192 | 6 | 198 | 18 |
-- | 224 | 7 | 231 | 21 |
-- | 256 | 8 | 264 | 24 |
--
-- This type constraint alias also perform all the GHC's cumbersome type level
-- literal handling.
--
type ConsistentEntropy ent mw csz =
( ValidEntropySize ent
, ValidChecksumSize ent csz
, ValidMnemonicSentence mw
, MnemonicWords ent ~ mw
)
-- | retrieve the initial entropy from a given 'MnemonicSentence'
--
-- This function validate the retrieved 'Entropy' is valid, i.e. that the
-- checksum is correct.
-- This means you should not create a new 'Entropy' from a 'MnemonicSentence',
-- instead, you should use a Random Number Generator to create a new 'Entropy'.
--
wordsToEntropy :: forall ent csz mw
. ConsistentEntropy ent mw csz
=> MnemonicSentence mw
-> Either (EntropyError csz) (Entropy ent)
wordsToEntropy (MnemonicSentence ms) =
let -- we don't revese the list here, we know that the first word index
-- is the highest first 11 bits of the entropy.
entropy = ListN.foldl' (\acc x -> acc `shiftL` 11 + toInteger (unWordIndex x)) 0 ms
initialEntropy :: ByteString
initialEntropy = i2ospOf_ nb (entropy `shiftR` fromInteger checksumsize)
cs = Checksum $ fromInteger (entropy .&. mask)
in toEntropyCheck initialEntropy cs
where
checksumsize = natVal (Proxy @csz)
entropysize = natVal (Proxy @ent)
nb = fromInteger entropysize `div` 8
mask = 2 ^ checksumsize - 1
-- | Given an entropy of size n, Create a list
--
entropyToWords :: forall n csz mw . ConsistentEntropy n mw csz
=> Entropy n
-> MnemonicSentence mw
entropyToWords (Entropy bs (Checksum w)) =
fromList $ reverse $ loop mw g
where
g = (os2ip bs `shiftL` fromIntegral csz) .|. fromIntegral w
csz = natVal (Proxy @csz)
mw = natVal (Proxy @mw)
loop nbWords acc
| nbWords == 0 = []
| otherwise =
let (acc', d) = acc `divMod` 2048
in wordIndex (fromIntegral d) : loop (nbWords - 1) acc'
-- -------------------------------------------------------------------------- --
-- Seed
-- -------------------------------------------------------------------------- --
newtype Seed = Seed ByteString
deriving (Show, Eq, Ord, Typeable, Semigroup, Monoid, ByteArrayAccess, ByteArray, IsString)
type Passphrase = String
-- | Create a seed from 'MmemonicSentence' and 'Passphrase' using the BIP39
-- algorithm.
sentenceToSeed :: ValidMnemonicSentence mw
=> MnemonicSentence mw -- ^ 'MmenomicPhrase' of mw words
-> Dictionary -- ^ Dictionary' of words/indexes
-> Passphrase -- ^ 'Passphrase' used to generate
-> Seed
sentenceToSeed mw dic =
phraseToSeed (mnemonicSentenceToMnemonicPhrase dic mw) dic
-- | Create a seed from 'MmemonicPhrase' and 'Passphrase' using the BIP39
-- algorithm.
phraseToSeed :: ValidMnemonicSentence mw
=> MnemonicPhrase mw -- ^ 'MmenomicPhrase' of mw words
-> Dictionary -- ^ Dictionary' of words/indexes
-> Passphrase -- ^ 'Passphrase' used to generate
-> Seed
phraseToSeed mw dic passphrase =
PBKDF2.fastPBKDF2_SHA512
(PBKDF2.Parameters 2048 64)
sentence
(toData ("mnemonic" `mappend` passphrase))
where
sentence = toData $ mnemonicPhraseToString dic mw
toData = String.toBytes String.UTF8
-- -------------------------------------------------------------------------- --
-- Mnemonic Sentence and Mnemonic Phrase
-- -------------------------------------------------------------------------- --
-- | Mnemonic Sentence is a list of 'WordIndex'.
--
-- This is the generic representation of a mnemonic phrase that can be used for
-- transalating to a different dictionary (example: English to Japanese).
--
-- This is mainly used to convert from/to the 'Entropy' and for 'cardanoSlSeed'
--
newtype MnemonicSentence (mw :: Nat) = MnemonicSentence
{ mnemonicSentenceToListN :: ListN mw WordIndex
}
deriving (Show, Eq, Ord, Typeable, NormalForm)
instance ValidMnemonicSentence mw => IsList (MnemonicSentence mw) where
type Item (MnemonicSentence mw) = WordIndex
fromList = MnemonicSentence . fromMaybe (error "invalid mnemonic size") . ListN.toListN
toList = ListN.unListN . mnemonicSentenceToListN
-- | Type Constraint to validate the given 'Nat' is valid for the supported
-- 'MnemonicSentence'
type ValidMnemonicSentence (mw :: Nat) =
( KnownNat mw
, NatWithinBound Int mw
, Elem mw '[9, 12, 15, 18, 21, 24]
)
-- | Human readable representation of a 'MnemonicSentence'
--
newtype MnemonicPhrase (mw :: Nat) = MnemonicPhrase
{ mnemonicPhraseToListN :: ListN mw String
}
deriving (Show, Eq, Ord, Typeable, NormalForm)
instance ValidMnemonicSentence mw => IsList (MnemonicPhrase mw) where
type Item (MnemonicPhrase mw) = String
fromList = fromRight (error "invalid mnemonic phrase") . mnemonicPhrase
toList = ListN.unListN . mnemonicPhraseToListN
mnemonicPhrase :: forall mw . ValidMnemonicSentence mw => [String] -> Either MnemonicWordsError (MnemonicPhrase mw)
mnemonicPhrase l = MnemonicPhrase <$> maybe
(Left $ ErrWrongNumberOfWords (length l) (natValInt @mw Proxy))
Right
(ListN.toListN l)
{-# INLINABLE mnemonicPhrase #-}
-- | check a given 'MnemonicPhrase' is valid for the given 'Dictionary'
--
checkMnemonicPhrase :: forall mw . ValidMnemonicSentence mw
=> Dictionary
-> MnemonicPhrase mw
-> Bool
checkMnemonicPhrase dic (MnemonicPhrase ln) =
ListN.foldl' (\acc s -> (dictionaryTestWord dic s && acc)) True ln
-- | convert the given 'MnemonicPhrase' to a generic 'MnemonicSentence'
-- with the given 'Dictionary'.
--
-- This function assumes the 'Dictionary' and the 'MnemonicPhrase' are
-- compatible (see 'checkMnemonicPhrase').
--
mnemonicPhraseToMnemonicSentence :: forall mw . ValidMnemonicSentence mw
=> Dictionary
-> MnemonicPhrase mw
-> Either DictionaryError (MnemonicSentence mw)
mnemonicPhraseToMnemonicSentence dic (MnemonicPhrase ln) = MnemonicSentence <$>
ListN.mapM (dictionaryWordToIndex dic) ln
-- | convert the given generic 'MnemonicSentence' to a human readable
-- 'MnemonicPhrase' targetting the language of the given 'Dictionary'.
mnemonicSentenceToMnemonicPhrase :: forall mw . ValidMnemonicSentence mw
=> Dictionary
-> MnemonicSentence mw
-> MnemonicPhrase mw
mnemonicSentenceToMnemonicPhrase dic (MnemonicSentence ln) = MnemonicPhrase $
ListN.map (dictionaryIndexToWord dic) ln
mnemonicPhraseToString :: forall mw . ValidMnemonicSentence mw
=> Dictionary
-> MnemonicPhrase mw
-> String
mnemonicPhraseToString dic (MnemonicPhrase ln) = mconcat $
intersperse (dictionaryWordSeparator dic) (ListN.unListN ln)
mnemonicSentenceToString :: forall mw . ValidMnemonicSentence mw
=> Dictionary
-> MnemonicSentence mw
-> String
mnemonicSentenceToString dic = mnemonicPhraseToString dic
. mnemonicSentenceToMnemonicPhrase dic
-- | translate the given 'MnemonicPhrase' from one dictionary into another.
--
-- This function assumes the source dictionary is compatible with the given
-- 'MnemonicPhrase' (see 'checkMnemonicPhrase')
--
translateTo :: forall mw . ValidMnemonicSentence mw
=> Dictionary -- ^ source dictionary
-> Dictionary -- ^ destination dictionary
-> MnemonicPhrase mw
-> Either DictionaryError (MnemonicPhrase mw)
translateTo dicSrc dicDst (MnemonicPhrase ln) = MnemonicPhrase <$>
ListN.mapM (return . dictionaryIndexToWord dicDst <=< dictionaryWordToIndex dicSrc) ln
------------------------------------------------------------------------
-- Helpers
------------------------------------------------------------------------
-- | convenient type level constraint to validate a given 'Nat' e is an elemnt
-- of the list of 'Nat' l.
type family Elem (e :: Nat) (l :: [Nat]) :: Constraint where
Elem e '[] = TypeError ('Text "offset: field "
':<>: 'ShowType e
':<>: 'Text " not elements of valids values")
Elem e (e ': _) = ()
Elem e (_ ': xs) = Elem e xs
-- -------------------------------------------------------------------------- --
-- Errors
-- -------------------------------------------------------------------------- --
data EntropyError csz
= ErrInvalidEntropyLength
Int -- Actual length in bits
Int -- Expected length in bits
| ErrInvalidEntropyChecksum
(Checksum csz) -- Actual checksum
(Checksum csz) -- Expected checksum
deriving (Show)
data MnemonicWordsError
= ErrWrongNumberOfWords
Int -- Actual number of words
Int -- Expected number of words
deriving (Show)