From 23d229781f54c59ef8a5e93212ead006cfb5f50d Mon Sep 17 00:00:00 2001 From: ozsay Date: Fri, 6 Sep 2019 10:45:27 +0300 Subject: [PATCH 1/4] work --- packages/asymmetric/README.md | 30 ++++++++++ .../__tests__/accounts-asymmetric.ts | 48 ++++++++++++++++ packages/asymmetric/package.json | 34 ++++++++++++ .../asymmetric/src/accounts-asymmetric.ts | 55 +++++++++++++++++++ packages/asymmetric/src/index.ts | 4 ++ .../src/types/asymmetric-login-type.ts | 7 +++ packages/asymmetric/src/types/index.ts | 2 + .../asymmetric/src/types/public-key-type.ts | 6 ++ packages/asymmetric/tsconfig.json | 9 +++ 9 files changed, 195 insertions(+) create mode 100644 packages/asymmetric/README.md create mode 100644 packages/asymmetric/__tests__/accounts-asymmetric.ts create mode 100644 packages/asymmetric/package.json create mode 100644 packages/asymmetric/src/accounts-asymmetric.ts create mode 100644 packages/asymmetric/src/index.ts create mode 100644 packages/asymmetric/src/types/asymmetric-login-type.ts create mode 100644 packages/asymmetric/src/types/index.ts create mode 100644 packages/asymmetric/src/types/public-key-type.ts create mode 100644 packages/asymmetric/tsconfig.json diff --git a/packages/asymmetric/README.md b/packages/asymmetric/README.md new file mode 100644 index 000000000..51c435bf9 --- /dev/null +++ b/packages/asymmetric/README.md @@ -0,0 +1,30 @@ +# @accounts/password + +[![npm](https://img.shields.io/npm/v/@accounts/password.svg?maxAge=2592000)](https://www.npmjs.com/package/@accounts/password) +![MIT License](https://img.shields.io/badge/license-MIT-blue.svg) + +## Install + +``` +yarn add @accounts/password +``` + +## Usage + +```js +import { AccountsServer } from '@accounts/server'; +import { AccountsPassword } from '@accounts/password'; + +export const accountsPassword = new AccountsPassword({ + // options +}); + +const accountsServer = new AccountsServer( + { + // options + }, + { + password: accountsPassword, + } +); +``` diff --git a/packages/asymmetric/__tests__/accounts-asymmetric.ts b/packages/asymmetric/__tests__/accounts-asymmetric.ts new file mode 100644 index 000000000..ce179e7be --- /dev/null +++ b/packages/asymmetric/__tests__/accounts-asymmetric.ts @@ -0,0 +1,48 @@ +import * as crypto from 'crypto'; + +import { PublicKeyType } from '../src/types'; +import { AccountsAsymmetric } from '../src'; + +describe('AccountsAsymmetric', () => { + it('a', async () => { + const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'sect239k1', + }); + const payload = 'some data to sign'; + + const sign = crypto.createSign('sha512'); + sign.write(payload); + sign.end(); + const signature = sign.sign(privateKey, 'hex'); + + const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); + + const service = new AccountsAsymmetric(); + + const publicKeyParams: PublicKeyType = { + key: publicKeyStr, + encoding: 'base64', + format: 'der', + type: 'spki', + }; + + const user = { + id: '123', + services: { + [service.serviceName]: publicKeyParams, + }, + }; + const findUserByServiceId = jest.fn(() => Promise.resolve(user)); + service.setStore({ findUserByServiceId } as any); + + const userFromService = await service.authenticate({ + signature, + payload, + publicKey: publicKeyStr, + signatureAlgorithm: 'sha512', + signatureFormat: 'hex', + }); + + expect(userFromService).toEqual(user); + }); +}); diff --git a/packages/asymmetric/package.json b/packages/asymmetric/package.json new file mode 100644 index 000000000..40284605f --- /dev/null +++ b/packages/asymmetric/package.json @@ -0,0 +1,34 @@ +{ + "name": "@accounts/asymmetric", + "version": "0.19.0", + "license": "MIT", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "clean": "rimraf lib", + "start": "tsc --watch", + "precompile": "yarn clean", + "compile": "tsc", + "prepublishOnly": "yarn compile", + "testonly": "jest", + "coverage": "jest --coverage" + }, + "jest": { + "testEnvironment": "node", + "preset": "ts-jest" + }, + "dependencies": { + "@accounts/types": "^0.19.0", + "tslib": "1.10.0" + }, + "devDependencies": { + "@accounts/server": "^0.19.0", + "@types/jest": "24.0.18", + "@types/node": "12.7.2", + "jest": "24.9.0", + "rimraf": "3.0.0" + }, + "peerDependencies": { + "@accounts/server": "^0.7.0" + } +} diff --git a/packages/asymmetric/src/accounts-asymmetric.ts b/packages/asymmetric/src/accounts-asymmetric.ts new file mode 100644 index 000000000..aaf3c7905 --- /dev/null +++ b/packages/asymmetric/src/accounts-asymmetric.ts @@ -0,0 +1,55 @@ +import { AuthenticationService, DatabaseInterface } from '@accounts/types'; +import { AccountsServer } from '@accounts/server'; +import * as crypto from 'crypto'; + +import { AsymmetricLoginType, PublicKeyType } from './types'; + +export default class AsymmetricService implements AuthenticationService { + public serviceName = 'asymmetric'; + public server!: AccountsServer; + private db!: DatabaseInterface; + + public setStore(store: DatabaseInterface) { + this.db = store; + } + + public async updatePublicKey( + userId: string, + { key, ...params }: PublicKeyType + ): Promise { + try { + await this.db.setService(userId, this.serviceName, { id: key, key, ...params }); + return true; + } catch (e) { + return false; + } + } + + public async authenticate(params: AsymmetricLoginType): Promise { + const user = await this.db.findUserByServiceId(this.serviceName, params.publicKey); + + if (!user) { + throw new Error(); + } + + try { + const verify = crypto.createVerify(params.signatureAlgorithm); + verify.write(params.payload); + verify.end(); + + const publicKeyParams: PublicKeyType = (user.services as any)[this.serviceName]; + + const publicKey = crypto.createPublicKey({ + key: Buffer.from(publicKeyParams.key, publicKeyParams.encoding), + format: publicKeyParams.format, + type: publicKeyParams.type, + }); + + const isVerified = verify.verify(publicKey, params.signature, params.signatureFormat); + + return isVerified ? user : null; + } catch (e) { + throw new Error(); + } + } +} diff --git a/packages/asymmetric/src/index.ts b/packages/asymmetric/src/index.ts new file mode 100644 index 000000000..f4c54211d --- /dev/null +++ b/packages/asymmetric/src/index.ts @@ -0,0 +1,4 @@ +import AccountsAsymmetric from './accounts-asymmetric'; + +export default AccountsAsymmetric; +export { AccountsAsymmetric }; diff --git a/packages/asymmetric/src/types/asymmetric-login-type.ts b/packages/asymmetric/src/types/asymmetric-login-type.ts new file mode 100644 index 000000000..1f005ccc4 --- /dev/null +++ b/packages/asymmetric/src/types/asymmetric-login-type.ts @@ -0,0 +1,7 @@ +export interface AsymmetricLoginType { + publicKey: string; + signature: string; + signatureAlgorithm: 'sha256' | 'sha512'; + signatureFormat: 'hex' | 'base64'; + payload: string; +} diff --git a/packages/asymmetric/src/types/index.ts b/packages/asymmetric/src/types/index.ts new file mode 100644 index 000000000..a03beb450 --- /dev/null +++ b/packages/asymmetric/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './asymmetric-login-type'; +export * from './public-key-type'; diff --git a/packages/asymmetric/src/types/public-key-type.ts b/packages/asymmetric/src/types/public-key-type.ts new file mode 100644 index 000000000..8a6ebd224 --- /dev/null +++ b/packages/asymmetric/src/types/public-key-type.ts @@ -0,0 +1,6 @@ +export interface PublicKeyType { + key: string; + encoding: 'utf8' | 'base64' | 'hex'; + format: 'pem' | 'der'; + type: 'pkcs1' | 'spki'; +} diff --git a/packages/asymmetric/tsconfig.json b/packages/asymmetric/tsconfig.json new file mode 100644 index 000000000..4ec56d0f8 --- /dev/null +++ b/packages/asymmetric/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "importHelpers": true + }, + "exclude": ["node_modules", "__tests__", "lib"] +} From 5b1d308d9996035c6881cfd0177f982b05c83f8a Mon Sep 17 00:00:00 2001 From: ozsay Date: Mon, 23 Sep 2019 16:44:47 +0300 Subject: [PATCH 2/4] more work --- .../__snapshots__/accounts-asymmetric.ts.snap | 5 + .../__tests__/accounts-asymmetric.ts | 137 +++++++++++++++++- packages/asymmetric/package.json | 5 +- .../asymmetric/src/accounts-asymmetric.ts | 22 ++- packages/asymmetric/src/errors.ts | 6 + packages/asymmetric/src/index.ts | 1 + .../asymmetric/src/types/error-messages.ts | 4 + packages/asymmetric/src/types/index.ts | 1 + 8 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 packages/asymmetric/__tests__/__snapshots__/accounts-asymmetric.ts.snap create mode 100644 packages/asymmetric/src/errors.ts create mode 100644 packages/asymmetric/src/types/error-messages.ts diff --git a/packages/asymmetric/__tests__/__snapshots__/accounts-asymmetric.ts.snap b/packages/asymmetric/__tests__/__snapshots__/accounts-asymmetric.ts.snap new file mode 100644 index 000000000..6bdbc920c --- /dev/null +++ b/packages/asymmetric/__tests__/__snapshots__/accounts-asymmetric.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountsAsymmetric throws error when the user is not found by public key 1`] = `[Error: User not found]`; + +exports[`AccountsAsymmetric throws error when the verification process fails 1`] = `[Error: Failed to verify signature]`; diff --git a/packages/asymmetric/__tests__/accounts-asymmetric.ts b/packages/asymmetric/__tests__/accounts-asymmetric.ts index ce179e7be..62d84a8d0 100644 --- a/packages/asymmetric/__tests__/accounts-asymmetric.ts +++ b/packages/asymmetric/__tests__/accounts-asymmetric.ts @@ -4,7 +4,142 @@ import { PublicKeyType } from '../src/types'; import { AccountsAsymmetric } from '../src'; describe('AccountsAsymmetric', () => { - it('a', async () => { + it('should update the public key', async () => { + const { publicKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'sect239k1', + }); + const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); + + const service = new AccountsAsymmetric(); + + const setService = jest.fn(() => Promise.resolve(null)); + service.setStore({ setService } as any); + + const publicKeyParams: PublicKeyType = { + key: publicKeyStr, + encoding: 'base64', + format: 'pem', + type: 'spki', + }; + + const res = await service.updatePublicKey('123', publicKeyParams); + + expect(res).toBeTruthy(); + expect(setService).toHaveBeenCalledWith('123', service.serviceName, { + id: publicKeyStr, + ...publicKeyParams, + }); + }); + + it('throws error when the user is not found by public key', async () => { + const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'sect239k1', + }); + const payload = 'some data to sign'; + + const sign = crypto.createSign('sha512'); + sign.write(payload); + sign.end(); + const signature = sign.sign(privateKey, 'hex'); + + const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); + + const service = new AccountsAsymmetric(); + + const findUserByServiceId = jest.fn(() => Promise.resolve(null)); + service.setStore({ findUserByServiceId } as any); + + await expect( + service.authenticate({ + signature, + payload, + publicKey: publicKeyStr, + signatureAlgorithm: 'sha512', + signatureFormat: 'hex', + }) + ).rejects.toMatchSnapshot(); + }); + + it('throws error when the verification process fails', async () => { + const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'sect239k1', + }); + const payload = 'some data to sign'; + + const sign = crypto.createSign('sha512'); + sign.write(payload); + sign.end(); + const signature = sign.sign(privateKey, 'hex'); + + const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); + + const service = new AccountsAsymmetric(); + + const publicKeyParams: PublicKeyType = { + key: publicKeyStr, + encoding: 'base64', + format: 'pem', + type: 'spki', + }; + + const user = { + id: '123', + services: { + [service.serviceName]: publicKeyParams, + }, + }; + const findUserByServiceId = jest.fn(() => Promise.resolve(user)); + service.setStore({ findUserByServiceId } as any); + + await expect( + service.authenticate({ + signature, + payload, + publicKey: publicKeyStr, + signatureAlgorithm: 'sha512', + signatureFormat: 'hex', + }) + ).rejects.toMatchSnapshot(); + }); + + it('should return null when signature is invalid', async () => { + const { publicKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'sect239k1', + }); + const payload = 'some data to sign'; + + const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); + + const service = new AccountsAsymmetric(); + + const publicKeyParams: PublicKeyType = { + key: publicKeyStr, + encoding: 'base64', + format: 'der', + type: 'spki', + }; + + const user = { + id: '123', + services: { + [service.serviceName]: publicKeyParams, + }, + }; + const findUserByServiceId = jest.fn(() => Promise.resolve(user)); + service.setStore({ findUserByServiceId } as any); + + const userFromService = await service.authenticate({ + signature: 'some signature', + payload, + publicKey: publicKeyStr, + signatureAlgorithm: 'sha512', + signatureFormat: 'hex', + }); + + expect(userFromService).toBeNull(); + }); + + it('should return user when verification is successful', async () => { const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'sect239k1', }); diff --git a/packages/asymmetric/package.json b/packages/asymmetric/package.json index 40284605f..e8d4e7a53 100644 --- a/packages/asymmetric/package.json +++ b/packages/asymmetric/package.json @@ -22,13 +22,12 @@ "tslib": "1.10.0" }, "devDependencies": { - "@accounts/server": "^0.19.0", "@types/jest": "24.0.18", - "@types/node": "12.7.2", + "@types/node": "12.7.4", "jest": "24.9.0", "rimraf": "3.0.0" }, "peerDependencies": { - "@accounts/server": "^0.7.0" + "@accounts/server": "^0.19.0" } } diff --git a/packages/asymmetric/src/accounts-asymmetric.ts b/packages/asymmetric/src/accounts-asymmetric.ts index aaf3c7905..ba6c09f7f 100644 --- a/packages/asymmetric/src/accounts-asymmetric.ts +++ b/packages/asymmetric/src/accounts-asymmetric.ts @@ -2,12 +2,26 @@ import { AuthenticationService, DatabaseInterface } from '@accounts/types'; import { AccountsServer } from '@accounts/server'; import * as crypto from 'crypto'; -import { AsymmetricLoginType, PublicKeyType } from './types'; +import { AsymmetricLoginType, PublicKeyType, ErrorMessages } from './types'; +import { errors } from './errors'; -export default class AsymmetricService implements AuthenticationService { +export interface AccountsAsymmetricOptions { + errors?: ErrorMessages; +} + +const defaultOptions = { + errors, +}; + +export default class AccountsAsymmetric implements AuthenticationService { public serviceName = 'asymmetric'; public server!: AccountsServer; private db!: DatabaseInterface; + private options: AccountsAsymmetricOptions & typeof defaultOptions; + + constructor(options: AccountsAsymmetricOptions = {}) { + this.options = { ...defaultOptions, ...options }; + } public setStore(store: DatabaseInterface) { this.db = store; @@ -29,7 +43,7 @@ export default class AsymmetricService implements AuthenticationService { const user = await this.db.findUserByServiceId(this.serviceName, params.publicKey); if (!user) { - throw new Error(); + throw new Error(this.options.errors.userNotFound); } try { @@ -49,7 +63,7 @@ export default class AsymmetricService implements AuthenticationService { return isVerified ? user : null; } catch (e) { - throw new Error(); + throw new Error(this.options.errors.verificationFailed); } } } diff --git a/packages/asymmetric/src/errors.ts b/packages/asymmetric/src/errors.ts new file mode 100644 index 000000000..508e015f6 --- /dev/null +++ b/packages/asymmetric/src/errors.ts @@ -0,0 +1,6 @@ +import { ErrorMessages } from './types'; + +export const errors: ErrorMessages = { + userNotFound: 'User not found', + verificationFailed: 'Failed to verify signature', +}; diff --git a/packages/asymmetric/src/index.ts b/packages/asymmetric/src/index.ts index f4c54211d..f3f789979 100644 --- a/packages/asymmetric/src/index.ts +++ b/packages/asymmetric/src/index.ts @@ -1,4 +1,5 @@ import AccountsAsymmetric from './accounts-asymmetric'; +export * from './types'; export default AccountsAsymmetric; export { AccountsAsymmetric }; diff --git a/packages/asymmetric/src/types/error-messages.ts b/packages/asymmetric/src/types/error-messages.ts new file mode 100644 index 000000000..0cb2242a6 --- /dev/null +++ b/packages/asymmetric/src/types/error-messages.ts @@ -0,0 +1,4 @@ +export interface ErrorMessages { + userNotFound: string; + verificationFailed: string; +} diff --git a/packages/asymmetric/src/types/index.ts b/packages/asymmetric/src/types/index.ts index a03beb450..028574650 100644 --- a/packages/asymmetric/src/types/index.ts +++ b/packages/asymmetric/src/types/index.ts @@ -1,2 +1,3 @@ export * from './asymmetric-login-type'; export * from './public-key-type'; +export * from './error-messages'; From 43296b44bf126e04815ad1cd9510d76665312b6b Mon Sep 17 00:00:00 2001 From: ozsay Date: Mon, 23 Sep 2019 17:17:04 +0300 Subject: [PATCH 3/4] fixes --- packages/asymmetric/README.md | 13 +++++-------- packages/asymmetric/package.json | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/asymmetric/README.md b/packages/asymmetric/README.md index 51c435bf9..ee0edb8a4 100644 --- a/packages/asymmetric/README.md +++ b/packages/asymmetric/README.md @@ -1,21 +1,18 @@ -# @accounts/password - -[![npm](https://img.shields.io/npm/v/@accounts/password.svg?maxAge=2592000)](https://www.npmjs.com/package/@accounts/password) -![MIT License](https://img.shields.io/badge/license-MIT-blue.svg) +# @accounts/asymmetric ## Install ``` -yarn add @accounts/password +yarn add @accounts/asymmetric ``` ## Usage ```js import { AccountsServer } from '@accounts/server'; -import { AccountsPassword } from '@accounts/password'; +import { AccountsAsymmetric } from '@accounts/asymmetric'; -export const accountsPassword = new AccountsPassword({ +export const accountsAsymmetric = new AccountsAsymmetric({ // options }); @@ -24,7 +21,7 @@ const accountsServer = new AccountsServer( // options }, { - password: accountsPassword, + asymmetric: accountsAsymmetric, } ); ``` diff --git a/packages/asymmetric/package.json b/packages/asymmetric/package.json index e8d4e7a53..f7a9c40b6 100644 --- a/packages/asymmetric/package.json +++ b/packages/asymmetric/package.json @@ -22,6 +22,7 @@ "tslib": "1.10.0" }, "devDependencies": { + "@accounts/server": "^0.19.0", "@types/jest": "24.0.18", "@types/node": "12.7.4", "jest": "24.9.0", From 68183d7d7b40a6280986338936048cdba7a351d7 Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 2 Oct 2019 16:49:20 +0300 Subject: [PATCH 4/4] make it work on node lts --- .../__snapshots__/accounts-asymmetric.ts.snap | 2 - .../__tests__/accounts-asymmetric.ts | 138 +++++++++++------- packages/asymmetric/package.json | 6 +- .../asymmetric/src/accounts-asymmetric.ts | 37 ++++- yarn.lock | 45 +++++- 5 files changed, 162 insertions(+), 66 deletions(-) diff --git a/packages/asymmetric/__tests__/__snapshots__/accounts-asymmetric.ts.snap b/packages/asymmetric/__tests__/__snapshots__/accounts-asymmetric.ts.snap index 6bdbc920c..34d8c14c9 100644 --- a/packages/asymmetric/__tests__/__snapshots__/accounts-asymmetric.ts.snap +++ b/packages/asymmetric/__tests__/__snapshots__/accounts-asymmetric.ts.snap @@ -1,5 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AccountsAsymmetric throws error when the user is not found by public key 1`] = `[Error: User not found]`; - -exports[`AccountsAsymmetric throws error when the verification process fails 1`] = `[Error: Failed to verify signature]`; diff --git a/packages/asymmetric/__tests__/accounts-asymmetric.ts b/packages/asymmetric/__tests__/accounts-asymmetric.ts index 62d84a8d0..4d1fd2ae4 100644 --- a/packages/asymmetric/__tests__/accounts-asymmetric.ts +++ b/packages/asymmetric/__tests__/accounts-asymmetric.ts @@ -5,10 +5,17 @@ import { AccountsAsymmetric } from '../src'; describe('AccountsAsymmetric', () => { it('should update the public key', async () => { - const { publicKey } = crypto.generateKeyPairSync('ec', { - namedCurve: 'sect239k1', + const { publicKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + }, }); - const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); const service = new AccountsAsymmetric(); @@ -16,7 +23,7 @@ describe('AccountsAsymmetric', () => { service.setStore({ setService } as any); const publicKeyParams: PublicKeyType = { - key: publicKeyStr, + key: publicKey, encoding: 'base64', format: 'pem', type: 'spki', @@ -26,24 +33,30 @@ describe('AccountsAsymmetric', () => { expect(res).toBeTruthy(); expect(setService).toHaveBeenCalledWith('123', service.serviceName, { - id: publicKeyStr, + id: publicKey, ...publicKeyParams, }); }); it('throws error when the user is not found by public key', async () => { - const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { - namedCurve: 'sect239k1', + const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + }, }); const payload = 'some data to sign'; - const sign = crypto.createSign('sha512'); + const sign = crypto.createSign('SHA256'); sign.write(payload); sign.end(); const signature = sign.sign(privateKey, 'hex'); - const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); - const service = new AccountsAsymmetric(); const findUserByServiceId = jest.fn(() => Promise.resolve(null)); @@ -53,33 +66,34 @@ describe('AccountsAsymmetric', () => { service.authenticate({ signature, payload, - publicKey: publicKeyStr, + publicKey, signatureAlgorithm: 'sha512', signatureFormat: 'hex', }) ).rejects.toMatchSnapshot(); }); - it('throws error when the verification process fails', async () => { - const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { - namedCurve: 'sect239k1', + it('should return null when signature is invalid', async () => { + const { publicKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + }, }); const payload = 'some data to sign'; - const sign = crypto.createSign('sha512'); - sign.write(payload); - sign.end(); - const signature = sign.sign(privateKey, 'hex'); - - const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); - const service = new AccountsAsymmetric(); const publicKeyParams: PublicKeyType = { - key: publicKeyStr, + key: publicKey, encoding: 'base64', format: 'pem', - type: 'spki', + type: 'pkcs1', }; const user = { @@ -91,32 +105,43 @@ describe('AccountsAsymmetric', () => { const findUserByServiceId = jest.fn(() => Promise.resolve(user)); service.setStore({ findUserByServiceId } as any); - await expect( - service.authenticate({ - signature, - payload, - publicKey: publicKeyStr, - signatureAlgorithm: 'sha512', - signatureFormat: 'hex', - }) - ).rejects.toMatchSnapshot(); + const userFromService = await service.authenticate({ + signature: 'some signature', + payload, + publicKey: publicKey, + signatureAlgorithm: 'sha256', + signatureFormat: 'hex', + }); + + expect(userFromService).toBeNull(); }); - it('should return null when signature is invalid', async () => { - const { publicKey } = crypto.generateKeyPairSync('ec', { - namedCurve: 'sect239k1', + it('should return user when verification is successful', async () => { + const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + }, }); const payload = 'some data to sign'; - const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); + const sign = crypto.createSign('sha256'); + sign.write(payload); + sign.end(); + const signature = sign.sign(privateKey, 'hex'); const service = new AccountsAsymmetric(); const publicKeyParams: PublicKeyType = { - key: publicKeyStr, + key: publicKey, encoding: 'base64', - format: 'der', - type: 'spki', + format: 'pem', + type: 'pkcs1', }; const user = { @@ -129,36 +154,43 @@ describe('AccountsAsymmetric', () => { service.setStore({ findUserByServiceId } as any); const userFromService = await service.authenticate({ - signature: 'some signature', + signature, payload, - publicKey: publicKeyStr, - signatureAlgorithm: 'sha512', + publicKey: publicKey, + signatureAlgorithm: 'sha256', signatureFormat: 'hex', }); - expect(userFromService).toBeNull(); + expect(userFromService).toEqual(user); }); - it('should return user when verification is successful', async () => { - const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { - namedCurve: 'sect239k1', + it('should return user when verification is successful using der format', async () => { + const { publicKey: publicKeyBuffer, privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'der', + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + }, }); + const publicKey = publicKeyBuffer.toString('base64'); const payload = 'some data to sign'; - const sign = crypto.createSign('sha512'); + const sign = crypto.createSign('sha256'); sign.write(payload); sign.end(); const signature = sign.sign(privateKey, 'hex'); - const publicKeyStr = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); - const service = new AccountsAsymmetric(); const publicKeyParams: PublicKeyType = { - key: publicKeyStr, + key: publicKey, encoding: 'base64', format: 'der', - type: 'spki', + type: 'pkcs1', }; const user = { @@ -173,8 +205,8 @@ describe('AccountsAsymmetric', () => { const userFromService = await service.authenticate({ signature, payload, - publicKey: publicKeyStr, - signatureAlgorithm: 'sha512', + publicKey, + signatureAlgorithm: 'sha256', signatureFormat: 'hex', }); diff --git a/packages/asymmetric/package.json b/packages/asymmetric/package.json index f7a9c40b6..3de6885ff 100644 --- a/packages/asymmetric/package.json +++ b/packages/asymmetric/package.json @@ -1,6 +1,6 @@ { "name": "@accounts/asymmetric", - "version": "0.19.0", + "version": "0.19.1", "license": "MIT", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -19,12 +19,14 @@ }, "dependencies": { "@accounts/types": "^0.19.0", + "node-forge": "^0.9.1", "tslib": "1.10.0" }, "devDependencies": { "@accounts/server": "^0.19.0", "@types/jest": "24.0.18", - "@types/node": "12.7.4", + "@types/node": "10.14.19", + "@types/node-forge": "^0.8.6", "jest": "24.9.0", "rimraf": "3.0.0" }, diff --git a/packages/asymmetric/src/accounts-asymmetric.ts b/packages/asymmetric/src/accounts-asymmetric.ts index ba6c09f7f..ce3ad7706 100644 --- a/packages/asymmetric/src/accounts-asymmetric.ts +++ b/packages/asymmetric/src/accounts-asymmetric.ts @@ -1,6 +1,8 @@ +import * as crypto from 'crypto'; + import { AuthenticationService, DatabaseInterface } from '@accounts/types'; import { AccountsServer } from '@accounts/server'; -import * as crypto from 'crypto'; +import forge from 'node-forge'; import { AsymmetricLoginType, PublicKeyType, ErrorMessages } from './types'; import { errors } from './errors'; @@ -53,17 +55,38 @@ export default class AccountsAsymmetric implements AuthenticationService { const publicKeyParams: PublicKeyType = (user.services as any)[this.serviceName]; - const publicKey = crypto.createPublicKey({ - key: Buffer.from(publicKeyParams.key, publicKeyParams.encoding), - format: publicKeyParams.format, - type: publicKeyParams.type, - }); + const pemText = + publicKeyParams.format === 'pem' ? publicKeyParams.key : this.derToPem(publicKeyParams); - const isVerified = verify.verify(publicKey, params.signature, params.signatureFormat); + const isVerified = verify.verify(pemText, params.signature, params.signatureFormat); return isVerified ? user : null; } catch (e) { throw new Error(this.options.errors.verificationFailed); } } + + private derToPem(publicKeyParams: PublicKeyType): string { + let derKey; + + switch (publicKeyParams.encoding) { + case 'utf8': { + derKey = forge.util.decodeUtf8(publicKeyParams.key); + break; + } + case 'hex': { + derKey = forge.util.hexToBytes(publicKeyParams.key); + break; + } + case 'base64': + default: { + derKey = forge.util.decode64(publicKeyParams.key); + break; + } + } + + const asnObj = forge.asn1.fromDer(derKey); + const publicKey = forge.pki.publicKeyFromAsn1(asnObj); + return forge.pki.publicKeyToPem(publicKey); + } } diff --git a/yarn.lock b/yarn.lock index fd94449ba..ae6ccaa67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2404,6 +2404,11 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/lodash@4.14.136": + version "4.14.136" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.136.tgz#413e85089046b865d960c9ff1d400e04c31ab60f" + integrity sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA== + "@types/lodash@4.14.138", "@types/lodash@^4.14.138": version "4.14.138" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.138.tgz#34f52640d7358230308344e579c15b378d91989e" @@ -2432,6 +2437,14 @@ "@types/bson" "*" "@types/node" "*" +"@types/mongoose@5.5.11": + version "5.5.11" + resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.5.11.tgz#8562bb84b4f3f41aebec27f263607bfe2183729e" + integrity sha512-Z1W2V3zrB+SeDGI6G1G5XR3JJkkMl4ni7a2Kmq10abdY0wapbaTtUT2/31N+UTPEzhB0KPXUgtQExeKxrc+hxQ== + dependencies: + "@types/mongodb" "*" + "@types/node" "*" + "@types/mongoose@5.5.17": version "5.5.17" resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.5.17.tgz#1f8eb3799368ae266758d2df1bd1a7cfca0f6875" @@ -2447,11 +2460,23 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@12.7.4", "@types/node@>=6", "@types/node@^10.1.0": +"@types/node-forge@^0.8.6": + version "0.8.6" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.8.6.tgz#5d13614e865ccf31f44856d0dda2cb0a288032aa" + integrity sha512-m2G+ipvx+1+BU4LrwrHcEGDk1MnioEVxTnCrkFsKT+BdDl/8OD3WbtspPFj3osnix9fzDIfy2O9p/AFyH3fTzg== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@12.7.4", "@types/node@>=6": version "12.7.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.4.tgz#64db61e0359eb5a8d99b55e05c729f130a678b04" integrity sha512-W0+n1Y+gK/8G2P/piTkBBN38Qc5Q1ZSO6B5H3QmPCUewaiXOo2GCAWZ4ElZCcNhjJuBSUSLGFUJnmlCn5+nxOQ== +"@types/node@10.14.19", "@types/node@^10.1.0": + version "10.14.19" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.19.tgz#f52742c7834a815dedf66edfc8a51547e2a67342" + integrity sha512-j6Sqt38ssdMKutXBUuAcmWF8QtHW1Fwz/mz4Y+Wd9mzpBiVFirjpNQf363hG5itkG+yGaD+oiLyb50HxJ36l9Q== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -7514,7 +7539,18 @@ graphql-toolkit@0.5.12, graphql-toolkit@^0.5.12: tslib "^1.9.3" valid-url "1.0.9" -graphql-tools@4.0.4, graphql-tools@4.0.5, graphql-tools@^4.0.0, graphql-tools@^4.0.5: +graphql-tools@4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.4.tgz#ca08a63454221fdde825fe45fbd315eb2a6d566b" + integrity sha512-chF12etTIGVVGy3fCTJ1ivJX2KB7OSG4c6UOJQuqOHCmBQwTyNgCDuejZKvpYxNZiEx7bwIjrodDgDe9RIkjlw== + dependencies: + apollo-link "^1.2.3" + apollo-utilities "^1.0.1" + deprecated-decorator "^0.1.6" + iterall "^1.1.3" + uuid "^3.1.0" + +graphql-tools@4.0.5, graphql-tools@^4.0.0, graphql-tools@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.5.tgz#d2b41ee0a330bfef833e5cdae7e1f0b0d86b1754" integrity sha512-kQCh3IZsMqquDx7zfIGWBau42xe46gmqabwYkpPlCLIjcEY1XK+auP7iGRD9/205BPyoQdY8hT96MPpgERdC9Q== @@ -10593,6 +10629,11 @@ node-forge@0.7.5: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" integrity sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ== +node-forge@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" + integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== + node-gyp@^5.0.2: version "5.0.3" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-5.0.3.tgz#80d64c23790244991b6d44532f0a351bedd3dd45"