mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 15:48:54 +00:00
infallibel wallet and account derivation
This commit is contained in:
@@ -4668,10 +4668,6 @@ CounterUnknown:
|
||||
AccountsExhausted:
|
||||
- type: "accountsExhausted"
|
||||
|
||||
Derivation:
|
||||
- type: "derivation"
|
||||
- derivationError: string
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 88dd6c7041a49d6c15e3d83b49f581bbc3a9ac77
|
||||
tag: 31360b40c4d3ede6baac78b00dec51df5812ba84
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -85,7 +85,7 @@ A command that acts on a profile's accounts names the profile and is rejected wh
|
||||
|
||||
`bind` without `account=` binds the account at a counter on the master, which is a high-water mark and not a count of bound accounts, and returns the bound account's index, path and address. With `account=<n>` it binds that account, which is how an account found by a scan is attached to the profile it belongs to, and it is rejected for an account another profile holds. After an import the counter is unknown rather than zero, because the phrase does not encode how many accounts it has been used for, so binding the next account is rejected until a scan sets the counter, while binding a known account is still allowed.
|
||||
|
||||
BIP-32 marks an index as hardened by setting its top bit, so an index at or above 2^31 already has that bit set and derives the same key as the index 2^31 below it: account 2^31 is account 0. That is a collision, not a loss of hardening, and it would put one key under two account indexes. An account index is a simplexmq type that only holds values below 2^31, so the command parser rejects 2^31 and above as a bad command, the columns have CHECK constraints for that bound, and reading a row that violates it is an error. The counter's bound is one higher than an account's, because it contains the next index to bind, and 2^31 there means the counter has passed every index that can be hardened, which `bind` without an index reports.
|
||||
BIP-32 marks an index as hardened by setting its top bit, so an index at or above 2^31 already has that bit set and derives the same key as the index 2^31 below it: account 2^31 is account 0. That is a collision, not a loss of hardening, and it would put one key under two account indexes. Derivation itself cannot fail: for the one key in 2^128 that BIP-32 declares invalid, simplexmq recomputes it as SLIP-0010 specifies and Trezor implements, instead of skipping the index, so every account index has a key. An account index is a simplexmq type that only holds values below 2^31, so the command parser rejects 2^31 and above as a bad command, the columns have CHECK constraints for that bound, and reading a row that violates it is an error. The counter's bound is one higher than an account's, because it contains the next index to bind, and 2^31 there means the counter has passed every index that can be hardened, which `bind` without an index reports.
|
||||
|
||||
`address` reads the counter without changing it, so two calls return the same address, and it derives an address for an account the database has no row for, which a device that lost its database requires. One address per call is sufficient: a caller that scans the tree calls it in a loop.
|
||||
|
||||
@@ -103,7 +103,6 @@ data WalletError
|
||||
| WEAccountNotHeld -- export account, on an account the profile does not hold
|
||||
| WECounterUnknown -- the counter is not set yet, after an import
|
||||
| WEAccountsExhausted -- bind without an index, when the counter has passed every index
|
||||
| WEDerivation {derivationError :: String} -- BIP-32 or BIP-39 derivation failed
|
||||
```
|
||||
|
||||
## Recovery
|
||||
|
||||
@@ -5316,7 +5316,6 @@ export type WalletError =
|
||||
| WalletError.AccountNotHeld
|
||||
| WalletError.CounterUnknown
|
||||
| WalletError.AccountsExhausted
|
||||
| WalletError.Derivation
|
||||
|
||||
export namespace WalletError {
|
||||
export type Tag =
|
||||
@@ -5328,7 +5327,6 @@ export namespace WalletError {
|
||||
| "accountNotHeld"
|
||||
| "counterUnknown"
|
||||
| "accountsExhausted"
|
||||
| "derivation"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
@@ -5365,11 +5363,6 @@ export namespace WalletError {
|
||||
export interface AccountsExhausted extends Interface {
|
||||
type: "accountsExhausted"
|
||||
}
|
||||
|
||||
export interface Derivation extends Interface {
|
||||
type: "derivation"
|
||||
derivationError: string
|
||||
}
|
||||
}
|
||||
|
||||
export type XFTPErrorType =
|
||||
|
||||
@@ -3746,10 +3746,6 @@ class WalletError_counterUnknown(TypedDict):
|
||||
class WalletError_accountsExhausted(TypedDict):
|
||||
type: Literal["accountsExhausted"]
|
||||
|
||||
class WalletError_derivation(TypedDict):
|
||||
type: Literal["derivation"]
|
||||
derivationError: str
|
||||
|
||||
WalletError = (
|
||||
WalletError_noMaster
|
||||
| WalletError_masterExists
|
||||
@@ -3759,10 +3755,9 @@ WalletError = (
|
||||
| WalletError_accountNotHeld
|
||||
| WalletError_counterUnknown
|
||||
| WalletError_accountsExhausted
|
||||
| WalletError_derivation
|
||||
)
|
||||
|
||||
WalletError_Tag = Literal["noMaster", "masterExists", "badMnemonic", "hiddenProfile", "accountBound", "accountNotHeld", "counterUnknown", "accountsExhausted", "derivation"]
|
||||
WalletError_Tag = Literal["noMaster", "masterExists", "badMnemonic", "hiddenProfile", "accountBound", "accountNotHeld", "counterUnknown", "accountsExhausted"]
|
||||
|
||||
class XFTPErrorType_BLOCK(TypedDict):
|
||||
type: Literal["BLOCK"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."88dd6c7041a49d6c15e3d83b49f581bbc3a9ac77" = "1bi0f34c3jb74n61r4inv52k8rc2pq75xxzk2wj7702xb4kdd9dn";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."31360b40c4d3ede6baac78b00dec51df5812ba84" = "0srnp3lsc09r3y5jsy6f6snb5d678l6bxjpxk77dl0lqx2gx7d2j";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
||||
@@ -65,7 +65,7 @@ import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeAlertKind (..), BadgeIss
|
||||
import Simplex.Chat.Badges.Code (badgeCodeText, parseBadgeCode)
|
||||
import Simplex.Chat.Badges.Service (BadgeBalance (..), BadgeServiceCommand (..), BadgeServiceErrorCode (..), BadgeServiceRequest (..), BadgeServiceResponse (..), BadgeStatement (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..), currentBadgeServiceVersion)
|
||||
import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim)
|
||||
import Simplex.Chat.Wallet (AccountIndex, AccountKey, WalletAddress, WalletError (..), accountSecret, deriveAccount, entropyFromMnemonic, newSeedEntropy, newWalletMaster, seedMnemonic)
|
||||
import Simplex.Chat.Wallet (AccountIndex, AccountKey, WalletAddress, WalletError (..), accountSecret, deriveAccount, importWalletMaster, newWalletMaster, seedMnemonic)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..))
|
||||
@@ -1506,10 +1506,9 @@ processChatCommand cxt nm = \case
|
||||
wallet_ <- withFastStore getWallet
|
||||
when (isJust wallet_) $ throwWalletError WEMasterExists
|
||||
-- the counter starts at 0 for a generated seed and is unknown for an imported one
|
||||
(entropy, nextAccount) <- case mnemonic_ of
|
||||
Nothing -> (,Just 0) <$> (asks random >>= atomically . newSeedEntropy)
|
||||
Just phrase -> (,Nothing) <$> liftWallet (entropyFromMnemonic phrase)
|
||||
master <- liftWallet $ newWalletMaster entropy
|
||||
(master, nextAccount) <- case mnemonic_ of
|
||||
Nothing -> (,Just 0) <$> (liftIO . newWalletMaster =<< asks random)
|
||||
Just phrase -> (,Nothing) <$> liftWallet (importWalletMaster phrase)
|
||||
created <- withFastStore' $ \db -> createWallet db master nextAccount
|
||||
unless created $ throwWalletError WEMasterExists
|
||||
pure $ CRWallet user (Just [])
|
||||
@@ -6039,7 +6038,7 @@ withWalletStore action = liftWallet =<< withFastStore action
|
||||
walletAccount :: Wallet -> AccountIndex -> CM (AccountKey, WalletAddress)
|
||||
walletAccount Wallet {walletMaster} n = do
|
||||
g <- asks random
|
||||
liftError' (ChatError . CEWallet) (deriveAccount g walletMaster n)
|
||||
liftIO $ deriveAccount g walletMaster n
|
||||
|
||||
chatCommandP :: Parser ChatCommand
|
||||
chatCommandP =
|
||||
|
||||
@@ -2813,7 +2813,8 @@ viewChatError isCmd logLevel testView = \case
|
||||
SDEUnknownDomain -> "is not included in the connection link's profile"
|
||||
in [plain $ "SimpleX name " <> strEncode domain <> " " <> reason]
|
||||
CEWallet walletErr ->
|
||||
let reason = case walletErr of
|
||||
let reason :: Text
|
||||
reason = case walletErr of
|
||||
WENoMaster -> "this device has no wallet"
|
||||
WEMasterExists -> "this device already has a wallet"
|
||||
WEBadMnemonic -> "not a valid 24 word recovery phrase"
|
||||
@@ -2822,7 +2823,6 @@ viewChatError isCmd logLevel testView = \case
|
||||
WEAccountNotHeld -> "this profile does not hold this account"
|
||||
WECounterUnknown -> "the next account is unknown after an import"
|
||||
WEAccountsExhausted -> "every account index is used"
|
||||
WEDerivation e -> "derivation failed: " <> T.pack e
|
||||
in [plain $ "wallet: " <> reason]
|
||||
CENotResolvedLocally -> ["no matching chat found, name resolution is disabled"]
|
||||
CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr]
|
||||
|
||||
+10
-15
@@ -6,9 +6,9 @@ module Simplex.Chat.Wallet
|
||||
AccountKey,
|
||||
WalletAddress (..),
|
||||
WalletError (..),
|
||||
newSeedEntropy,
|
||||
entropyFromMnemonic,
|
||||
newWalletMaster,
|
||||
entropyFromMnemonic,
|
||||
importWalletMaster,
|
||||
seedMnemonic,
|
||||
deriveAccount,
|
||||
accountSecret,
|
||||
@@ -16,11 +16,8 @@ module Simplex.Chat.Wallet
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteArray.Encoding as BAE
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
@@ -31,7 +28,6 @@ import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
import Simplex.Messaging.Encoding.String (strEncode)
|
||||
import Simplex.Messaging.Eth.Address (addressFromPrivateKey)
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Util (liftError')
|
||||
|
||||
type AccountKey = S.Secp256k1PrivateKey
|
||||
|
||||
@@ -51,30 +47,29 @@ data WalletError
|
||||
| WEAccountNotHeld
|
||||
| WECounterUnknown
|
||||
| WEAccountsExhausted
|
||||
| WEDerivation {derivationError :: String}
|
||||
deriving (Eq, Show)
|
||||
|
||||
masterStrength :: B39.EntropyStrength
|
||||
masterStrength = B39.ES256
|
||||
|
||||
newSeedEntropy :: TVar ChaChaDRG -> STM B39.WalletEntropy
|
||||
newSeedEntropy = B39.randomEntropy masterStrength
|
||||
newWalletMaster :: TVar ChaChaDRG -> IO B32.WalletMaster
|
||||
newWalletMaster g = (`B32.mkWalletMaster` "") <$> atomically (B39.randomEntropy masterStrength g)
|
||||
|
||||
entropyFromMnemonic :: Text -> Either WalletError B39.WalletEntropy
|
||||
entropyFromMnemonic phrase = case B39.parsePhrase phrase of
|
||||
Right ent | B39.entropyWordCount ent == 24 -> Right ent
|
||||
_ -> Left WEBadMnemonic
|
||||
|
||||
newWalletMaster :: B39.WalletEntropy -> Either WalletError B32.WalletMaster
|
||||
newWalletMaster ent = first WEDerivation $ B32.mkWalletMaster ent ""
|
||||
importWalletMaster :: Text -> Either WalletError B32.WalletMaster
|
||||
importWalletMaster phrase = (`B32.mkWalletMaster` "") <$> entropyFromMnemonic phrase
|
||||
|
||||
seedMnemonic :: B32.WalletMaster -> Text
|
||||
seedMnemonic = decodeLatin1 . B39.entropyPhrase . B32.masterEntropy
|
||||
|
||||
deriveAccount :: TVar ChaChaDRG -> B32.WalletMaster -> AccountIndex -> IO (Either WalletError (AccountKey, WalletAddress))
|
||||
deriveAccount g master n = runExceptT $ do
|
||||
k <- B32.xkKey <$> liftError' WEDerivation (B32.derivePath g (B32.walletMasterKey master) path)
|
||||
a <- liftIO $ addressFromPrivateKey g k
|
||||
deriveAccount :: TVar ChaChaDRG -> B32.WalletMaster -> AccountIndex -> IO (AccountKey, WalletAddress)
|
||||
deriveAccount g master n = do
|
||||
k <- B32.xkKey <$> B32.derivePath g (B32.walletMasterKey master) path
|
||||
a <- addressFromPrivateKey g k
|
||||
pure (k, WalletAddress {accountIndex = n, keyPath = decodeLatin1 $ B32.renderPath path, address = decodeLatin1 $ strEncode a})
|
||||
where
|
||||
path = bip44Path Ethereum n
|
||||
|
||||
@@ -33,12 +33,12 @@ testPhrase24 :: Text
|
||||
testPhrase24 = T.unwords $ replicate 23 "abandon" <> ["art"]
|
||||
|
||||
walletMaster :: Text -> B32.WalletMaster
|
||||
walletMaster phrase = either error id $ B32.mkWalletMaster (either error id $ B39.parsePhrase phrase) ""
|
||||
walletMaster phrase = B32.mkWalletMaster (either error id $ B39.parsePhrase phrase) ""
|
||||
|
||||
walletAccount :: Text -> Word32 -> IO (AccountKey, WalletAddress)
|
||||
walletAccount phrase n = do
|
||||
g <- C.newRandom
|
||||
either (error . show) id <$> deriveAccount g (walletMaster phrase) (fromJust $ mkAccountIndex n)
|
||||
deriveAccount g (walletMaster phrase) (fromJust $ mkAccountIndex n)
|
||||
|
||||
addressFromSecret :: String -> IO String
|
||||
addressFromSecret secret = do
|
||||
|
||||
Reference in New Issue
Block a user