|
5 | 5 | import * as crypto from 'crypto'; |
6 | 6 | import * as nock from 'nock'; |
7 | 7 | import * as should from 'should'; |
| 8 | +import assert = require('assert'); |
8 | 9 |
|
9 | | -import { common } from '@bitgo/sdk-core'; |
| 10 | +import { common, generateGPGKeyPair, encryptAndSignText } from '@bitgo/sdk-core'; |
10 | 11 | import { bip32, ECPair } from '@bitgo/utxo-lib'; |
11 | 12 | import * as _ from 'lodash'; |
12 | 13 | import * as BitGoJS from '../../src/index'; |
@@ -687,4 +688,117 @@ describe('BitGo Prototype Methods', function () { |
687 | 688 | response.user.ecdhKeychain.should.equal('some-xpub'); |
688 | 689 | }); |
689 | 690 | }); |
| 691 | + |
| 692 | + describe('passkey authentication', () => { |
| 693 | + afterEach(function ensureNoPendingMocks() { |
| 694 | + nock.cleanAll(); |
| 695 | + nock.pendingMocks().should.be.empty(); |
| 696 | + }); |
| 697 | + |
| 698 | + it('should authenticate with a passkey', async () => { |
| 699 | + const userId = '123'; |
| 700 | + const passkey = `{"id": "id", "response": {"authenticatorData": "123", "clientDataJSON": "123", "signature": "123", "userHandle": "${userId}"}}`; |
| 701 | + const keyPair = await generateGPGKeyPair('secp256k1'); |
| 702 | + |
| 703 | + nock('https://bitgo.fakeurl') |
| 704 | + .persist() |
| 705 | + .get('/api/v1/client/constants') |
| 706 | + .reply(200, { ttl: 3600, constants: { passkeyBitGoGpgKey: keyPair.publicKey } }); |
| 707 | + |
| 708 | + nock('https://bitgo.fakeurl') |
| 709 | + .post('/api/auth/v1/session') |
| 710 | + .reply(200, async (uri, requestBody) => { |
| 711 | + assert(typeof requestBody === 'object'); |
| 712 | + should.exist(requestBody.publicKey); |
| 713 | + should.exist(requestBody.userId); |
| 714 | + should.exist(requestBody.passkey); |
| 715 | + requestBody.userId.should.equal(userId); |
| 716 | + requestBody.passkey.should.equal(passkey); |
| 717 | + const encryptedToken = (await encryptAndSignText( |
| 718 | + 'access_token', |
| 719 | + requestBody.publicKey, |
| 720 | + keyPair.privateKey |
| 721 | + )) as string; |
| 722 | + |
| 723 | + return { |
| 724 | + encryptedToken: encryptedToken, |
| 725 | + user: { username: '[email protected]' }, |
| 726 | + }; |
| 727 | + }); |
| 728 | + |
| 729 | + const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' }); |
| 730 | + const response = await bitgo.authenticateWithPasskey(passkey); |
| 731 | + should.exist(response.access_token); |
| 732 | + response.access_token.should.equal('access_token'); |
| 733 | + }); |
| 734 | + |
| 735 | + it('should not authenticate with wrong encryption key', async () => { |
| 736 | + const keyPair = await generateGPGKeyPair('secp256k1'); |
| 737 | + |
| 738 | + nock('https://bitgo.fakeurl') |
| 739 | + .persist() |
| 740 | + .get('/api/v1/client/constants') |
| 741 | + .reply(200, { ttl: 3600, constants: { passkeyBitGoGpgKey: keyPair.publicKey } }); |
| 742 | + nock('https://bitgo.fakeurl') |
| 743 | + .post('/api/auth/v1/session') |
| 744 | + .reply(200, async () => { |
| 745 | + const keyPair = await generateGPGKeyPair('secp256k1'); |
| 746 | + const encryptedToken = (await encryptAndSignText( |
| 747 | + 'access_token', |
| 748 | + keyPair.publicKey, |
| 749 | + keyPair.privateKey |
| 750 | + )) as string; |
| 751 | + return { |
| 752 | + encryptedToken: encryptedToken, |
| 753 | + user: { username: '[email protected]' }, |
| 754 | + }; |
| 755 | + }); |
| 756 | + |
| 757 | + const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' }); |
| 758 | + try { |
| 759 | + await bitgo.authenticateWithPasskey( |
| 760 | + '{"id": "id", "response": {"authenticatorData": "123", "clientDataJSON": "123", "signature": "123", "userHandle": "123"}}' |
| 761 | + ); |
| 762 | + assert.fail('Expected error not thrown'); |
| 763 | + } catch (e) { |
| 764 | + assert.equal(e.message, 'Error decrypting message: Session key decryption failed.'); |
| 765 | + } |
| 766 | + }); |
| 767 | + |
| 768 | + it('should not authenticate with wrong signing key', async () => { |
| 769 | + const userId = '123'; |
| 770 | + const passkey = `{"id": "id", "response": {"authenticatorData": "123", "clientDataJSON": "123", "signature": "123", "userHandle": "${userId}"}}`; |
| 771 | + const badKeyPair = await generateGPGKeyPair('secp256k1'); |
| 772 | + const bitgoKeyPair = await generateGPGKeyPair('secp256k1'); |
| 773 | + |
| 774 | + nock('https://bitgo.fakeurl') |
| 775 | + .persist() |
| 776 | + .get('/api/v1/client/constants') |
| 777 | + .reply(200, { ttl: 3600, constants: { passkeyBitGoGpgKey: bitgoKeyPair.publicKey } }); |
| 778 | + |
| 779 | + nock('https://bitgo.fakeurl') |
| 780 | + .post('/api/auth/v1/session') |
| 781 | + .reply(200, async (uri, requestBody) => { |
| 782 | + assert(typeof requestBody === 'object'); |
| 783 | + const encryptedToken = (await encryptAndSignText( |
| 784 | + 'access_token', |
| 785 | + requestBody.publicKey, |
| 786 | + badKeyPair.privateKey |
| 787 | + )) as string; |
| 788 | + |
| 789 | + return { |
| 790 | + encryptedToken: encryptedToken, |
| 791 | + user: { username: '[email protected]' }, |
| 792 | + }; |
| 793 | + }); |
| 794 | + |
| 795 | + const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' }); |
| 796 | + try { |
| 797 | + await bitgo.authenticateWithPasskey(passkey); |
| 798 | + assert.fail('Expected error not thrown'); |
| 799 | + } catch (e) { |
| 800 | + assert(e.message.startsWith('Error decrypting message: Could not find signing key with key ID')); |
| 801 | + } |
| 802 | + }); |
| 803 | + }); |
690 | 804 | }); |
0 commit comments