mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 17:58:47 +00:00
iteration 6
This commit is contained in:
@@ -46,7 +46,7 @@ The alternative is one account owning several names. A name's owner address is p
|
||||
|
||||
### Why the account level is hardened
|
||||
|
||||
The alternative is BIP-44's ordinary address level, `m/44'/60'/0'/0/n`, which is what MetaMask enumerates and is therefore the friendlier path. It is not hardened, and [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) has a known weakness there: the extended public key of a parent, together with one non-hardened child's private key, yields the parent private key and from it every sibling. An exported account key is one half, and any wallet that enumerates accounts produces the other. The two levels below an account are not hardened, so the two halves together reveal that account's key; nothing else is derived under an account and the account level itself is hardened, so they reveal no other account's key. This satisfies objectives 2 and 3, and it is worth the loss of MetaMask's default path.
|
||||
The alternative is BIP-44's ordinary address level, `m/44'/60'/0'/0/n`, which is what MetaMask enumerates and is therefore the friendlier path. It is not hardened, and [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) has a known weakness there: the extended public key of a parent, together with one non-hardened child's private key, yields the parent private key and from it every sibling. An exported account key is one half, and any wallet that enumerates accounts produces the other. The two levels below an account are not hardened, so the two halves together reveal the extended private key at the account level, m/44'/60'/n'; nothing else is derived under an account and the account level itself is hardened, so they reveal no other account's key. This satisfies objectives 2 and 3, and it is worth the loss of MetaMask's default path.
|
||||
|
||||
### Why 24 words
|
||||
|
||||
@@ -56,7 +56,7 @@ Entropy cannot be added to a seed afterwards and the uses of a seed can grow, so
|
||||
|
||||
An account is bound to at most one chat profile, and a profile to any number of accounts, because a profile can own any number of names. Only one of those names points at the profile as its SimpleX domain name, but which one is recorded by the names layer, not by the wallet: the wallet records only which profile an account belongs to.
|
||||
|
||||
Accounts are allocated in order and never reused, because an account the device no longer tracks still owns whatever it holds, and an account can remain unbound. Nothing is bound when a profile is created; an account is bound on first use, when the user buys a name for a profile, so a device on which the user never buys anything has never derived a key.
|
||||
Accounts are allocated in order and never reused, because an account the device no longer tracks still owns whatever it holds. An account can remain unbound. Nothing is bound when a profile is created; an account is bound on first use, when the user buys a name for a profile, so a device on which the user neither buys anything nor requests an address has never derived a key.
|
||||
|
||||
An account cannot be bound to a hidden profile, so no name can be bought for a hidden profile. Two things would leak: the master derives every account, so whoever can use any profile can also derive a hidden profile's account keys; and a name is written into the profile's own database row and listed across the device, while a hidden profile is a filter on what is shown, not encryption. Closing either requires changes in the profiles and in the name record, not in the key layout. Incognito is a property of a connection in this app rather than of a profile, so there is nothing at this level to reject; an incognito connection has no profile of its own to bind an account to.
|
||||
|
||||
@@ -75,7 +75,7 @@ These commands are an internal API, called by the names commands and by any late
|
||||
derived at; without account=, the next free one
|
||||
/_wallet export master the master phrase
|
||||
/_wallet export account <userId> <n> the secret of an account the profile holds,
|
||||
0x and 64 hex
|
||||
0x and 64 hex digits
|
||||
/_wallet delete delete the master entropy and its accounts
|
||||
```
|
||||
|
||||
|
||||
@@ -2816,7 +2816,7 @@ viewChatError isCmd logLevel testView = \case
|
||||
WENoMaster -> "this device has no wallet"
|
||||
WEMasterExists -> "this device already has a wallet"
|
||||
WEBadMnemonic -> "not a valid 24 word recovery phrase"
|
||||
WEHiddenProfile -> "a hidden profile cannot hold an account"
|
||||
WEHiddenProfile -> "an account cannot be bound to a hidden profile"
|
||||
WEAccountBound -> "another profile holds this account"
|
||||
WEAccountNotHeld -> "this profile does not hold this account"
|
||||
WECounterUnknown -> "the next account is unknown after an import"
|
||||
|
||||
@@ -31,8 +31,8 @@ testPhrase24 = T.unwords $ replicate 23 "abandon" <> ["art"]
|
||||
seedEntropy :: Text -> BA.ScrubbedBytes
|
||||
seedEntropy phrase = B39.mnemonicToEntropy . either error id $ B39.parseMnemonic phrase
|
||||
|
||||
walletAccount :: BA.ScrubbedBytes -> AccountIndex -> IO (AccountKey, WalletAddress)
|
||||
walletAccount entropy n = either (error . show) id <$> deriveAccount entropy n
|
||||
walletAccount :: Text -> AccountIndex -> IO (AccountKey, WalletAddress)
|
||||
walletAccount phrase n = either (error . show) id <$> deriveAccount (seedEntropy phrase) n
|
||||
|
||||
addressFromSecret :: String -> IO String
|
||||
addressFromSecret secret = do
|
||||
@@ -50,14 +50,14 @@ accountBound cc idx = (take 1 . words <$> getTermLine cc) `shouldReturn` [idx]
|
||||
walletDerivationTests :: Spec
|
||||
walletDerivationTests = do
|
||||
Hspec.it "derives the addresses another wallet derives for the same phrase" $ do
|
||||
let addrOf n = address . snd <$> walletAccount (seedEntropy testPhrase12) n
|
||||
let addrOf n = address . snd <$> walletAccount testPhrase12 n
|
||||
addrOf 0 `shouldReturn` "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"
|
||||
addrOf 1 `shouldReturn` "0x78839F6054d7ed13918bAe0473BA31b1Ca9D7265"
|
||||
Hspec.it "the exported secret is the one another wallet shows for that account" $ do
|
||||
(k, _) <- walletAccount (seedEntropy testPhrase12) 0
|
||||
(k, _) <- walletAccount testPhrase12 0
|
||||
accountSecret k `shouldBe` "0x1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727"
|
||||
Hspec.it "every account has its own address" $ do
|
||||
addrs <- mapM (fmap (address . snd) . walletAccount (seedEntropy testPhrase12)) [0 .. 9]
|
||||
addrs <- mapM (fmap (address . snd) . walletAccount testPhrase12) [0 .. 9]
|
||||
length (nub addrs) `shouldBe` 10
|
||||
Hspec.it "renders a secret whose first byte is zero with 64 hex digits" $ do
|
||||
k <- either error id <$> S.mkPrivateKey (BA.convert $ B.pack ('\0' : replicate 31 '\1'))
|
||||
@@ -65,8 +65,8 @@ walletDerivationTests = do
|
||||
take 4 secret `shouldBe` "0x00"
|
||||
length secret `shouldBe` 66
|
||||
Hspec.it "renders the path an account is derived at" $ do
|
||||
(keyPath . snd <$> walletAccount (seedEntropy testPhrase12) 0) `shouldReturn` "m/44'/60'/0'/0/0"
|
||||
(keyPath . snd <$> walletAccount (seedEntropy testPhrase12) 7) `shouldReturn` "m/44'/60'/7'/0/0"
|
||||
(keyPath . snd <$> walletAccount testPhrase12 0) `shouldReturn` "m/44'/60'/0'/0/0"
|
||||
(keyPath . snd <$> walletAccount testPhrase12 7) `shouldReturn` "m/44'/60'/7'/0/0"
|
||||
Hspec.it "rejects an account index at or above 2^31" $
|
||||
(void <$> deriveAccount (seedEntropy testPhrase12) 2147483648) `shouldReturn` Left WEIndexTooLarge
|
||||
Hspec.it "round-trips the phrase it was imported from" $
|
||||
@@ -214,7 +214,7 @@ testWalletExport ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
(idx', path', addr', _) <- exportRow <$> getTermLine alice
|
||||
idx' `shouldBe` "1"
|
||||
path' `shouldBe` "m/44'/60'/1'/0/0"
|
||||
(T.unpack . address . snd <$> walletAccount (seedEntropy testPhrase24) 1) `shouldReturn` addr'
|
||||
(T.unpack . address . snd <$> walletAccount testPhrase24 1) `shouldReturn` addr'
|
||||
alice ##> "/_wallet address account=1"
|
||||
(words <$> getTermLine alice) `shouldReturn` ["1", "m/44'/60'/1'/0/0", addr']
|
||||
|
||||
@@ -274,7 +274,7 @@ testWalletHiddenProfile ps = withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
alice <## "messages are hidden (use /tail to view)"
|
||||
alice <## "profile is hidden"
|
||||
alice ##> "/_wallet bind 2"
|
||||
alice <## "wallet: a hidden profile cannot hold an account"
|
||||
alice <## "wallet: an account cannot be bound to a hidden profile"
|
||||
|
||||
testWalletExportNotHeld :: HasCallStack => TestParams -> IO ()
|
||||
testWalletExportNotHeld ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
|
||||
Reference in New Issue
Block a user