diff --git a/docs/rfcs/2026-09-10-name-ownership-keys.md b/docs/rfcs/2026-09-10-name-ownership-keys.md index 32f740f3f1..85037134c9 100644 --- a/docs/rfcs/2026-09-10-name-ownership-keys.md +++ b/docs/rfcs/2026-09-10-name-ownership-keys.md @@ -124,6 +124,18 @@ import, so `/_wallet bind` with no account can hand out one that already owns names. Only a scan of owned names can restore the mark, and that lands with the registrar. +## Hidden profiles + +`/_wallet` names the other profiles on the seed and never numbers them, so a +hidden profile leaves no gap in a list of account indexes. + +That hides its existence from the listing, and nothing more. The seed is one per +device, so whoever unlocks any profile can export the phrase and derive every +account, including a hidden profile's. A hidden profile's names are not +pseudonymous against someone who already holds the device and one password. This +is a consequence of one seed per device, and a key per profile rather than per +device is what would change it. + ## Scope Not here, and unchanged from the prototype: buying a name, the names protocol, diff --git a/src/Simplex/Chat/Store/Wallets.hs b/src/Simplex/Chat/Store/Wallets.hs index 99eb4b226e..6d2b659764 100644 --- a/src/Simplex/Chat/Store/Wallets.hs +++ b/src/Simplex/Chat/Store/Wallets.hs @@ -88,20 +88,22 @@ bindAccountIndex db user@User {userId} sId = \case if acct >= 0x80000000 then pure $ Left "no free account on this key" else Right () <$ bindUser db userId sId acct - Just acct -> do - taken <- - maybeFirstRow fromOnly $ - DB.query - db - "SELECT 1 FROM users WHERE wallet_seed_id = ? AND wallet_account_index = ? AND user_id != ?" - (sId, fromIntegral acct :: Int64, userId) - case (taken :: Maybe Int64) of - Just _ -> pure $ Left "another profile uses this account" - Nothing -> do - bindUser db userId sId (fromIntegral acct) - -- the counter moves past it, so the next profile is not handed the same one - setNextAccountIndex db sId (fromIntegral acct + 1) - pure $ Right () + Just acct + | acct >= 0x80000000 -> pure $ Left "account index too large" + | otherwise -> do + taken <- + maybeFirstRow fromOnly $ + DB.query + db + "SELECT 1 FROM users WHERE wallet_seed_id = ? AND wallet_account_index = ? AND user_id != ?" + (sId, fromIntegral acct :: Int64, userId) + case (taken :: Maybe Int64) of + Just _ -> pure $ Left "another profile uses this account" + Nothing -> do + bindUser db userId sId (fromIntegral acct) + -- the counter moves past it, so the next profile is not handed the same one + setNextAccountIndex db sId (fromIntegral acct + 1) + pure $ Right () -- | So two profiles cannot be handed the same account. takeAccountIndex :: DB.Connection -> SeedId -> IO Int64