Skip to content

Commit 5a6e36f

Browse files
committed
feat(sdk-api): add decryptKeys method for bulk wallet key decryption
TICKET: CSI-384
1 parent 60bb013 commit 5a6e36f

File tree

5 files changed

+265
-2
lines changed

5 files changed

+265
-2
lines changed

modules/sdk-api/src/bitgoAPI.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
BitGoRequest,
77
CoinConstructor,
88
common,
9+
DecryptKeysOptions,
910
DecryptOptions,
1011
defaultConstants,
1112
EcdhDerivedKeypair,
@@ -622,6 +623,57 @@ export class BitGoAPI implements BitGoBase {
622623
}
623624
}
624625

626+
/**
627+
* Attempt to decrypt multiple wallet keys with the provided passphrase
628+
* @param {DecryptKeysOptions} params - Parameters object containing wallet key pairs and password
629+
* @param {Array<{walletId: string, encryptedPrv: string}>} params.walletIdEncryptedKeyPairs - Array of wallet ID and encrypted private key pairs
630+
* @param {string} params.password - The passphrase to attempt decryption with
631+
* @returns {string[]} - Array of wallet IDs for which decryption failed
632+
*/
633+
decryptKeys(params: DecryptKeysOptions): string[] {
634+
params = params || {};
635+
if (!params.walletIdEncryptedKeyPairs) {
636+
throw new Error('Missing parameter: walletIdEncryptedKeyPairs');
637+
}
638+
639+
if (!params.password) {
640+
throw new Error('Missing parameter: password');
641+
}
642+
643+
if (!Array.isArray(params.walletIdEncryptedKeyPairs)) {
644+
throw new Error('walletIdEncryptedKeyPairs must be an array');
645+
}
646+
647+
if (params.walletIdEncryptedKeyPairs.length === 0) {
648+
return [];
649+
}
650+
651+
const failedWalletIds: string[] = [];
652+
653+
for (const keyPair of params.walletIdEncryptedKeyPairs) {
654+
if (!keyPair.walletId || typeof keyPair.walletId !== 'string') {
655+
throw new Error('each key pair must have a string walletId');
656+
}
657+
658+
if (!keyPair.encryptedPrv || typeof keyPair.encryptedPrv !== 'string') {
659+
throw new Error('each key pair must have a string encryptedPrv');
660+
}
661+
662+
try {
663+
this.decrypt({
664+
input: keyPair.encryptedPrv,
665+
password: params.password,
666+
});
667+
// If no error was thrown, decryption was successful
668+
} catch (error) {
669+
// If decryption fails, add the walletId to the failed list
670+
failedWalletIds.push(keyPair.walletId);
671+
}
672+
}
673+
674+
return failedWalletIds;
675+
}
676+
625677
/**
626678
* Serialize this BitGo object to a JSON object.
627679
*

modules/sdk-api/test/unit/bitgoAPI.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'should';
22
import { BitGoAPI } from '../../src/bitgoAPI';
33
import { ProxyAgent } from 'proxy-agent';
4+
import * as sinon from 'sinon';
45

56
describe('Constructor', function () {
67
describe('cookiesPropagationEnabled argument', function () {
@@ -125,4 +126,183 @@ describe('Constructor', function () {
125126
result.should.equal(expectedUrl);
126127
});
127128
});
129+
130+
describe('decryptKeys', function () {
131+
let bitgo: BitGoAPI;
132+
133+
beforeEach(function () {
134+
bitgo = new BitGoAPI({
135+
env: 'test',
136+
});
137+
});
138+
139+
afterEach(function () {
140+
sinon.restore();
141+
});
142+
143+
it('should throw if no params are provided', function () {
144+
try {
145+
// @ts-expect-error - intentionally calling with no params for test
146+
bitgo.decryptKeys();
147+
throw new Error('Expected error but got none');
148+
} catch (e) {
149+
e.message.should.containEql('Missing parameter');
150+
}
151+
});
152+
153+
it('should throw if walletIdEncryptedKeyPairs is missing', function () {
154+
try {
155+
// @ts-expect-error - intentionally missing required param
156+
bitgo.decryptKeys({ password: 'password123' });
157+
throw new Error('Expected error but got none');
158+
} catch (e) {
159+
e.message.should.containEql('Missing parameter: walletIdEncryptedKeyPairs');
160+
}
161+
});
162+
163+
it('should throw if password is missing', function () {
164+
try {
165+
// @ts-expect-error - intentionally missing required param
166+
bitgo.decryptKeys({ walletIdEncryptedKeyPairs: [] });
167+
throw new Error('Expected error but got none');
168+
} catch (e) {
169+
e.message.should.containEql('Missing parameter: password');
170+
}
171+
});
172+
173+
it('should throw if walletIdEncryptedKeyPairs is not an array', function () {
174+
try {
175+
// @ts-expect-error - intentionally providing wrong type
176+
bitgo.decryptKeys({ walletIdEncryptedKeyPairs: 'not an array', password: 'password123' });
177+
throw new Error('Expected error but got none');
178+
} catch (e) {
179+
e.message.should.equal('walletIdEncryptedKeyPairs must be an array');
180+
}
181+
});
182+
183+
it('should return empty array for empty walletIdEncryptedKeyPairs', function () {
184+
const result = bitgo.decryptKeys({ walletIdEncryptedKeyPairs: [], password: 'password123' });
185+
result.should.be.an.Array();
186+
result.should.be.empty();
187+
});
188+
189+
it('should throw if any walletId is missing or not a string', function () {
190+
try {
191+
bitgo.decryptKeys({
192+
walletIdEncryptedKeyPairs: [
193+
// @ts-expect-error - intentionally missing walletId
194+
{
195+
encryptedPrv: 'encrypted-data',
196+
},
197+
],
198+
password: 'password123',
199+
});
200+
throw new Error('Expected error but got none');
201+
} catch (e) {
202+
e.message.should.equal('each key pair must have a string walletId');
203+
}
204+
205+
try {
206+
bitgo.decryptKeys({
207+
walletIdEncryptedKeyPairs: [
208+
{
209+
// @ts-expect-error - intentionally providing wrong type
210+
walletId: 123,
211+
encryptedPrv: 'encrypted-data',
212+
},
213+
],
214+
password: 'password123',
215+
});
216+
throw new Error('Expected error but got none');
217+
} catch (e) {
218+
e.message.should.equal('each key pair must have a string walletId');
219+
}
220+
});
221+
222+
it('should throw if any encryptedPrv is missing or not a string', function () {
223+
try {
224+
bitgo.decryptKeys({
225+
walletIdEncryptedKeyPairs: [
226+
// @ts-expect-error - intentionally missing encryptedPrv
227+
{
228+
walletId: 'wallet-id-1',
229+
},
230+
],
231+
password: 'password123',
232+
});
233+
throw new Error('Expected error but got none');
234+
} catch (e) {
235+
e.message.should.equal('each key pair must have a string encryptedPrv');
236+
}
237+
238+
try {
239+
bitgo.decryptKeys({
240+
walletIdEncryptedKeyPairs: [
241+
{
242+
walletId: 'wallet-id-1',
243+
// @ts-expect-error - intentionally providing wrong type
244+
encryptedPrv: 123,
245+
},
246+
],
247+
password: 'password123',
248+
});
249+
throw new Error('Expected error but got none');
250+
} catch (e) {
251+
e.message.should.equal('each key pair must have a string encryptedPrv');
252+
}
253+
});
254+
255+
it('should return walletIds of keys that failed to decrypt', function () {
256+
// Create a stub for the decrypt method
257+
const decryptStub = sinon.stub(bitgo, 'decrypt');
258+
259+
// Make it succeed for first wallet and fail for second wallet
260+
decryptStub.onFirstCall().returns('decrypted-key-1');
261+
decryptStub.onSecondCall().throws(new Error('decryption failed'));
262+
263+
const result = bitgo.decryptKeys({
264+
walletIdEncryptedKeyPairs: [
265+
{ walletId: 'wallet-id-1', encryptedPrv: 'encrypted-data-1' },
266+
{ walletId: 'wallet-id-2', encryptedPrv: 'encrypted-data-2' },
267+
],
268+
password: 'password123',
269+
});
270+
271+
result.should.be.an.Array();
272+
result.should.have.length(1);
273+
result[0].should.equal('wallet-id-2');
274+
});
275+
276+
it('should correctly process multiple wallet keys', function () {
277+
// Create a spy on the decrypt method
278+
const decryptStub = sinon.stub(bitgo, 'decrypt');
279+
280+
// Configure the stub to throw for specific wallets
281+
decryptStub
282+
.withArgs({ input: 'encrypted-data-2', password: 'password123' })
283+
.throws(new Error('decryption failed'));
284+
decryptStub
285+
.withArgs({ input: 'encrypted-data-4', password: 'password123' })
286+
.throws(new Error('decryption failed'));
287+
decryptStub.returns('success'); // Default return for other calls
288+
289+
const result = bitgo.decryptKeys({
290+
walletIdEncryptedKeyPairs: [
291+
{ walletId: 'wallet-id-1', encryptedPrv: 'encrypted-data-1' },
292+
{ walletId: 'wallet-id-2', encryptedPrv: 'encrypted-data-2' },
293+
{ walletId: 'wallet-id-3', encryptedPrv: 'encrypted-data-3' },
294+
{ walletId: 'wallet-id-4', encryptedPrv: 'encrypted-data-4' },
295+
],
296+
password: 'password123',
297+
});
298+
299+
// Should be called once for each wallet
300+
decryptStub.callCount.should.equal(4);
301+
302+
// Should include only the failed wallet IDs
303+
result.should.be.an.Array();
304+
result.should.have.length(2);
305+
result.should.containDeep(['wallet-id-2', 'wallet-id-4']);
306+
});
307+
});
128308
});

modules/sdk-core/src/api/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export interface DecryptOptions {
99
password?: string;
1010
}
1111

12+
export interface DecryptKeysOptions {
13+
walletIdEncryptedKeyPairs: Array<{
14+
walletId: string;
15+
encryptedPrv: string;
16+
}>;
17+
password: string;
18+
}
19+
1220
export interface EncryptOptions {
1321
input: string;
1422
password?: string;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { BitGoRequest, DecryptOptions, EncryptOptions, GetSharingKeyOptions, IRequestTracer } from '../api';
1+
import {
2+
BitGoRequest,
3+
DecryptKeysOptions,
4+
DecryptOptions,
5+
EncryptOptions,
6+
GetSharingKeyOptions,
7+
IRequestTracer,
8+
} from '../api';
29
import { IBaseCoin } from './baseCoin';
310
import { CoinConstructor } from './coinFactory';
411
import { EnvironmentName } from './environments';
@@ -8,6 +15,7 @@ export interface BitGoBase {
815
wallets(): any; // TODO - define v1 wallets type
916
coin(coinName: string): IBaseCoin; // need to change it to BaseCoin once it's moved to @bitgo/sdk-core
1017
decrypt(params: DecryptOptions): string;
18+
decryptKeys(params: DecryptKeysOptions): string[];
1119
del(url: string): BitGoRequest;
1220
encrypt(params: EncryptOptions): string;
1321
readonly env: EnvironmentName;

yarn.lock

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11304,7 +11304,7 @@ formidable@^1.1.1, formidable@^1.2.0:
1130411304
resolved "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
1130511305
integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==
1130611306

11307-
formidable@^3.5.1:
11307+
formidable@^3.5.1, formidable@^3.5.2:
1130811308
version "3.5.2"
1130911309
resolved "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz#207c33fecdecb22044c82ba59d0c63a12fb81d77"
1131011310
integrity sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==
@@ -18986,6 +18986,21 @@ [email protected]:
1898618986
qs "^6.5.1"
1898718987
readable-stream "^2.0.5"
1898818988

18989+
superagent@^10.1.1:
18990+
version "10.2.0"
18991+
resolved "https://registry.npmjs.org/superagent/-/superagent-10.2.0.tgz#28f262a0a56433e0df53db978ceaa51f23e762ab"
18992+
integrity sha512-IKeoGox6oG9zyDeizaezkJ2/aK0wc5la9st7WsAKyrAkfJ56W3whVbVtF68k6wuc87/y9T85NyON5FLz7Mrzzw==
18993+
dependencies:
18994+
component-emitter "^1.3.0"
18995+
cookiejar "^2.1.4"
18996+
debug "^4.3.4"
18997+
fast-safe-stringify "^2.1.1"
18998+
form-data "^4.0.0"
18999+
formidable "^3.5.2"
19000+
methods "^1.1.2"
19001+
mime "2.6.0"
19002+
qs "^6.11.0"
19003+
1898919004
superagent@^3.8.3:
1899019005
version "3.8.3"
1899119006
resolved "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128"

0 commit comments

Comments
 (0)