Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion modules/express/test/unit/typedRoutes/generateWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,6 @@ describe('Generate Wallet Typed Routes Tests', function () {
const res = await agent.post(`/api/v2/${coin}/wallet/generate`).send({
label,
enterprise,
passphrase: 'test-passphrase',
type: 'trading',
userKeySigningRequired: false,
});
Expand Down
46 changes: 34 additions & 12 deletions modules/sdk-core/src/bitgo/wallet/iWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,20 +295,42 @@ export const GenerateLightningWalletOptionsCodec = t.intersection(
);
export type GenerateLightningWalletOptions = t.TypeOf<typeof GenerateLightningWalletOptionsCodec>;

const GenerateGoAccountWalletBaseOptionsCodec = t.intersection([
t.strict({
label: t.string,
enterprise: t.string,
type: t.literal('trading'),
}),
t.partial({
// Codec intentionally accepts only 2: v1 is the implicit default and never sent on the wire.
encryptionVersion: t.literal(2),
}),
]);

const GenerateGoAccountWalletPasswordOptionsCodec = t.intersection([
t.strict({
passphrase: t.string,
passcodeEncryptionCode: t.string,
}),
t.partial({
userKeySigningRequired: t.boolean,
}),
]);

const GenerateGoAccountWalletPasswordlessOptionsCodec = t.intersection([
t.strict({
userKeySigningRequired: t.literal(false),
}),
t.partial({
passphrase: t.undefined,
passcodeEncryptionCode: t.undefined,
}),
]);

export const GenerateGoAccountWalletOptionsCodec = t.intersection(
[
t.strict({
label: t.string,
passphrase: t.string,
enterprise: t.string,
passcodeEncryptionCode: t.string,
type: t.literal('trading'),
}),
t.partial({
// Codec intentionally accepts only 2: v1 is the implicit default and never sent on the wire.
encryptionVersion: t.literal(2),
userKeySigningRequired: t.boolean,
}),
GenerateGoAccountWalletBaseOptionsCodec,
t.union([GenerateGoAccountWalletPasswordOptionsCodec, GenerateGoAccountWalletPasswordlessOptionsCodec]),
],
'GenerateGoAccountWalletOptions'
);
Expand Down
27 changes: 16 additions & 11 deletions modules/sdk-core/src/bitgo/wallet/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,16 +240,19 @@ export class Wallets implements IWallets {

const keychainParams: AddKeychainOptions = {
pub: keychain.pub,
encryptedPrv: await this.bitgo.encrypt({
password: passphrase,
input: keychain.prv,
encryptionVersion,
}),
originalPasscodeEncryptionCode: passcodeEncryptionCode,
keyType: 'independent',
source: 'user',
};

if (passphrase !== undefined && passcodeEncryptionCode !== undefined) {
keychainParams.encryptedPrv = await this.bitgo.encrypt({
password: passphrase,
input: keychain.prv,
encryptionVersion,
});
keychainParams.originalPasscodeEncryptionCode = passcodeEncryptionCode;
}

const userKeychain = await this.baseCoin.keychains().add(keychainParams);

const walletParams: SupplementGenerateWalletOptions = {
Expand Down Expand Up @@ -351,11 +354,13 @@ export class Wallets implements IWallets {
);

const walletData = await this.generateGoAccountWallet(options);
walletData.encryptedWalletPassphrase = await this.bitgo.encrypt({
input: options.passphrase,
password: options.passcodeEncryptionCode,
encryptionVersion: options.encryptionVersion,
});
if (options.passphrase !== undefined && options.passcodeEncryptionCode !== undefined) {
walletData.encryptedWalletPassphrase = await this.bitgo.encrypt({
input: options.passphrase,
password: options.passcodeEncryptionCode,
encryptionVersion: options.encryptionVersion,
});
}
return walletData;
}

Expand Down
38 changes: 38 additions & 0 deletions modules/sdk-core/test/unit/bitgo/wallet/walletOptionsCodecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,42 @@ describe('wallet options codecs with encryptionVersion', () => {
it('GenerateGoAccountWalletOptionsCodec works without encryptionVersion', () => {
assert.ok(isRight(GenerateGoAccountWalletOptionsCodec.decode(goAccountBase)));
});

it('GenerateGoAccountWalletOptionsCodec accepts password fields when user key signing is disabled', () => {
assert.ok(
isRight(
GenerateGoAccountWalletOptionsCodec.decode({
...goAccountBase,
userKeySigningRequired: false,
})
)
);
});

it('GenerateGoAccountWalletOptionsCodec accepts omitted password fields when user key signing is disabled', () => {
assert.ok(
isRight(
GenerateGoAccountWalletOptionsCodec.decode({
label: 'test',
enterprise: 'ent',
type: 'trading',
userKeySigningRequired: false,
})
)
);
});

it('GenerateGoAccountWalletOptionsCodec rejects a single password field when user key signing is disabled', () => {
assert.ok(
isLeft(
GenerateGoAccountWalletOptionsCodec.decode({
label: 'test',
passphrase: 'pass',
enterprise: 'ent',
type: 'trading',
userKeySigningRequired: false,
})
)
);
});
});
45 changes: 43 additions & 2 deletions modules/sdk-core/test/unit/bitgo/wallet/walletsGoAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('Wallets - GoAccount (OFC trading) wallet creation', function () {
beforeEach(function () {
mockKeychains = {
create: sinon.stub().returns({ pub: userPub, prv: userPrv }),
add: sinon.stub().resolves({ id: 'user-key-id', pub: userPub, encryptedPrv: 'encrypted-prv' }),
add: sinon.stub().callsFake(async (params) => ({ id: 'user-key-id', ...params })),
};

const mockWalletData = { id: 'wallet-id', keys: ['user-key-id'] };
Expand Down Expand Up @@ -56,7 +56,7 @@ describe('Wallets - GoAccount (OFC trading) wallet creation', function () {
});

it('should forward userKeySigningRequired: false in coinSpecific when provided', async function () {
await wallets.generateWallet({
const result = await wallets.generateWallet({
label: 'Test OFC Wallet',
passphrase: 'test-passphrase',
enterprise: 'enterprise-123',
Expand All @@ -69,6 +69,11 @@ describe('Wallets - GoAccount (OFC trading) wallet creation', function () {
const sentParams = postChain.send.firstCall.args[0];
sentParams.should.have.property('coinSpecific');
sentParams.coinSpecific.should.have.property('userKeySigningRequired', false);

const keychainParams = mockKeychains.add.firstCall.args[0];
keychainParams.should.have.property('encryptedPrv', `encrypted:test-passphrase:${userPrv}`);
keychainParams.should.have.property('originalPasscodeEncryptionCode', 'pce-code');
result.should.have.property('encryptedWalletPassphrase', 'encrypted:pce-code:test-passphrase');
});

it('should forward userKeySigningRequired: true in coinSpecific when explicitly set', async function () {
Expand Down Expand Up @@ -97,4 +102,40 @@ describe('Wallets - GoAccount (OFC trading) wallet creation', function () {
const sentParams = postChain.send.firstCall.args[0];
sentParams.should.not.have.property('coinSpecific');
});

it('should generate a public-only user key when user key signing is disabled and password fields are omitted', async function () {
const result = await wallets.generateWallet({
label: 'Passwordless OFC Wallet',
enterprise: 'enterprise-123',
type: 'trading',
userKeySigningRequired: false,
});

assert.ok(mockKeychains.add.calledOnce, 'user key should be uploaded once');
const keychainParams = mockKeychains.add.firstCall.args[0];
keychainParams.should.have.property('pub', userPub);
keychainParams.should.not.have.property('encryptedPrv');
keychainParams.should.not.have.property('originalPasscodeEncryptionCode');

assert.ok(postChain.send.calledOnce, 'POST /wallet/add should be called once');
postChain.send.firstCall.args[0].coinSpecific.should.have.property('userKeySigningRequired', false);
result.should.not.have.property('encryptedWalletPassphrase');
result.should.not.have.property('warning');
});

it('should reject passwordless generation when only one password field is supplied', async function () {
await assert.rejects(
wallets.generateWallet({
label: 'Passwordless OFC Wallet',
enterprise: 'enterprise-123',
type: 'trading',
userKeySigningRequired: false,
passphrase: 'partial-password-input',
}),
/error\(s\) parsing generate go account request params/
);

assert.strictEqual(mockKeychains.add.called, false);
assert.strictEqual(postChain.send.called, false);
});
});
Loading