Skip to content

Commit fed5e02

Browse files
authored
Merge pull request #5916 from BitGo/CSI-384
feat(sdk-api): add decryptKeys method for bulk wallet key decryption
2 parents 959fb86 + 5a6e36f commit fed5e02

File tree

4 files changed

+249
-1
lines changed

4 files changed

+249
-1
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;

0 commit comments

Comments
 (0)