mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-28 04:48:58 +00:00
core: export the secret of a single name key
/_wallet export gives the seed mnemonic, /_wallet export <account> <name> gives the secret of one derived key, so a single name can be handed over without the seed. Both are what a wallet takes on import: the mnemonic as a recovery phrase, the secret as hex. The commands are named for what they return, APIWalletExportSeedMnemonic and APIWalletExportDerivedSecret, as the difference is which secret leaves the device. 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
e7f5908d01
commit
db1b6413e0
@@ -68,7 +68,7 @@ import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Types.UITheme
|
||||
import Simplex.Chat.Wallet (AccountIndex)
|
||||
import Simplex.Chat.Wallet (AccountIndex, NameIndex)
|
||||
import Simplex.Chat.Util (liftIOEither)
|
||||
import Simplex.FileTransfer.Description (FileDescriptionURI)
|
||||
import Simplex.Messaging.Server.Information (ServerPublicInfo)
|
||||
@@ -420,7 +420,8 @@ data ChatCommand
|
||||
| APIWallet
|
||||
| APIWalletCreate
|
||||
| APIWalletImport {recoveryPhrase :: Text}
|
||||
| APIWalletExport
|
||||
| APIWalletExportSeedMnemonic
|
||||
| APIWalletExportDerivedSecret {accountIndex :: AccountIndex, nameIndex :: NameIndex}
|
||||
| APIWalletDelete
|
||||
| APISendCallInvitation ContactId CallType
|
||||
| SendCallInvitation ContactName CallType
|
||||
@@ -750,7 +751,8 @@ allowRemoteCommand = \case
|
||||
APIWallet -> False
|
||||
APIWalletCreate -> False
|
||||
APIWalletImport _ -> False
|
||||
APIWalletExport -> False
|
||||
APIWalletExportSeedMnemonic -> False
|
||||
APIWalletExportDerivedSecret {} -> False
|
||||
APIWalletDelete -> False
|
||||
_ -> True
|
||||
|
||||
@@ -854,7 +856,8 @@ data ChatResponse
|
||||
| CRServiceResponse {user :: User, responseData :: J.Object}
|
||||
| CRServiceReplyAccepted {user :: User, connectionId :: AgentConnId}
|
||||
| CRWallet {user :: User, walletKeyExists :: Bool, walletAccounts :: [(Text, AccountIndex, Bool, [(Text, Text)])]}
|
||||
| CRWalletPhrase {user :: User, recoveryPhrase :: Text}
|
||||
| CRWalletSeedMnemonic {user :: User, recoveryPhrase :: Text}
|
||||
| CRWalletDerivedSecret {user :: User, keyPath :: Text, address :: Text, derivedSecret :: Text}
|
||||
| CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact}
|
||||
| CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool, msgSigned :: Bool}
|
||||
| CRGroupsList {user :: User, groups :: [GroupInfo]}
|
||||
|
||||
@@ -59,7 +59,7 @@ 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, getDeviceSeed, getOrCreateAccountRef, getSeedAccounts, importSeed)
|
||||
import Simplex.Chat.Wallet (NameIndex, WalletSeed (..), accountAddress, deriveNameKey, importRecoveryKey, newSeed, recoveryKeyPhrase, renderNameKeyPath)
|
||||
import Simplex.Chat.Wallet (NameIndex, WalletSeed (..), accountAddress, accountSecret, deriveNameKey, importRecoveryKey, newSeed, recoveryKeyPhrase, renderNameKeyPath)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..))
|
||||
@@ -1515,10 +1515,14 @@ processChatCommand cxt nm = \case
|
||||
r <- withFastStore' $ \db -> importSeed db user entropy
|
||||
when (isNothing r) $ throwCmdError "this device already has a wallet key"
|
||||
processChatCommand cxt nm APIWallet
|
||||
APIWalletExport -> withUser $ \user -> do
|
||||
APIWalletExportSeedMnemonic -> withUser $ \user -> do
|
||||
seed <- withFastStore' getDeviceSeed >>= maybe (throwCmdError noKeyError) pure
|
||||
phrase <- either (throwCmdError . ("wallet: " <>)) pure $ recoveryKeyPhrase seed
|
||||
pure $ CRWalletPhrase user (safeDecodeUtf8 phrase)
|
||||
pure $ CRWalletSeedMnemonic user (safeDecodeUtf8 phrase)
|
||||
APIWalletExportDerivedSecret acct nameIdx -> withUser $ \user -> do
|
||||
seed <- withFastStore' getDeviceSeed >>= maybe (throwCmdError noKeyError) pure
|
||||
acc <- either (throwCmdError . ("wallet: " <>)) pure $ deriveNameKey seed acct nameIdx
|
||||
pure $ CRWalletDerivedSecret user (renderNameKeyPath acct nameIdx) (tshow $ accountAddress acc) (safeDecodeUtf8 $ accountSecret acc)
|
||||
APIWalletDelete -> withUser $ \_ -> do
|
||||
seed <- withFastStore' getDeviceSeed >>= maybe (throwCmdError noKeyError) pure
|
||||
withFastStore' $ \db -> deleteSeed db (wsId seed)
|
||||
@@ -5586,7 +5590,8 @@ chatCommandP =
|
||||
"/_service_response " *> (APISendServiceResponse <$> A.decimal <* A.space <*> strP <* A.space <*> jsonP),
|
||||
"/_wallet create" $> APIWalletCreate,
|
||||
"/_wallet import " *> (APIWalletImport <$> textP),
|
||||
"/_wallet export" $> APIWalletExport,
|
||||
"/_wallet export " *> (APIWalletExportDerivedSecret <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_wallet export" $> APIWalletExportSeedMnemonic,
|
||||
"/_wallet delete" $> APIWalletDelete,
|
||||
"/_wallet" $> APIWallet,
|
||||
"/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP),
|
||||
|
||||
@@ -199,7 +199,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, te
|
||||
plain ("account " <> tshow acct <> " (" <> n <> (if active then ", active" else "") <> ")")
|
||||
: zipWith nameRow [0 :: Int ..] keys
|
||||
nameRow k (path, addr) = plain $ " name " <> tshow k <> " " <> path <> " " <> addr
|
||||
CRWalletPhrase u phrase -> ttyUser u [plain phrase]
|
||||
CRWalletSeedMnemonic u phrase -> ttyUser u [plain phrase]
|
||||
CRWalletDerivedSecret u path addr secret -> ttyUser u [plain $ path <> " " <> addr <> " " <> secret]
|
||||
CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView
|
||||
CRPublicGroupCreated u g _groupLink _relays -> ttyUser u $ viewGroupCreated g testView
|
||||
CRPublicGroupCreationFailed u results -> ttyUser u $ viewPublicGroupCreationFailed results
|
||||
|
||||
@@ -17,11 +17,13 @@ module Simplex.Chat.Wallet
|
||||
deriveNameKey,
|
||||
renderNameKeyPath,
|
||||
accountAddress,
|
||||
accountSecret,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.ByteArray.Encoding as BAE
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Text (Text)
|
||||
@@ -91,3 +93,7 @@ deriveNameKey s acc nm = do
|
||||
|
||||
accountAddress :: WalletAccount -> Address
|
||||
accountAddress = addressFromPrivateKey . waKey
|
||||
|
||||
-- | Hex, as wallets take it when a key is imported on its own.
|
||||
accountSecret :: WalletAccount -> ByteString
|
||||
accountSecret a = "0x" <> BAE.convertToBase BAE.Base16 (S.unPrivateKey $ waKey a)
|
||||
|
||||
Reference in New Issue
Block a user