mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 17:58:47 +00:00
core: wallet export and delete act on the device key
The key is one per device, so both commands read it directly instead of going through the active profile's account. A profile without an account of its own can now export, and the delete error says that the key goes for every profile on the device. The confirmation word is lowercased, as the phrase is when imported. Two known limitations are written down: account indexes restart at 0 after an import, so profiles are bound in the order they ask for a key, and the gap left by a hidden profile in the printed indexes shows that it exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX
This commit is contained in:
co-authored by
Claude Opus 5
parent
70d4c128bd
commit
e5d18bd59e
@@ -58,7 +58,7 @@ import qualified Data.UUID.V4 as V4
|
||||
import Simplex.Chat.Library.Subscriber
|
||||
import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), badgeServerCredential, maxXFTPFileSize, mkBadgeStatus, verifyCredential)
|
||||
import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim)
|
||||
import Simplex.Chat.Store.Wallets (deleteSeed, getBoundAccount, getDeviceSeed, getOrCreateAccountRef, getSeedAccounts, importSeed)
|
||||
import Simplex.Chat.Store.Wallets (deleteSeed, getDeviceSeed, getOrCreateAccountRef, getSeedAccounts, importSeed)
|
||||
import Simplex.Chat.Wallet (NameIndex, WalletSeed (..), accountAddress, deriveNameKey, importRecoveryKey, newSeed, recoveryKeyPhrase, renderNameKeyPath)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
@@ -1496,7 +1496,8 @@ processChatCommand cxt nm = \case
|
||||
accs <- case seed_ of
|
||||
Nothing -> pure []
|
||||
Just seed -> do
|
||||
-- hidden profiles are left out, as they are by /users
|
||||
-- hidden profiles are left out, as they are by /users, but the gap in
|
||||
-- account indexes still shows that one exists
|
||||
as <- filter (\(_, _, active, hidden) -> active || not hidden) <$> withFastStore' (\db -> getSeedAccounts db (wsId seed))
|
||||
forM as $ \(n, acct, active, _) -> do
|
||||
keys <- forM [0 .. walletNamesShown - 1] $ \k -> do
|
||||
@@ -1515,17 +1516,17 @@ processChatCommand cxt nm = \case
|
||||
when (isNothing r) $ throwCmdError "this device already has a wallet key"
|
||||
processChatCommand cxt nm APIWallet
|
||||
APIWalletExport -> withUser $ \user -> do
|
||||
(seed, _) <- withFastStore' (\db -> getBoundAccount db user) >>= maybe (throwCmdError noKeyError) pure
|
||||
seed <- withFastStore' getDeviceSeed >>= maybe (throwCmdError noKeyError) pure
|
||||
phrase <- either (throwCmdError . ("wallet: " <>)) pure $ recoveryKeyPhrase seed
|
||||
pure $ CRWalletPhrase user (safeDecodeUtf8 phrase)
|
||||
APIWalletDelete confirmWord -> withUser $ \user -> do
|
||||
seed <- withFastStore' getDeviceSeed >>= maybe (throwCmdError noKeyError) pure
|
||||
phrase <- either (throwCmdError . ("wallet: " <>)) pure $ recoveryKeyPhrase seed
|
||||
case reverse . T.words $ safeDecodeUtf8 phrase of
|
||||
w : _ | w == confirmWord -> do
|
||||
w : _ | w == T.toLower confirmWord -> do
|
||||
withFastStore' $ \db -> deleteSeed db (wsId seed)
|
||||
processChatCommand cxt nm APIWallet
|
||||
_ -> throwCmdError "to confirm, pass the last word of the recovery phrase"
|
||||
_ -> throwCmdError "this deletes the wallet key for all profiles on this device, to confirm pass the last word of the recovery phrase"
|
||||
APISendCallInvitation contactId callType -> withUser $ \user -> do
|
||||
-- party initiating call
|
||||
ct <- withFastStore $ \db -> getContact db cxt user contactId
|
||||
@@ -5467,7 +5468,7 @@ walletNamesShown :: NameIndex
|
||||
walletNamesShown = 2
|
||||
|
||||
noKeyError :: String
|
||||
noKeyError = "no wallet key for this profile"
|
||||
noKeyError = "no wallet key on this device"
|
||||
|
||||
chatCommandP :: Parser ChatCommand
|
||||
chatCommandP =
|
||||
|
||||
@@ -12,7 +12,8 @@ CREATE TABLE wallet_seeds (
|
||||
wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
seed BLOB NOT NULL, -- BIP-39 entropy, 16-32 bytes
|
||||
-- known issue: after import this starts at 0, so a recovered device can
|
||||
-- re-issue an account that already owns names
|
||||
-- re-issue an account that already owns names, and profiles are bound in
|
||||
-- the order they ask for a key, not the order they had
|
||||
next_account_index INTEGER NOT NULL DEFAULT 0,
|
||||
-- one key per device for now
|
||||
single_seed INTEGER NOT NULL DEFAULT 1 UNIQUE
|
||||
|
||||
@@ -858,7 +858,8 @@ CREATE TABLE wallet_seeds(
|
||||
wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
seed BLOB NOT NULL, -- BIP-39 entropy, 16-32 bytes
|
||||
-- known issue: after import this starts at 0, so a recovered device can
|
||||
-- re-issue an account that already owns names
|
||||
-- re-issue an account that already owns names, and profiles are bound in
|
||||
-- the order they ask for a key, not the order they had
|
||||
next_account_index INTEGER NOT NULL DEFAULT 0,
|
||||
-- one key per device for now
|
||||
single_seed INTEGER NOT NULL DEFAULT 1 UNIQUE
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
module Simplex.Chat.Store.Wallets
|
||||
( getDeviceSeed,
|
||||
getBoundAccount,
|
||||
getSeedAccounts,
|
||||
getOrCreateAccountRef,
|
||||
importSeed,
|
||||
|
||||
@@ -65,7 +65,7 @@ testWalletCreate ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet"
|
||||
alice <## "no wallet key"
|
||||
alice ##> "/_wallet export"
|
||||
alice <## "bad chat command: no wallet key for this profile"
|
||||
alice <## "bad chat command: no wallet key on this device"
|
||||
alice ##> "/_wallet create"
|
||||
rows <- accountRows alice "alice, active" 0
|
||||
map fst rows `shouldBe` ["m/44'/60'/0'/0/0", "m/44'/60'/0'/0/1"]
|
||||
@@ -86,11 +86,16 @@ testWalletSecondProfile :: HasCallStack => TestParams -> IO ()
|
||||
testWalletSecondProfile ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create"
|
||||
rows <- accountRows alice "alice, active" 0
|
||||
alice ##> "/_wallet export"
|
||||
phrase <- getTermLine alice
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice ##> "/_wallet"
|
||||
_ <- accountRows alice "alice" 0
|
||||
alice <## "this profile has no wallet key"
|
||||
-- the key belongs to the device, so a profile without an account exports it too
|
||||
alice ##> "/_wallet export"
|
||||
alice <## phrase
|
||||
alice ##> "/_wallet create"
|
||||
_ <- accountRows alice "alice" 0
|
||||
rows' <- accountRows alice "alisa, active" 1
|
||||
@@ -115,8 +120,8 @@ testWalletDelete ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet import " <> B.unpack testPhrase)
|
||||
_ <- accountRows alice "alice, active" 0
|
||||
alice ##> "/_wallet delete abandon"
|
||||
alice <## "bad chat command: to confirm, pass the last word of the recovery phrase"
|
||||
alice ##> "/_wallet delete about"
|
||||
alice <## "bad chat command: this deletes the wallet key for all profiles on this device, to confirm pass the last word of the recovery phrase"
|
||||
alice ##> "/_wallet delete About"
|
||||
alice <## "no wallet key"
|
||||
-- deleting unbinds the profile, so a key can be imported again
|
||||
alice ##> ("/_wallet import " <> B.unpack testPhrase)
|
||||
|
||||
Reference in New Issue
Block a user