Skip to content

Commit ff43ff7

Browse files
committed
feat(scripts): 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 TICKET: WCN-174
1 parent 6f43e3f commit ff43ff7

1 file changed

Lines changed: 23 additions & 6 deletions

File tree

scripts/upgrade-wallet-encryption.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* --coin tbtc \
99
* --walletId <walletId> \
1010
* --passphrase <walletPassphrase> \
11-
* --accessToken <bearerToken> \
11+
* --accessToken <sessionToken> \
1212
* [--otp <code>] \
1313
* [--boxD <ciphertext>] \
1414
* [--boxA <ciphertext>] \
@@ -157,9 +157,9 @@ function parseArgs() {
157157
.option('passphrase', { type: 'string', demandOption: true, description: 'Current wallet passphrase' })
158158
.option('accessToken', { type: 'string', demandOption: true, description: 'Short-lived BitGo access token' })
159159
.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' })
160+
.option('boxD', { type: 'string', description: 'Box D ciphertext from the original keycard', coerce: (v: string) => v?.replace(/\s/g, '') })
161+
.option('boxA', { type: 'string', description: 'Box A ciphertext from the original keycard (MPCv2)', coerce: (v: string) => v?.replace(/\s/g, '') })
162+
.option('boxB', { type: 'string', description: 'Box B ciphertext from the original keycard', coerce: (v: string) => v?.replace(/\s/g, '') })
163163
.option('passcodeEncryptionCode', {
164164
type: 'string',
165165
description: 'Passcode encryption code (fetched automatically if omitted)',
@@ -204,16 +204,33 @@ async function main() {
204204
if (dryRun) console.log('[dry-run] No changes will be persisted.');
205205

206206
const bitgo = new BitGo({ env });
207+
207208
bitgo.authenticateWithAccessToken({ accessToken });
208209

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

214224
const wallet = await bitgo.coin(coin).wallets().get({ id: walletId });
215225
console.log(`Wallet: ${wallet.label()} (${walletId})`);
216226

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

0 commit comments

Comments
 (0)