feat: add script to reencrypt wallet with v2 encryption#9204
Conversation
00ed82a to
567bcdf
Compare
567bcdf to
a30b584
Compare
| console.log(`Wallet: ${wallet.label()} (${walletId})`); | ||
|
|
||
| // Fetch passcodeEncryptionCode — required to decrypt Box D and to generate Box D in the new keycard. | ||
| // Skipped in dry-run if not provided, since no keycard is produced anyway. |
There was a problem hiding this comment.
What's a dry run? Just like a test? Will this be something the client developers already have some frame of reference for, or does this need some kind of explanation somewhere?
There was a problem hiding this comment.
I actually updated this so that it isnt skipped in dry-run when boxD is provided but forgot to push the change 😅
A dry run will attempt the decrypt + reencrypt but wont update anything in the backend. Just a way to test that the passphrase/boxD work properly
a30b584 to
db796c3
Compare
4d55f08 to
ab75384
Compare
There was a problem hiding this comment.
this belongs in a different PR
| // SJCL envelopes have no "v" field or v=1 | ||
| if (!envelope.v || envelope.v === 1) return 1; | ||
| } catch { | ||
| // not JSON |
There was a problem hiding this comment.
don't we want to about this condition?
There was a problem hiding this comment.
updated to throw on unrecognized version
| 'passcoderecovery endpoint did not return a passcodeEncryptionCode — pass --passcodeEncryptionCode manually' | ||
| ); | ||
| } | ||
| return recoveryInfo.passcodeEncryptionCode as string; |
There was a problem hiding this comment.
don't cast, validate
| * jsPDF reads `.src` in Node.js; computeKeyCardImageDimensions reads `.width`/`.height`. | ||
| * This replicates what the BitGo UI does before calling drawKeycard. | ||
| */ | ||
| async function loadKeycardImage(url: string): Promise<HTMLImageElement | undefined> { |
There was a problem hiding this comment.
why are you silently suppressing all errors?
There was a problem hiding this comment.
updated now to log a warning. the image isnt critical for the keycard so it wont throw
| const args = process.argv.slice(2); | ||
| const get = (flag: string): string | undefined => { | ||
| const idx = args.indexOf(flag); | ||
| return idx !== -1 ? args[idx + 1] : undefined; | ||
| }; | ||
|
|
||
| const env = (get('--env') ?? 'test') as EnvironmentName; | ||
| const coin = get('--coin'); | ||
| const walletId = get('--walletId'); | ||
| const passphrase = get('--passphrase'); | ||
| const accessToken = get('--accessToken'); | ||
| const otp = get('--otp'); | ||
| const boxD = get('--boxD'); | ||
| const boxA = get('--boxA'); | ||
| const boxB = get('--boxB'); | ||
| const passcodeEncryptionCode = get('--passcodeEncryptionCode'); | ||
| const dryRun = args.includes('--dry-run'); |
There was a problem hiding this comment.
use a real option parser, I believe we typically use yargs
There was a problem hiding this comment.
updated to use yargs
| // Main | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| async function main() { |
There was a problem hiding this comment.
this is long and complicated enough that it deserves to be in modules/ properly, with a test
maybe even in modules/key-card/
There was a problem hiding this comment.
hmm, i think this script is supposed to only be a temporary measure for users to opt in to v2 wallets until the UI defaults to v2 and provides a way to reencrypt wallets. Not sure if we need to move this to the module, @pranavjain97 what do you think?
There was a problem hiding this comment.
we're going to need this script indefinitely for non-UI users with older wallets so we could potentially move it to the module. however, this can be done as a followup and shouldn't be blocking
ab75384 to
6f43e3f
Compare
06084fd to
ff43ff7
Compare
ff43ff7 to
455f6c1
Compare
pranavjain97
left a comment
There was a problem hiding this comment.
mpcv2 guard added, was able to re-encrypt with reduced mpcv2 keycard
|
@claude give approval rating |
|
Claude finished @pranavjain97's task —— View job Code Review and Approval RatingTasks:
Overall Approval Rating: ⭐⭐⭐⭐⭐ STRONG APPROVEThis is a well-implemented, security-conscious feature that addresses a critical need for wallet encryption upgrades. The code quality is high and previous reviewer feedback has been properly addressed. Strengths🔒 Security Excellence
🛠️ Robust Implementation
📋 Excellent Documentation
Previous Reviewer Feedback Resolution ✅All feedback from @OttoAllmendinger and @pranavjain97 has been properly addressed:
Code Quality AssessmentArchitecture & Design - Excellent
Error Handling - Very Good
Testing & Validation - Good
Documentation - Outstanding
Minor Observations (No Action Required)
Recommendation✅ APPROVE - This PR is ready for merge. It addresses a critical operational need with a well-designed, secure implementation that properly handles edge cases and provides excellent user experience through comprehensive documentation and dry-run capabilities. The code demonstrates BitGo's high security standards and will enable safe migration of wallet encryption for users requiring enhanced security provided by v2 encryption (Argon2id + AES-256-GCM). Run Details:
|
OttoAllmendinger
left a comment
There was a problem hiding this comment.
not going to ACK without module and tests, flushing
OttoAllmendinger
left a comment
There was a problem hiding this comment.
see previous comment
I've moved the business logic to the key card module with tests. still have the script to invoke/e2e sample for clients. @OttoAllmendinger can you review? |
zahin-mohammad
left a comment
There was a problem hiding this comment.
In addition to moving this out, have we done an e2e test with the keycard with
- bitgo ui
- wallet recovery wizard
| @@ -0,0 +1,420 @@ | |||
| /** | |||
There was a problem hiding this comment.
Agree with Otto to a certain degree. This deserves a proper module + tests rather than living only as a one-off script — but I don't think modules/key-card/ is the right home for it.
@bitgo/key-card already owns more than PDF drawing (generateQrData selects/assembles box payloads from keychains, including Box D encryption). That precedent covers "given keychains, produce QR data + PDF."
runUpgradeWalletEncryption goes well beyond that: session unlock, passcoderecovery, decrypt/reencrypt, PUT keychain updates, MPCv2 boxA/boxB recovery, dry-run orchestration. That's wallet/keychain lifecycle logic, not keycard presentation.
Here is a proposed split (take it with a grain of salt):
- put the reencrypt primitives (
getEncryptionVersion, v1→v2 reencrypt) onKeychainsin sdk-core — it already hasupdateSingleKeychainPassword/getEncryptionVersion - keep wallet-scoped orchestration on Wallet (or a thin dedicated helper), with unit tests there
- keep
@bitgo/key-cardas a dependency only for regenerating the PDF from the updated keychains
There was a problem hiding this comment.
Requesting changes here because removing this from this module after-the-fact will either lead to a breaking change or unnecessary tech-debt.
There was a problem hiding this comment.
Agreed, moved the primitives to wallet/keychain sdk-core
Adds a wallet encryption upgrade path from v1 (SJCL / PBKDF2-SHA256 + AES-256-CCM) to v2 (Argon2id + AES-256-GCM), invokable via the SDK and driven by a CLI script. sdk-core: - Keychains.getEncryptionVersion: now public, throws on unknown versions - Keychains.reencryptAsV2: new instance method for v1 to v2 upgrade - Wallet.upgradeEncryption: orchestrates the full flow with an injectable PDF generator for testability key-card: - Exports loadKeycardImage and createKeycardPdfGenerator factory (returns an UpgradeEncryptionPdfGenerator compatible with the wallet method) scripts/upgrade-wallet-encryption.ts: - Thin CLI wrapper around Wallet.upgradeEncryption - Handles MPCv2 (boxA/boxB), boxD-derived passphrase, dry-run, already-unlocked session, and PDF regeneration TICKET: WCN-174
f87bf33 to
364e027
Compare
zahin-mohammad
left a comment
There was a problem hiding this comment.
Logs can be moved in a follow up, along with OFC support. (OFC support can be done via key rotation instead of re-encryption)
| throw new Error( | ||
| 'Failed to decrypt with the provided passphrase. ' + | ||
| 'If the wallet password was changed after creation, provide the original passphrase so the backup key can be decrypted.' | ||
| ); |
There was a problem hiding this comment.
Nothing about this method is specific to the backup key? I am confused by this error message...
| .post(this.bitgo.microservicesUrl('/api/v1/user/unlock')) | ||
| .send({ otp: otp ?? '0000000', duration: 600 }) | ||
| .result(); | ||
| console.log('Session unlocked.'); |
There was a problem hiding this comment.
Nit: don't add console logs into the sdk. If needed, accept a logger as a dependency, and let the caller pass in a logger (i.e. your script).
| const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([ | ||
| keychainsApi.get({ id: keyIds[0] }), | ||
| keychainsApi.get({ id: keyIds[1] }), | ||
| keychainsApi.get({ id: keyIds[2] }), | ||
| ]); |
There was a problem hiding this comment.
What about wallets that do not have 3 keys? (i.e. OFC)
Ticket: WCN-174
This pull request introduces significant enhancements to the wallet encryption upgrade process and improves compatibility for PDF keycard generation in Node.js environments. The main changes include adding a new script to automate wallet keychain re-encryption and updating the keycard drawing utilities to support server-side (Node.js) usage.
Key changes:
1. New Wallet Encryption Upgrade Script
scripts/upgrade-wallet-encryption.ts, a comprehensive CLI tool to upgrade wallet keychains from v1 (SJCL/PBKDF2-SHA256 + AES-256-CCM) to v2 (Argon2id + AES-256-GCM) encryption, with support for regenerating the keycard PDF and handling various edge cases (e.g., changed passphrases, legacy backups).2. Node.js Compatibility for Keycard PDF Generation
drawKeycardand related functions inmodules/key-card/src/drawKeycard.tsto detect Node.js environments and handle image and QR code rendering appropriately (e.g., using base64 data URLs instead of DOM elements). [1] [2] [3]HTMLCanvasElementand base64 string inputs, ensuring PDF generation works in both browser and Node.js contexts. [1] [2]These changes ensure that wallet upgrades and keycard regeneration can be performed reliably in automated or server-side workflows, improving maintainability and operational flexibility.