haskoin-wallet-0.9.4: src/Haskoin/Wallet/Entropy.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Haskoin.Wallet.Entropy where
import Control.Monad.IO.Class ( MonadIO, liftIO )
import Control.Monad (replicateM, unless, when)
import Control.Monad.Except
import Data.Bits (Bits (setBit, xor))
import qualified Data.ByteString as BS
import Data.List (foldl', nub)
import Data.Text (Text)
import Data.Word (Word8)
import Haskoin.Crypto (Mnemonic, toMnemonic, fromMnemonic)
import Haskoin.Util (bsToInteger)
import Haskoin.Wallet.Util (chunksOf)
import Numeric (showIntAtBase)
import Numeric.Natural (Natural)
import qualified System.Console.Haskeline as Haskeline
import qualified System.Console.Haskeline as Haskline
import qualified System.Directory as D
import System.Entropy (getEntropy)
import System.IO (IOMode (ReadMode), withBinaryFile)
import Text.Read (readMaybe)
-- Produces uniformily distributed dice rolls from a random byte
-- 0, 0x00 -> [6,6,6]
-- 1, 0x01 -> [6,6,1]
-- 215, 0xd7 -> [5,5,5]
-- 216, 0xd8 -> [6,6]
-- 217, 0xd9 -> [6,1]
-- 251, 0xfb -> [5,5]
-- Anything above 0xfb is dropped
word8ToBase6 :: Word8 -> [Natural]
word8ToBase6 w
| w > 0xfb = [] -- Drop otherwise we don't have uniform distribution
| w > 0xd7 =
let res = f <$> encodeBase6 (BS.pack [w - 0xd8])
in replicate (2 - length res) 6 <> res
| otherwise =
let res = f <$> encodeBase6 (BS.pack [w])
in replicate (3 - length res) 6 <> res
where
f :: Char -> Natural
f = read . (: [])
b6Data :: String
b6Data = "612345"
b6 :: Int -> Char
b6 = (b6Data !!)
encodeBase6 :: BS.ByteString -> String
encodeBase6 bs
| BS.null bs = mempty
| otherwise = showIntAtBase 6 b6 (bsToInteger bs) ""
-- Produces Bools using base6ToBool. Once 8 Bools have been
-- accumulated, it is transformed to a Word8 using boolsToWord8.
-- Any unused Bools are returned along with the Word8s.
base6ToWord8 :: [Natural] -> [Bool] -> ([Word8], [Bool])
base6ToWord8 ns obs
| length bs < 8 = ([], bs)
| otherwise =
let (ws, rest) = f bs []
in (boolsToWord8 <$> ws, rest)
where
bs = concatMap base6ToBool ns <> obs
f :: [Bool] -> [[Bool]] -> ([[Bool]], [Bool])
f xs acc
| length xs < 8 = (acc, xs)
| otherwise =
let (l, r) = splitAt (length xs - 8) xs
in f l ([r] <> acc)
-- Produce uniformily distributed bits from dice rolls
-- 6 -> [0,0]
-- 1 -> [0,1]
-- 2 -> [1,0]
-- 3 -> [1,1]
-- 4 -> [0]
-- 5 -> [1]
base6ToBool :: Natural -> [Bool]
base6ToBool =
\case
6 -> [False, False]
1 -> [False, True]
2 -> [True, False]
3 -> [True, True]
4 -> [False]
5 -> [True]
_ -> error "Invalid base6ToBool"
-- Big Endian
-- [T,F,F,F,F,F,F,F] -> 128
-- [F,F,F,F,F,F,F,T] -> 1
boolsToWord8 :: [Bool] -> Word8
boolsToWord8 xs
| length xs /= 8 = error "Invalid boolsToWord8"
| otherwise =
foldl' setBit 0 ys
where
ys = fmap fst $ filter snd $ zip [0 ..] $ reverse xs
-- Packs bools into a ByteString
packBools :: [Bool] -> BS.ByteString
packBools bs
| length bs `mod` 8 /= 0 = error "Invalid packBools length"
| otherwise = BS.pack $ boolsToWord8 <$> chunksOf 8 bs
-- Mix entropy of same length by xoring them
xorBytes :: BS.ByteString -> BS.ByteString -> BS.ByteString
xorBytes ent1 ent2
| BS.length ent1 == BS.length ent2 =
BS.pack $ BS.zipWith xor ent1 ent2
| otherwise = error "Entropy is not of the same length"
-- Split secret s into n parts
-- generate k1 .. k(n-1) randomly
-- compute kn = s xor k1 xor .. xor k(n-1)
-- return k1 .. kn
-- We have the property that s = k1 xor ... xor kn
splitEntropy :: Natural -> BS.ByteString -> IO [BS.ByteString]
splitEntropy n secret
| n < 2 = error "splitEntropy invalid number"
| otherwise = do
ks <- replicateM (fromIntegral n - 1) (snd <$> systemEntropy len)
return $ splitEntropyWith secret ks
where
len = fromIntegral $ BS.length secret
splitEntropyWith :: BS.ByteString -> [BS.ByteString] -> [BS.ByteString]
splitEntropyWith secret ks = foldl' xorBytes secret ks : ks
mergeMnemonicParts :: [Text] -> Either String Mnemonic
mergeMnemonicParts mnems
| length mnems < 2 = Left "Only one mnemonic provided"
| length (nub mnems) /= length mnems = Left "Two mnemonics are identical"
| otherwise = do
bs <- mapM fromMnemonic mnems
let ent = foldl' xorBytes (head bs) (tail bs)
toMnemonic ent
askInputDice :: IO Natural
askInputDice = do
inputM <-
Haskeline.runInputT Haskline.defaultSettings $
Haskeline.getInputLine "Dice roll: "
case readMaybe =<< inputM of
Just n ->
if n `elem` [1 .. 6]
then return n
else badInput
_ -> badInput
where
badInput = do
putStrLn "Invalid dice roll. Must be in range [1..6]"
askInputDice
-- TODO: Ask the dice rolls in sequences of 5 or so
getDiceEntropy :: Natural -> IO BS.ByteString
getDiceEntropy ent = do
putStrLn "Enter your dice rolls (from 1 to 6)"
f [] []
where
f :: [Word8] -> [Bool] -> IO BS.ByteString
f res acc
| length res == fromIntegral ent =
return $ BS.pack res
| otherwise = do
d <- askInputDice
let (ws, bs) = base6ToWord8 [d] acc
unless (null ws) $ do
putStrLn $
show (length res + length ws)
<> "/"
<> show ent
<> " bytes collected"
f (ws <> res) bs
-- Generate a mnemonic with optional dice entropy and key splitting
genMnemonic ::
(MonadIO m) =>
Natural ->
Bool ->
Natural ->
ExceptT String m (Text, Mnemonic, [Mnemonic])
genMnemonic reqEnt reqDice splitIn
| splitIn == 0 = throwError "Invalid split option"
| reqEnt `elem` [16, 20 .. 32] = do
(origEnt, sysEnt) <- liftIO $ systemEntropy reqEnt
ent <-
if reqDice
then do
diceEnt <- liftIO $ getDiceEntropy reqEnt
return $ sysEnt `xorBytes` diceEnt
else return sysEnt
when (BS.length ent /= fromIntegral reqEnt) $
throwError "Something went wrong with the entropy size"
mnem <- liftEither $ toMnemonic ent
if splitIn > 1
then do
ks <- liftIO $ splitEntropy splitIn ent
unless (ent == foldl' xorBytes (head ks) (tail ks)) $
throwError "Something went wrong while splitting the keys"
splitMnems <- liftEither $ mapM toMnemonic ks
unsplitMnem <- liftEither $ mergeMnemonicParts splitMnems
unless (mnem == unsplitMnem) $
throwError "Something went wrong while reconstructing the mnemonic"
return (origEnt, mnem, splitMnems)
else return (origEnt, mnem, [])
| otherwise = throwError "The entropy value can only be in [16,20..32]"
systemEntropy :: Natural -> IO (Text, BS.ByteString)
systemEntropy bytes = do
exists <- D.doesFileExist "/dev/random"
if exists
then ("/dev/random",) <$> devRandom bytes
else ("System.Entropy.getEntropy",) <$> getEntropy (fromIntegral bytes)
devRandom :: Natural -> IO BS.ByteString
devRandom bytes =
withBinaryFile "/dev/random" ReadMode (`BS.hGet` fromIntegral bytes)