Skip to content

Commit 06084fd

Browse files
committed
feat(script): improve upgrade-wallet-encryption robustness
WCN-174 - Fail early for MPCv2 wallets when --boxA/--boxB not provided to avoid generating incomplete keycards - Handle already-unlocked session gracefully instead of crashing - Strip whitespace from box values to handle PDF line-wrapped JSON - Clarify --accessToken must be a session token from login, not a UI OAuth token TICKET: WCN-174
1 parent 6f43e3f commit 06084fd

1 file changed

Lines changed: 27 additions & 9 deletions

File tree

scripts/upgrade-wallet-encryption.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@
88
* --coin tbtc \
99
* --walletId <walletId> \
1010
* --passphrase <walletPassphrase> \
11-
* --accessToken <bearerToken> \
11+
* --accessToken <sessionToken> \
1212
* [--otp <code>] \
1313
* [--boxD <ciphertext>] \
1414
* [--boxA <ciphertext>] \
1515
* [--boxB <ciphertext>] \
1616
* [--passcodeEncryptionCode <code>] \
1717
* [--dry-run]
1818
*
19-
* --accessToken: Short-lived BitGo access token. Generate this using the following guide
20-
* https://developers.bitgo.com/docs/get-started-access-tokens#1-create-short-lived-access-token
19+
* --accessToken: A session token obtained by logging in via the BitGo API (POST /api/v2/user/login).
20+
* Note: UI-generated OAuth access tokens lack the scope for the passcoderecovery endpoint.
21+
* Use a session token from login instead.
2122
* --boxD: Box D from the original keycard. Required if the wallet passphrase has been changed since the
2223
* wallet was created (i.e. the current passphrase is not the original one used at creation time).
2324
* --boxA: Box A from the original keycard. Required for MPCv2 wallets — reducedEncryptedPrv is never stored
@@ -155,11 +156,11 @@ function parseArgs() {
155156
.option('coin', { type: 'string', demandOption: true, description: 'Coin ticker (e.g. tbtc, teth)' })
156157
.option('walletId', { type: 'string', demandOption: true, description: 'Wallet ID' })
157158
.option('passphrase', { type: 'string', demandOption: true, description: 'Current wallet passphrase' })
158-
.option('accessToken', { type: 'string', demandOption: true, description: 'Short-lived BitGo access token' })
159+
.option('accessToken', { type: 'string', demandOption: true, description: 'Session token from POST /api/v2/user/login' })
159160
.option('otp', { type: 'string', description: 'OTP for session unlock' })
160-
.option('boxD', { type: 'string', description: 'Box D ciphertext from the original keycard' })
161-
.option('boxA', { type: 'string', description: 'Box A ciphertext from the original keycard (MPCv2)' })
162-
.option('boxB', { type: 'string', description: 'Box B ciphertext from the original keycard' })
161+
.option('boxD', { type: 'string', description: 'Box D ciphertext from the original keycard', coerce: (v: string) => v?.replace(/\s/g, '') })
162+
.option('boxA', { type: 'string', description: 'Box A ciphertext from the original keycard (MPCv2)', coerce: (v: string) => v?.replace(/\s/g, '') })
163+
.option('boxB', { type: 'string', description: 'Box B ciphertext from the original keycard', coerce: (v: string) => v?.replace(/\s/g, '') })
163164
.option('passcodeEncryptionCode', {
164165
type: 'string',
165166
description: 'Passcode encryption code (fetched automatically if omitted)',
@@ -204,16 +205,33 @@ async function main() {
204205
if (dryRun) console.log('[dry-run] No changes will be persisted.');
205206

206207
const bitgo = new BitGo({ env });
208+
207209
bitgo.authenticateWithAccessToken({ accessToken });
208210

209211
if (!dryRun) {
210-
await bitgo.unlock({ otp: otp ?? '0000000', duration: 600 });
211-
console.log('Session unlocked.');
212+
try {
213+
await bitgo.unlock({ otp: otp ?? '0000000', duration: 600 });
214+
console.log('Session unlocked.');
215+
} catch (err: unknown) {
216+
const msg = err instanceof Error ? err.message : String(err);
217+
if (msg.includes('already unlocked longer')) {
218+
console.log('Session already unlocked (longer duration) — proceeding.');
219+
} else {
220+
throw err;
221+
}
222+
}
212223
}
213224

214225
const wallet = await bitgo.coin(coin).wallets().get({ id: walletId });
215226
console.log(`Wallet: ${wallet.label()} (${walletId})`);
216227

228+
if (wallet.multisigTypeVersion() === 'MPCv2' && (!boxA || !boxB)) {
229+
throw new Error(
230+
'This is an MPCv2 wallet. --boxA and --boxB are required to re-encrypt the reducedEncryptedPrv ' +
231+
'key shares stored on the keycard. Without them the new keycard will be missing Box A and Box B.'
232+
);
233+
}
234+
217235
// Fetch passcodeEncryptionCode — needed to decrypt Box D (when provided) and to generate Box D
218236
// in the new keycard. Always fetched when boxD is provided; otherwise skipped in dry-run.
219237
const needsPec = boxD || !dryRun;

0 commit comments

Comments
 (0)