stop the generic wallet PATCH from being able to replace the seed
the route typed its body as {name, defaultBip, config} and passed the
parsed object straight to updateWallet, which also accepted sealedSeed
for the passphrase change. a TypeScript annotation strips nothing at
runtime, so any authenticated caller could send a sealedSeed key and
overwrite the encrypted seed — no passphrase, no unlock. encryptSecret
encrypts nonsense happily, so the damage would have surfaced at the next
unlock, not at the write.
updateWallet can no longer touch the seed at all; resealing moves to
replaceSealedSeed, whose only caller has already proved knowledge of the
old passphrase. the route rebuilds its patch field by field as well, so
the next field added there cannot re-open it.
verified against the test wallet: the envelope is byte-identical after
the same request that previously would have replaced it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
getSealedSeed,
|
||||
createWallet,
|
||||
updateWallet,
|
||||
replaceSealedSeed,
|
||||
setActiveWallet,
|
||||
deleteWallet,
|
||||
getActiveWallet,
|
||||
@@ -173,7 +174,14 @@ async function handleWallets(ctx: OfficerContext, seg: string[]): Promise<Respon
|
||||
if (name.length > 64) return badRequest('name is too long (64 characters max)');
|
||||
patch.name = name;
|
||||
}
|
||||
const updated = await updateWallet(userId, walletId, patch);
|
||||
// Rebuilt field by field rather than forwarded: `body<T>` is a cast, so the parsed object holds
|
||||
// whatever the caller sent, not what the type says. updateWallet can no longer write the seed
|
||||
// either — that is the belt to this brace.
|
||||
const updated = await updateWallet(userId, walletId, {
|
||||
name: patch.name,
|
||||
defaultBip: patch.defaultBip,
|
||||
config: patch.config,
|
||||
});
|
||||
invalidate(walletId);
|
||||
return updated ? json({ wallet: updated }) : json({ error: 'wallet not found' }, 404);
|
||||
}
|
||||
@@ -520,7 +528,7 @@ async function changePassphraseRoute(ctx: OfficerContext, walletId: number): Pro
|
||||
|
||||
const env = await loadEnvelope(ctx.userId, walletId);
|
||||
const resealed = await changePassphrase(env, oldPassphrase, newPassphrase);
|
||||
await updateWallet(ctx.userId, walletId, { sealedSeed: JSON.stringify(resealed) });
|
||||
await replaceSealedSeed(ctx.userId, walletId, JSON.stringify(resealed));
|
||||
// Force a re-unlock under the new passphrase rather than leaving a session opened by the old one.
|
||||
sessionFor(walletId).lock();
|
||||
return json({ ok: true });
|
||||
|
||||
Reference in New Issue
Block a user