This repository has been archived by the owner on Aug 18, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 630
/
Mnemonic.hs
390 lines (318 loc) · 11.4 KB
/
Mnemonic.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
-- | Module providing restoring from backup phrase functionality
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Cardano.Mnemonic
(
-- * Types
Mnemonic
, Entropy
, EntropySize
, MnemonicWords
-- * Errors
, MnemonicError(..)
, MnemonicException(..)
-- ** Re-exports from 'cardano-crypto'
, EntropyError(..)
, DictionaryError(..)
, MnemonicWordsError(..)
-- * Creating @Mnemonic@ (resp. @Entropy@)
, mkEntropy
, mkMnemonic
, genEntropy
-- * Converting from and to @Mnemonic@ (resp. @Entropy@)
, mnemonicToEntropy
, mnemonicToSeed
, mnemonicToAesKey
, entropyToMnemonic
, entropyToByteString
) where
import Universum
import Basement.Sized.List (unListN)
import Control.Arrow (left)
import Control.Lens ((?~))
import Crypto.Encoding.BIP39
import Crypto.Hash (Blake2b_256, Digest, hash)
import Data.Aeson (FromJSON (..), ToJSON (..))
import Data.Aeson.Types (Parser)
import Data.ByteArray (constEq, convert)
import Data.ByteString (ByteString)
import Data.Default (Default (def))
import Data.Swagger (NamedSchema (..), ToSchema (..), maxItems,
minItems)
import Formatting (bprint, build, formatToString, (%))
import Test.QuickCheck (Arbitrary (..))
import Test.QuickCheck.Gen (vectorOf)
import Pos.Binary (serialize')
import Pos.Crypto (AesKey (..))
import Pos.Infra.Util.LogSafe (SecureLog)
import qualified Basement.Compat.Base as Basement
import qualified Basement.String as Basement
import qualified Basement.UArray as Basement
import qualified Crypto.Encoding.BIP39.English as Dictionary
import qualified Crypto.Random.Entropy as Crypto
import qualified Data.ByteString.Char8 as B8
import qualified Formatting.Buildable
--
-- TYPES
--
-- | A backup-phrase in the form of a non-empty of Mnemonic words
-- Constructor isn't exposed.
data Mnemonic (mw :: Nat) = Mnemonic
{ mnemonicToEntropy :: Entropy (EntropySize mw)
, mnemonicToSentence :: MnemonicSentence mw
} deriving (Eq, Show)
--
-- ERRORS
--
data MnemonicException csz = UnexpectedEntropyError (EntropyError csz)
deriving (Show, Typeable)
data MnemonicError csz
= ErrMnemonicWords MnemonicWordsError
| ErrEntropy (EntropyError csz)
| ErrDictionary DictionaryError
| ErrForbidden
deriving (Show)
--
-- CONSTRUCTORS
--
-- | Smart-constructor for the Entropy
mkEntropy
:: forall n csz. (ValidEntropySize n, ValidChecksumSize n csz)
=> ByteString
-> Either (EntropyError csz) (Entropy n)
mkEntropy = toEntropy
-- | Generate Entropy of a given size using a random seed.
--
-- Example:
-- do
-- ent <- genEntropy :: IO (Entropy 12)
genEntropy
:: forall n csz. (ValidEntropySize n, ValidChecksumSize n csz)
=> IO (Entropy n)
genEntropy =
let
size =
fromIntegral $ natVal (Proxy @n)
eitherToIO =
either (throwM . UnexpectedEntropyError) return
in
(eitherToIO . mkEntropy) =<< Crypto.getEntropy (size `div` 8)
-- | Smart-constructor for the Mnemonic
mkMnemonic
:: forall mw n csz.
( ConsistentEntropy n mw csz
, EntropySize mw ~ n
)
=> [Text]
-> Either (MnemonicError csz) (Mnemonic mw)
mkMnemonic wordsm = do
phrase <- left ErrMnemonicWords
$ mnemonicPhrase @mw (toUtf8String <$> wordsm)
sentence <- left ErrDictionary
$ mnemonicPhraseToMnemonicSentence Dictionary.english phrase
entropy <- left ErrEntropy
$ wordsToEntropy sentence
when (isForbiddenMnemonic sentence) $ Left ErrForbidden
pure Mnemonic
{ mnemonicToEntropy = entropy
, mnemonicToSentence = sentence
}
--
-- CONVERSIONS
--
-- | Convert a mnemonic to a seed that can be used to initiate a HD wallet.
-- Note that our current implementation deviates from BIP-39 as:
--
-- - We do not use the password to produce the seed
-- - We rely on a fast blake2b hashing function rather than a slow PKBDF2
--
-- Somehow, we also convert mnemonic to raw bytes using a Blake2b_256 but with
-- a slightly different approach when converting them to aesKey when redeeming
-- paper wallets... In this case, we do not serialize the inputs and outputs.
--
-- For now, we have two use case for that serialization function. When creating
-- an HD wallet seed, in which case, the function we use is `serialize'` from
-- the Pos.Binary module. And, when creating an AESKey seed in which case we
-- simply pass the `identity` function.
mnemonicToSeed
:: Mnemonic mw
-> ByteString
mnemonicToSeed =
serialize' . blake2b . serialize' . entropyToByteString . mnemonicToEntropy
-- | Convert a mnemonic to a seed AesKey. Almost identical to @MnemonictoSeed@
-- minus the extra serialization.
mnemonicToAesKey
:: Mnemonic mw
-> AesKey
mnemonicToAesKey =
AesKey. blake2b . entropyToByteString . mnemonicToEntropy
-- | Convert an Entropy to a corresponding Mnemonic Sentence
entropyToMnemonic
:: forall mw n csz.
( ValidMnemonicSentence mw
, ValidEntropySize n
, ValidChecksumSize n csz
, n ~ EntropySize mw
, mw ~ MnemonicWords n
)
=> Entropy n
-> Mnemonic mw
entropyToMnemonic entropy = Mnemonic
{ mnemonicToSentence = entropyToWords entropy
, mnemonicToEntropy = entropy
}
-- | Convert 'Entropy' to a raw 'ByteString'
entropyToByteString
:: Entropy n
-> ByteString
entropyToByteString =
entropyRaw
--
-- INTERNALS
--
-- Constant-time comparison of any sentence with the 12-word example mnemonic
isForbiddenMnemonic :: (ValidMnemonicSentence mw) => MnemonicSentence mw -> Bool
isForbiddenMnemonic sentence =
let
bytes =
sentenceToRawString sentence
forbiddenMnemonics = sentenceToRawString <$>
[ mnemonicToSentence (def @(Mnemonic 12))
]
in
any (constEq bytes) forbiddenMnemonics
sentenceToRawString :: (ValidMnemonicSentence mw) => MnemonicSentence mw -> Basement.UArray Word8
sentenceToRawString =
Basement.toBytes Basement.UTF8 . mnemonicSentenceToString Dictionary.english
-- | Simple Blake2b 256-bit of a ByteString
blake2b :: ByteString -> ByteString
blake2b =
convert @(Digest Blake2b_256) . hash
toUtf8String :: Text -> Basement.String
toUtf8String =
Basement.fromString . toString
fromUtf8String :: Basement.String -> Text
fromUtf8String =
toText . Basement.toList
-- | The initial seed has to be vector or length multiple of 4 bytes and shorter
-- than 64 bytes. Not that this is good for testing or examples, but probably
-- not for generating truly random Mnemonic words.
--
-- See 'Crypto.Random.Entropy (getEntropy)'
instance
( ValidEntropySize n
, ValidChecksumSize n csz
) => Arbitrary (Entropy n) where
arbitrary =
let
size = fromIntegral $ natVal (Proxy @n)
entropy = mkEntropy @n . B8.pack <$> vectorOf (size `quot` 8) arbitrary
in
either (error . show . UnexpectedEntropyError) identity <$> entropy
-- Same remark from 'Arbitrary Entropy' applies here.
instance
( n ~ EntropySize mw
, mw ~ MnemonicWords n
, ValidChecksumSize n csz
, ValidEntropySize n
, ValidMnemonicSentence mw
, Arbitrary (Entropy n)
) => Arbitrary (Mnemonic mw) where
arbitrary =
entropyToMnemonic <$> arbitrary @(Entropy n)
instance (KnownNat csz) => Exception (MnemonicException csz)
-- FIXME: Suggestion, we could -- when certain flags are turned on -- display
-- a fingerprint of the Mnemonic, like a PKBDF2 over n iterations. This could be
-- useful for debug to know whether two users are using the same mnemonic words
-- and relatively benign EVEN THOUGH, it will permit to somewhat tight requests
-- to a specific identity (since mnemonic words are 'unique', they are supposed
-- to uniquely identify users, hence the privacy issue). For debbugging only and
-- with the user consent, that's something we could do.
instance Buildable (Mnemonic mw) where
build _ =
"<mnemonic>"
instance Buildable (SecureLog (Mnemonic mw)) where
build _ =
"<mnemonic>"
instance Buildable (MnemonicError csz) where
build = \case
ErrMnemonicWords (ErrWrongNumberOfWords a e) ->
bprint ("MnemonicError: Invalid number of mnemonic words: got "%build%" words, expected "%build%" words") a e
ErrDictionary (ErrInvalidDictionaryWord w) ->
bprint ("MnemonicError: Invalid dictionary word: "%build%"") (fromUtf8String w)
ErrEntropy (ErrInvalidEntropyLength a e) ->
bprint ("MnemonicError: Invalid entropy length: got "%build%" bits, expected "%build%" bits") a e
ErrEntropy (ErrInvalidEntropyChecksum a e) ->
bprint ("MnemonicError: Invalid entropy checksum: got "%build%", expected "%build) (show' a) (show' e)
ErrForbidden ->
bprint "Forbidden Mnemonic: an example Mnemonic has been submitted. \
\Please generate a fresh and private Mnemonic from a trusted source"
where
show' :: Checksum csz -> String
show' = show
-- | To use everytime we need to show an example of a Mnemonic. This particular
-- mnemonic is rejected to prevent users from using it on a real wallet.
instance Default (Mnemonic 12) where
def =
let
wordsm =
[ "squirrel"
, "material"
, "silly"
, "twice"
, "direct"
, "slush"
, "pistol"
, "razor"
, "become"
, "junk"
, "kingdom"
, "flee"
]
phrase = either (error . show) id
(mnemonicPhrase @12 (toUtf8String <$> wordsm))
sentence = either (error . show) id
(mnemonicPhraseToMnemonicSentence Dictionary.english phrase)
entropy = either (error . show) id
(wordsToEntropy @(EntropySize 12) sentence)
in Mnemonic
{ mnemonicToSentence = sentence
, mnemonicToEntropy = entropy
}
instance
( n ~ EntropySize mw
, mw ~ MnemonicWords n
, ValidChecksumSize n csz
, ValidEntropySize n
, ValidMnemonicSentence mw
, Arbitrary (Entropy n)
) => FromJSON (Mnemonic mw) where
parseJSON =
parseJSON >=> (eitherToParser . mkMnemonic)
instance ToJSON (Mnemonic mw) where
toJSON =
toJSON
. map (fromUtf8String . dictionaryIndexToWord Dictionary.english)
. unListN
. mnemonicSentenceToListN
. mnemonicToSentence
instance (KnownNat mw) => ToSchema (Mnemonic mw) where
declareNamedSchema _ = do
let mw = natVal (Proxy :: Proxy mw)
NamedSchema _ schema <- declareNamedSchema (Proxy @[Text])
return $ NamedSchema (Just "Mnemonic") schema
& minItems ?~ fromIntegral mw
& maxItems ?~ fromIntegral mw
--
-- Miscellaneous
--
-- | Convert a given Either to an Aeson Parser
eitherToParser :: Buildable a => Either a b -> Parser b
eitherToParser =
either (fail . formatToString build) pure