Skip to content

Commit db2bbce

Browse files
committed
feat(sdk-core): extend upgradeEncryption to support OFC wallets
- handle variable key count (OFC has 2 keys, no backup) - skip PDF when backup/bitgo keychains absent - include pub in user key PUT for OFC server validation Ticket: WAL-1694
1 parent 6c83cf1 commit db2bbce

3 files changed

Lines changed: 82 additions & 11 deletions

File tree

modules/bitgo/test/v2/unit/wallet.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7079,5 +7079,69 @@ describe('V2 Wallet:', function () {
70797079

70807080
await wallet.upgradeEncryption({ passphrase, passcodeEncryptionCode: 'pec-supplied' });
70817081
});
7082+
7083+
describe('OFC wallets (2 keys, no backup)', function () {
7084+
const ofcCoin = bitgo.coin('ofc');
7085+
const ofcUserKeyId = 'ofc00000000000000000000000000001';
7086+
const ofcBitgoKeyId = 'ofc00000000000000000000000000002';
7087+
const ofcWalletData = {
7088+
id: 'ofc0000000000000000000000wallet1',
7089+
coin: 'ofc',
7090+
keys: [ofcUserKeyId, ofcBitgoKeyId],
7091+
coinSpecific: {},
7092+
multisigType: 'onchain' as const,
7093+
type: 'hot' as const,
7094+
};
7095+
const ofcWallet = new Wallet(bitgo, ofcCoin, ofcWalletData as any);
7096+
7097+
function nockOfcKeychain(id: string, keychain: Record<string, unknown>) {
7098+
return nock(bgUrl)
7099+
.get(`/api/v2/ofc/key/${id}`)
7100+
.reply(200, { id, pub: 'pub', type: 'independent', ...keychain });
7101+
}
7102+
7103+
it('re-encrypts the user key and skips bitgo key with no encryptedPrv', async function () {
7104+
const userEnc = await bitgo.encrypt({ input: 'userPrv', password: passphrase, encryptionVersion: 1 });
7105+
7106+
nockUnlock();
7107+
nockOfcKeychain(ofcUserKeyId, { encryptedPrv: userEnc });
7108+
nockOfcKeychain(ofcBitgoKeyId, {});
7109+
7110+
const puts: Array<{ id: string; body: Record<string, unknown> }> = [];
7111+
nock(bgUrl)
7112+
.put(`/api/v2/ofc/key/${ofcUserKeyId}`, (body) => {
7113+
puts.push({ id: ofcUserKeyId, body });
7114+
return true;
7115+
})
7116+
.reply(200, {});
7117+
7118+
const result = await ofcWallet.upgradeEncryption({ passphrase, passcodeEncryptionCode: 'pec' });
7119+
assert.strictEqual(result, undefined);
7120+
puts.should.have.length(1);
7121+
puts[0].id.should.equal(ofcUserKeyId);
7122+
JSON.parse(puts[0].body.encryptedPrv as string).v.should.equal(2);
7123+
});
7124+
7125+
it('skips user key already at v2', async function () {
7126+
const userV2 = await bitgo.encrypt({ input: 'userPrv', password: passphrase, encryptionVersion: 2 });
7127+
7128+
nockUnlock();
7129+
nockOfcKeychain(ofcUserKeyId, { encryptedPrv: userV2 });
7130+
nockOfcKeychain(ofcBitgoKeyId, {});
7131+
7132+
await ofcWallet.upgradeEncryption({ passphrase, passcodeEncryptionCode: 'pec' });
7133+
// No PUTs expected — nock will error if any unexpected call is made.
7134+
});
7135+
7136+
it('makes no PUT calls in dry-run mode', async function () {
7137+
const userEnc = await bitgo.encrypt({ input: 'userPrv', password: passphrase, encryptionVersion: 1 });
7138+
7139+
nockOfcKeychain(ofcUserKeyId, { encryptedPrv: userEnc });
7140+
nockOfcKeychain(ofcBitgoKeyId, {});
7141+
7142+
const result = await ofcWallet.upgradeEncryption({ passphrase, dryRun: true });
7143+
assert.strictEqual(result, undefined);
7144+
});
7145+
});
70827146
});
70837147
});

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3486,10 +3486,11 @@ export class Wallet implements IWallet {
34863486
}
34873487

34883488
const keyIds = this.keyIds();
3489+
const fetchKeychain = (id: string | undefined) => (id ? keychainsApi.get({ id }) : Promise.resolve(undefined));
34893490
const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([
3490-
keychainsApi.get({ id: keyIds[0] }),
3491-
keychainsApi.get({ id: keyIds[1] }),
3492-
keychainsApi.get({ id: keyIds[2] }),
3491+
keychainsApi.get({ id: keyIds[KeyIndices.USER] }),
3492+
fetchKeychain(keyIds[KeyIndices.BACKUP]),
3493+
fetchKeychain(keyIds[KeyIndices.BITGO]),
34933494
]);
34943495

34953496
const updated: Array<{ type: string; id: string }> = [];
@@ -3507,7 +3508,7 @@ export class Wallet implements IWallet {
35073508
if (!dryRun) {
35083509
await this.bitgo
35093510
.put(this.baseCoin.url(`/key/${encodeURIComponent(userKeychain.id)}`))
3510-
.send({ encryptedPrv: newEncryptedPrv, originalEncryptedPrv: newEncryptedPrv })
3511+
.send({ encryptedPrv: newEncryptedPrv, originalEncryptedPrv: newEncryptedPrv, pub: userKeychain.pub })
35113512
.result();
35123513
}
35133514
updated.push({ type: 'user', id: userKeychain.id });
@@ -3519,8 +3520,8 @@ export class Wallet implements IWallet {
35193520
// Re-encrypt backup key. May be encrypted under the original passphrase if the wallet
35203521
// password was changed after creation — fall back to originalPassphrase if current fails.
35213522
// Source: server-stored encryptedPrv (preferred) or boxB for older/keycard-only wallets.
3522-
const serverStored = !!backupKeychain.encryptedPrv;
3523-
const backupSource = backupKeychain.encryptedPrv ?? boxB;
3523+
const serverStored = !!backupKeychain?.encryptedPrv;
3524+
const backupSource = backupKeychain?.encryptedPrv ?? boxB;
35243525
if (backupSource) {
35253526
if (keychainsApi.getEncryptionVersion(backupSource) === 2) {
35263527
skipped.push({ type: 'backup', reason: 'already v2' });
@@ -3531,15 +3532,16 @@ export class Wallet implements IWallet {
35313532
originalPassphrase,
35323533
'backup key'
35333534
);
3534-
backupKeychain.encryptedPrv = newEncryptedPrv;
3535-
if (!dryRun && serverStored) {
3535+
if (backupKeychain) backupKeychain.encryptedPrv = newEncryptedPrv;
3536+
if (!dryRun && serverStored && backupKeychain) {
35363537
// Only PUT if the key was server-stored; boxB-only wallets have no server record.
35373538
await this.bitgo
35383539
.put(this.baseCoin.url(`/key/${encodeURIComponent(backupKeychain.id)}`))
35393540
.send({ encryptedPrv: newEncryptedPrv })
35403541
.result();
35413542
}
3542-
updated.push({ type: serverStored ? 'backup' : 'backup (keycard only)', id: backupKeychain.id });
3543+
const backupId = backupKeychain?.id ?? boxB ?? 'unknown';
3544+
updated.push({ type: serverStored ? 'backup' : 'backup (keycard only)', id: backupId });
35433545
}
35443546
} else {
35453547
skipped.push({ type: 'backup', reason: 'public key only' });
@@ -3573,7 +3575,7 @@ export class Wallet implements IWallet {
35733575
);
35743576
updated.push({ type: 'user reducedEncryptedPrv (keycard only)', id: userKeychain.id });
35753577
}
3576-
if (boxB && backupKeychain.encryptedPrv) {
3578+
if (boxB && backupKeychain?.encryptedPrv) {
35773579
// MPCv2: encryptedPrv exists server-side but reducedEncryptedPrv does not.
35783580
// Re-encrypt boxB so the new keycard uses the reduced form instead of the full blob.
35793581
backupKeychain.reducedEncryptedPrv = await this.reencryptCreationTimeKey(
@@ -3585,7 +3587,7 @@ export class Wallet implements IWallet {
35853587
updated.push({ type: 'backup reducedEncryptedPrv (keycard only)', id: backupKeychain.id });
35863588
}
35873589

3588-
if (!generatePdf) {
3590+
if (!generatePdf || !backupKeychain || !bitgoKeychain) {
35893591
console.log('Done (no PDF generator provided).');
35903592
return undefined;
35913593
}

scripts/upgrade-wallet-encryption.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
* [--passcodeEncryptionCode <code>] \
1717
* [--dry-run]
1818
*
19+
* OFC wallets (--coin ofc):
20+
* Only --passphrase is required. --boxD / --boxA / --boxB do not apply.
21+
* No keycard PDF is produced. The script iterates all key IDs on the wallet
22+
* and re-encrypts any that still use v1 (SJCL) encryption.
23+
*
1924
* --accessToken: Short-lived BitGo access token. Generate this using the following guide
2025
* https://developers.bitgo.com/docs/get-started-access-tokens#1-create-short-lived-access-token
2126
* --boxD: Box D from the original keycard. Required if the wallet passphrase has been changed since the

0 commit comments

Comments
 (0)