From 67c38498f413b37d56c390305c2bcb8f1d2ba943 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Sat, 7 Dec 2019 10:13:06 -0300 Subject: [PATCH] Add methods to generate keys using ECDH --- jest.setup.js | 2 + package-lock.json | 242 ++++++++++++++++++++++++-------- package.json | 2 +- src/client/crypto/index.test.ts | 33 +++++ src/client/crypto/index.ts | 75 ++++++++++ src/client/db/index.test.ts | 20 +-- 6 files changed, 305 insertions(+), 69 deletions(-) create mode 100644 src/client/crypto/index.test.ts diff --git a/jest.setup.js b/jest.setup.js index a107b7e..fe58f8d 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1 +1,3 @@ require('fake-indexeddb/auto') +// eslint-disable-next-line +window.crypto = require('isomorphic-webcrypto') diff --git a/package-lock.json b/package-lock.json index 80e9855..a06ae0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2279,41 +2279,37 @@ "@types/yargs": "^12.0.9" } }, - "@trust/keyto": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@trust/keyto/-/keyto-0.3.7.tgz", - "integrity": "sha512-t5kWWCTkPgg24JWVuCTPMx7l13F7YHdxBeJkT1vmoHjROgiOIEAN8eeY+iRmP1Hwsx+S7U55HyuqSsECr08a8A==", + "@peculiar/asn1-schema": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-1.0.3.tgz", + "integrity": "sha512-Tfgj9eNJ6cTKEtEuidKenLHMx/Q5M8KEE9hnohHqvdpqHJXWYr5RlT3GjAHPjGXy5+mr7sSfuXfzE6aAkEGN7A==", "dev": true, "requires": { - "asn1.js": "^5.0.1", - "base64url": "^3.0.1", - "elliptic": "^6.4.1" - }, - "dependencies": { - "asn1.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.2.0.tgz", - "integrity": "sha512-Q7hnYGGNYbcmGrCPulXfkEw7oW7qjWeM4ZTALmgpuIcZLxyqqKYWxCZg2UBm8bklrnB4m2mGyJPWfoktdORD8A==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - } + "asn1js": "^2.0.22", + "tslib": "^1.9.3" } }, - "@trust/webcrypto": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@trust/webcrypto/-/webcrypto-0.9.2.tgz", - "integrity": "sha512-5iMAVcGYKhqLJGjefB1nzuQSqUJTru0nG4CytpBT/GGp1Piz/MVnj2jORdYf4JBYzggCIa8WZUr2rchP2Ngn/w==", + "@peculiar/json-schema": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.6.tgz", + "integrity": "sha512-A8DM0ueA+LUqD/HuNPHDd8yMkhbRmnV0iosxyB/uOV1cfiKlCKXDeqkzHTOZpveRI05iCjZxqkPZ2+Nnw1wB4A==", "dev": true, "requires": { - "@trust/keyto": "^0.3.4", - "base64url": "^3.0.0", - "elliptic": "^6.4.0", - "node-rsa": "^0.4.0", - "text-encoding": "^0.6.1" + "tslib": "^1.10.0" + } + }, + "@peculiar/webcrypto": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.0.21.tgz", + "integrity": "sha512-dMQe+vTKSKDpiizQj5q7lFqU56zBgavrjcST4d8RMxEbmgoUOuAUOXlkI5DoqVy3ktcfAhk6CRV4YkaSUEXdAg==", + "dev": true, + "requires": { + "@peculiar/asn1-schema": "^1.0.3", + "@peculiar/json-schema": "^1.1.5", + "asn1js": "^2.0.26", + "pvtsutils": "^1.0.6", + "tslib": "^1.10.0", + "webcrypto-core": "^1.0.14" } }, "@types/babel__core": { @@ -2782,6 +2778,28 @@ } } }, + "@unimodules/core": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@unimodules/core/-/core-4.0.0.tgz", + "integrity": "sha512-lHxRmCG9DK3/aA2lnBKPS32K95NpYE10mZQRp5dycSptgN0DIeWWHuE01SndcSUACGyEP+tDO+DnGo8mhLlt4Q==", + "dev": true, + "optional": true, + "requires": { + "compare-versions": "^3.4.0" + } + }, + "@unimodules/react-native-adapter": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@unimodules/react-native-adapter/-/react-native-adapter-4.0.0.tgz", + "integrity": "sha512-zGAyDhqAEWvshdSxc523srP6OAZaSr95Cv5EuxLJbFGcJENHhK8o/qxhwS7/LYTF3LqtOlnSlwQta3v3y6kF4A==", + "dev": true, + "optional": true, + "requires": { + "invariant": "^2.2.4", + "lodash": "^4.5.0", + "prop-types": "^15.6.1" + } + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -3360,6 +3378,12 @@ "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" }, + "asmcrypto.js": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/asmcrypto.js/-/asmcrypto.js-0.22.0.tgz", + "integrity": "sha512-usgMoyXjMbx/ZPdzTSXExhMPur2FTdz/Vo5PVx2gIaBcdAAJNOFlsdgqveM8Cff7W0v+xrf9BwjOV26JSAF9qA==", + "dev": true + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -3380,6 +3404,15 @@ "minimalistic-assert": "^1.0.0" } }, + "asn1js": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.0.26.tgz", + "integrity": "sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ==", + "dev": true, + "requires": { + "pvutils": "^1.0.17" + } + }, "assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", @@ -3466,6 +3499,24 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, + "b64-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/b64-lite/-/b64-lite-1.4.0.tgz", + "integrity": "sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w==", + "dev": true, + "requires": { + "base-64": "^0.1.0" + } + }, + "b64u-lite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/b64u-lite/-/b64u-lite-1.1.0.tgz", + "integrity": "sha512-929qWGDVCRph7gQVTC6koHqQIpF4vtVaSbwLltFQo44B1bYUquALswZdBKFfrJCPEnsCOvWkJsPdQYZ/Ukhw8A==", + "dev": true, + "requires": { + "b64-lite": "^1.4.0" + } + }, "babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", @@ -3875,6 +3926,12 @@ } } }, + "base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=", + "dev": true + }, "base64-arraybuffer": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", @@ -3897,12 +3954,6 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, - "base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", - "dev": true - }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -4692,6 +4743,13 @@ "dev": true, "optional": true }, + "compare-versions": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.5.1.tgz", + "integrity": "sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg==", + "dev": true, + "optional": true + }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", @@ -5948,6 +6006,16 @@ } } }, + "expo-random": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/expo-random/-/expo-random-7.0.0.tgz", + "integrity": "sha512-+Ajxz4ZwTl2xGIGOg24xaIAw/RvGfKbkDlpsnwKc1FQr7Eka3ZvdQ3q6XKCXtqMG99jXmk9w6YVcbSUMyakpag==", + "dev": true, + "optional": true, + "requires": { + "base64-js": "^1.3.0" + } + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -7939,6 +8007,25 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "isomorphic-webcrypto": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/isomorphic-webcrypto/-/isomorphic-webcrypto-2.3.2.tgz", + "integrity": "sha512-XbUC6ZwVvJfts2FYfAuNRU99tt89EzTknZcKZjxx6/6hxNeLgF6sIDbA/RdA3spbcNrXYyPOHa90khbUgZWarw==", + "dev": true, + "requires": { + "@peculiar/webcrypto": "^1.0.19", + "@unimodules/core": "*", + "@unimodules/react-native-adapter": "*", + "asmcrypto.js": "^0.22.0", + "b64-lite": "^1.3.1", + "b64u-lite": "^1.0.1", + "expo-random": "*", + "msrcrypto": "^1.5.6", + "react-native-securerandom": "^0.1.1", + "str2buf": "^1.3.0", + "webcrypto-shim": "^0.1.4" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -9841,6 +9928,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "msrcrypto": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/msrcrypto/-/msrcrypto-1.5.7.tgz", + "integrity": "sha512-vH/uVdMPgdtLrDCdR2gWps2fB10EYWjXYi67W9RzNSd5Jch3noWGUvNUXSIJA87VTDaE+wvjS7yRSN4gALTslg==", + "dev": true + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -9981,23 +10074,6 @@ } } }, - "node-rsa": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-0.4.2.tgz", - "integrity": "sha1-1jkXKewWqDDtWjgEKzFX0tXXJTA=", - "dev": true, - "requires": { - "asn1": "0.2.3" - }, - "dependencies": { - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - } - } - }, "node-sass": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.0.tgz", @@ -10839,6 +10915,30 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "pvtsutils": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.0.6.tgz", + "integrity": "sha512-0yNrOdJyLE7FZzmeEHTKanwBr5XbmDAd020cKa4ZiTYuGMBYBZmq7vHOhcOqhVllh6gghDBbaz1lnVdOqiB7cw==", + "dev": true, + "requires": { + "@types/node": "^10.14.17", + "tslib": "^1.10.0" + }, + "dependencies": { + "@types/node": { + "version": "10.17.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.6.tgz", + "integrity": "sha512-0a2X6cgN3RdPBL2MIlR6Lt0KlM7fOFsutuXcdglcOq6WvLnYXgPQSh0Mx6tO1KCAE8MxbHSOSTWDoUxRq+l3DA==", + "dev": true + } + } + }, + "pvutils": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz", + "integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ==", + "dev": true + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -10939,6 +11039,16 @@ "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==", "dev": true }, + "react-native-securerandom": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/react-native-securerandom/-/react-native-securerandom-0.1.1.tgz", + "integrity": "sha1-8TBiOkEsM4sK+t7bwgTFy7i/IHA=", + "dev": true, + "optional": true, + "requires": { + "base64-js": "*" + } + }, "react-redux": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.3.tgz", @@ -12447,6 +12557,12 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, + "str2buf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/str2buf/-/str2buf-1.3.0.tgz", + "integrity": "sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA==", + "dev": true + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -12876,12 +12992,6 @@ } } }, - "text-encoding": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13776,6 +13886,22 @@ } } }, + "webcrypto-core": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.0.15.tgz", + "integrity": "sha512-PA4VeKekgPxlmp18Opd4hrdOvtjsJCHyKpNfCyjLWEFIh/7M37QCFgCssx/MVBuNHBkzs9Q7W8Rm4BFgCKheUQ==", + "dev": true, + "requires": { + "pvtsutils": "^1.0.6", + "tslib": "^1.10.0" + } + }, + "webcrypto-shim": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/webcrypto-shim/-/webcrypto-shim-0.1.5.tgz", + "integrity": "sha512-mE+E00gulvbLjHaAwl0kph60oOLQRsKyivEFgV9DMM/3Y05F1vZvGq12hAcNzHRnYxyEOABBT/XMtwGSg5xA7A==", + "dev": true + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index ce402d7..02ee3f6 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "@babel/core": "^7.7.2", "@babel/polyfill": "^7.7.0", "@babel/preset-env": "^7.7.1", - "@trust/webcrypto": "^0.9.2", "@types/classnames": "^2.2.9", "@types/debug": "^4.1.5", "@types/ejs": "^2.6.3", @@ -93,6 +92,7 @@ "eslint-plugin-import": "^2.18.2", "eslint-plugin-react": "^7.16.0", "fake-indexeddb": "^3.0.0", + "isomorphic-webcrypto": "^2.3.2", "jest": "^24.9.0", "loose-envify": "^1.4.0", "node-sass": "^4.13.0", diff --git a/src/client/crypto/index.test.ts b/src/client/crypto/index.test.ts new file mode 100644 index 0000000..03151fd --- /dev/null +++ b/src/client/crypto/index.test.ts @@ -0,0 +1,33 @@ +import { encrypt, decrypt, generateECDHKeyPair, deriveECDHKey } from './index' + +describe('crypto', () => { + + let keypair1: CryptoKeyPair + let keypair2: CryptoKeyPair + + let derived1: CryptoKey + let derived2: CryptoKey + beforeAll(async () => { + keypair1 = await generateECDHKeyPair() + keypair2 = await generateECDHKeyPair() + + derived1 = await deriveECDHKey({ + privateKey: keypair1.privateKey, + publicKey: keypair2.publicKey, + }) + derived2 = await deriveECDHKey({ + privateKey: keypair2.privateKey, + publicKey: keypair1.publicKey, + }) + }) + + describe('encrypt and decrypt', () => { + it('can be encrypted with one pair and decrypted with other', async () => { + const message = 'test message' + const encrypted = await encrypt(derived1, message) + const decrypted = await decrypt(derived2, encrypted) + expect(decrypted).toEqual(message) + }) + }) + +}) diff --git a/src/client/crypto/index.ts b/src/client/crypto/index.ts index e69de29..39460d4 100644 --- a/src/client/crypto/index.ts +++ b/src/client/crypto/index.ts @@ -0,0 +1,75 @@ +export async function generateECDHKeyPair() { + const key = await window.crypto.subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'P-256', + }, + /* extractable */ false, + ['deriveKey'], + ) + return key +} + +export async function deriveECDHKey(params: { + privateKey: CryptoKey + publicKey: CryptoKey +}) { + const { privateKey, publicKey } = params + const derivedKey = await window.crypto.subtle.deriveKey( + { + name: 'ECDH', + public: publicKey, + }, + privateKey, + { + name: 'AES-CTR', + length: 256, + }, + /* extractable */ false, + ['encrypt', 'decrypt'], + ) + + return derivedKey +} + +export async function encrypt(key: CryptoKey, data: string): Promise { + const encrypted = await window.crypto.subtle.encrypt( + { + name: 'AES-CTR', + counter: new Uint8Array(16), + length: 128, + }, + key, + str2ab(data), + ) + return ab2str(encrypted) +} + +export async function decrypt(key: CryptoKey, data: string): Promise { + const arrayBuffer = str2ab(data) + const decrypted = await window.crypto.subtle.decrypt( + { + name: 'AES-CTR', + counter: new ArrayBuffer(16), + length: 128, + }, + key, + arrayBuffer, + ) + return ab2str(decrypted) +} + + +function ab2str(buf: ArrayBuffer): string { + return String.fromCharCode.apply( + null, new Uint16Array(buf) as unknown as number[]) +} + +function str2ab(str: string): ArrayBuffer { + const buf = new ArrayBuffer(str.length*2) // 2 bytes for each char + const bufView = new Uint16Array(buf) + for (let i=0, strLen=str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i) + } + return buf +} diff --git a/src/client/db/index.test.ts b/src/client/db/index.test.ts index f27503b..122f519 100644 --- a/src/client/db/index.test.ts +++ b/src/client/db/index.test.ts @@ -4,16 +4,16 @@ describe('db', () => { const TEST_DB = 'TEST_DB' - async function getError(promise: Promise): Promise { - let error: Error - try { - await promise - } catch (err) { - error = err - } - expect(error!).toBeTruthy() - return error! - } + // async function getError(promise: Promise): Promise { + // let error: Error + // try { + // await promise + // } catch (err) { + // error = err + // } + // expect(error!).toBeTruthy() + // return error! + // } afterEach(async () => { db && db.close()