Compare commits

...

7 Commits

Author SHA1 Message Date
db738e978a Generate ECDH key pairs on application startup.
All checks were successful
continuous-integration/drone/push Build is passing
Potential TODO:

- share the keys with other peers.
- think about if it makes sense to encrypt signal messages.
2020-01-07 18:12:00 +01:00
023c2f640b Make private key extractable too.
Main reason: https://bugzilla.mozilla.org/show_bug.cgi?id=1133698

Unfortunately, because of this bug Firefox cannot save non-exportable
keys to IndexedDB. As a workaround, we call exportKey before saving and
store JsonWebKey instead. We use importKey again while reading the key
from the database.

Extra reason for this change - during testing isomorphic-webcrypto
complains about keys read from IndexedDB.

While being less secure, this could also help users export their keys if
they want to migrate their identities to another computer...
2020-01-07 18:11:22 +01:00
46e751cea3 Merge branch 'master' into crypto 2020-01-07 15:59:18 +01:00
dd8c11bea4 Add ability to import exported public key 2020-01-04 12:46:35 +01:00
928ca5ca64 Remove Node 8 from tests and try with 10
Travis CI failed with Node 8 because of isometric-webcrypto
2019-12-07 10:52:25 -03:00
67c38498f4 Add methods to generate keys using ECDH 2019-12-07 10:14:01 -03:00
d0f14f83f9 Add test for IndexedDB 2019-12-02 22:42:18 -03:00
14 changed files with 655 additions and 1 deletions

View File

@ -1,5 +1,5 @@
language: node_js
node_js:
- "8"
- "10"
- "12"
script: npm run ci

View File

@ -0,0 +1,3 @@
require('fake-indexeddb/auto')
// eslint-disable-next-line
window.crypto = require('isomorphic-webcrypto')

264
package-lock.json generated
View File

@ -2279,6 +2279,39 @@
"@types/yargs": "^12.0.9"
}
},
"@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": {
"asn1js": "^2.0.22",
"tslib": "^1.9.3"
}
},
"@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": {
"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": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.2.tgz",
@ -2745,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",
@ -3323,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",
@ -3343,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",
@ -3429,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",
@ -3838,11 +3926,23 @@
}
}
},
"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",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
},
"base64-arraybuffer-es6": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.5.0.tgz",
"integrity": "sha512-UCIPaDJrNNj5jG2ZL+nzJ7czvZV/ZYX6LaIRgfVU1k1edJOQg7dkbiSKzwHkNp6aHEHER/PhlFBrMYnlvJJQEw==",
"dev": true
},
"base64-js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
@ -4643,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",
@ -5899,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",
@ -6109,6 +6226,16 @@
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true
},
"fake-indexeddb": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-3.0.0.tgz",
"integrity": "sha512-VrnV9dJWlVWvd8hp9MMR+JS4RLC4ZmToSkuCg91ZwpYE5mSODb3n5VEaV62Hf3AusnbrPfwQhukU+rGZm5W8PQ==",
"dev": true,
"requires": {
"realistic-structured-clone": "^2.0.1",
"setimmediate": "^1.0.5"
}
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
@ -7880,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",
@ -9782,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",
@ -10763,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",
@ -10863,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",
@ -11279,6 +11465,26 @@
}
}
},
"realistic-structured-clone": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/realistic-structured-clone/-/realistic-structured-clone-2.0.2.tgz",
"integrity": "sha512-5IEvyfuMJ4tjQOuKKTFNvd+H9GSbE87IcendSBannE28PTrbolgaVg5DdEApRKhtze794iXqVUFKV60GLCNKEg==",
"dev": true,
"requires": {
"core-js": "^2.5.3",
"domexception": "^1.0.1",
"typeson": "^5.8.2",
"typeson-registry": "^1.0.0-alpha.20"
},
"dependencies": {
"core-js": {
"version": "2.6.10",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz",
"integrity": "sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==",
"dev": true
}
}
},
"realpath-native": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz",
@ -11843,6 +12049,12 @@
}
}
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
"dev": true
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
@ -12345,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",
@ -13134,6 +13352,36 @@
"integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==",
"dev": true
},
"typeson": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/typeson/-/typeson-5.13.0.tgz",
"integrity": "sha512-xcSaSt+hY/VcRYcqZuVkJwMjDXXJb4CZd51qDocpYw8waA314ygyOPlKhsGsw4qKuJ0tfLLUrxccrm+xvyS0AQ==",
"dev": true
},
"typeson-registry": {
"version": "1.0.0-alpha.29",
"resolved": "https://registry.npmjs.org/typeson-registry/-/typeson-registry-1.0.0-alpha.29.tgz",
"integrity": "sha512-DqRoIx0CtmBGXuumFk7ex5QE6JCWHNKry6D1psGUUB9uIPRrj/SCtuRAidZjLgieWpwn1EfrTFtG0IN2t//F8A==",
"dev": true,
"requires": {
"base64-arraybuffer-es6": "0.5.0",
"typeson": "5.13.0",
"whatwg-url": "7.0.0"
},
"dependencies": {
"whatwg-url": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz",
"integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==",
"dev": true,
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
}
}
},
"uglify-js": {
"version": "3.6.9",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.9.tgz",
@ -13638,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",

View File

@ -91,6 +91,8 @@
"eslint": "^6.6.0",
"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",

View File

@ -1,9 +1,15 @@
import _debug from 'debug'
import socket from '../socket'
import { Dispatch, ThunkResult } from '../store'
import { callId } from '../window'
import { ClientSocket } from '../socket'
import * as NotifyActions from './NotifyActions'
import * as SocketActions from './SocketActions'
import { generateECDHKeyPair, exportKey } from '../crypto'
import { getIdentity, Identity, saveIdentity } from '../db'
import { ME } from '../constants'
const debug = _debug('peercalls')
export interface InitAction {
type: 'INIT'
@ -22,6 +28,11 @@ export const init = (): ThunkResult<Promise<void>> =>
async (dispatch, getState) => {
const socket = await dispatch(connect())
const keyPair = await dispatch(findOrCreateEncryptionKeys())
const publicKey = keyPair && await exportKey(keyPair.publicKey)
// TODO use publicKey
publicKey
dispatch(SocketActions.handshake({
socket,
roomName: callId,
@ -43,3 +54,45 @@ export const connect = () => (dispatch: Dispatch) => {
})
})
}
export const findOrCreateEncryptionKeys = () => async (
dispatch: Dispatch,
): Promise<CryptoKeyPair | undefined> => {
let identity: Identity | undefined
try {
identity = await getIdentity(ME)
} catch (err) {
dispatch(NotifyActions.error('This browser does not support IndexedDB'))
debug('IndexedDB error: %s', err)
return undefined
}
if (identity && identity.privateKey) {
dispatch(NotifyActions.info('Using encryption keys from IndexedDB'))
return {
privateKey: identity.privateKey,
publicKey: identity.publicKey,
}
}
let keyPair: CryptoKeyPair
try {
keyPair = await generateECDHKeyPair()
} catch (err) {
dispatch(NotifyActions.error('Unable to generate encryption keys'))
debug('Unable to generate encryption keys: %s', err)
return undefined
}
const { privateKey, publicKey } = keyPair
identity = {
id: ME,
privateKey,
publicKey,
}
await saveIdentity(identity)
return keyPair
}

View File

@ -0,0 +1,49 @@
import { exportKey, encrypt, decrypt, generateECDHKeyPair, deriveECDHKey, hasWebCryptoAPI, importKey } 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('hasWebCryptoAPI', () => {
it('returns true when crypto.subtle api is available', () => {
expect(hasWebCryptoAPI()).toBe(true)
})
})
describe('exportKey and importKey', () => {
it('exports public key', async () => {
const value = await exportKey(keypair1.publicKey)
console.log(value)
const key = await importKey(value)
expect(key).toBeTruthy()
expect(key).toEqual(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)
})
})
})

107
src/client/crypto/index.ts Normal file
View File

@ -0,0 +1,107 @@
export function hasWebCryptoAPI(): boolean {
return !!(window && window.crypto && window.crypto.subtle)
}
export function hasWebCryptoAPI2(): boolean {
return !!(window?.crypto?.subtle)
}
export async function generateECDHKeyPair() {
const key = await window.crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-256',
},
/* extractable */ true,
['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 */ true,
['encrypt', 'decrypt'],
)
return derivedKey
}
export async function encrypt(key: CryptoKey, data: string): Promise<string> {
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<string> {
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)
}
export async function exportKey(key: CryptoKey) {
return await window.crypto.subtle.exportKey('jwk', key)
}
function ab2str(
buf: ArrayBuffer,
ArrayType: typeof Uint16Array | typeof Uint8Array = Uint16Array,
): string {
return String.fromCharCode.apply(
null, new ArrayType(buf) as unknown as number[])
}
export async function importKey(keyData: JsonWebKey) {
if (keyData.kty !== 'EC' || keyData.crv !== 'P-256') {
throw new Error(`Unsupported key type: ${keyData.kty}, crv: ${keyData.crv}`)
}
const key = await window.crypto.subtle.importKey(
'jwk',
keyData,
{
name: 'ECDH',
namedCurve: keyData.crv,
},
/* extractable */ true,
keyData.key_ops || [],
)
return key
}
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
}

View File

@ -0,0 +1,34 @@
import { getIdentity, saveIdentity } from './identities'
import { promisify } from './promisify'
import { generateECDHKeyPair } from '../crypto'
describe('identities', () => {
const TEST_DB = 'TEST_DB'
let keyPair: CryptoKeyPair
beforeEach(async () => {
keyPair = await generateECDHKeyPair()
})
afterEach(async () => {
await promisify(window.indexedDB.deleteDatabase(TEST_DB))
})
describe('getIdentity', () => {
it('reads saved identity', async () => {
const identity = {
id: 'one',
...keyPair,
}
await saveIdentity(identity)
const identity2 = await getIdentity(identity.id)
expect(identity2).not.toBe(identity)
expect(identity2).toEqual(identity)
})
it('does not fail when not found', async () => {
const identity = await getIdentity('notfound')
expect(identity).toBe(undefined)
})
})
})

View File

@ -0,0 +1,53 @@
import { open } from './open'
import { promisify } from './promisify'
import { exportKey, importKey } from '../crypto'
export interface Identity {
id: string
publicKey: CryptoKey
privateKey?: CryptoKey
}
export interface StoredIdentity {
id: string
publicKey: JsonWebKey
privateKey?: JsonWebKey
}
const DB = 'PEERCALLS'
export async function getIdentity(id: string): Promise<Identity | undefined> {
const db = await open(DB, 1)
let stored: StoredIdentity | undefined
try {
const tx = db.transaction('identities', 'readonly')
const store = tx.objectStore('identities')
stored = await promisify(store.get(id))
} finally {
db.close()
}
return stored && {
id: stored.id,
privateKey: stored.privateKey && await importKey(stored.privateKey),
publicKey: await importKey(stored.publicKey),
}
}
export async function saveIdentity(identity: Identity): Promise<void> {
const stored = {
id: identity.id,
privateKey: identity.privateKey && await exportKey(identity.privateKey),
publicKey: await exportKey(identity.publicKey),
}
const db = await open(DB, 1)
try {
const tx = db.transaction('identities', 'readwrite')
const store = tx.objectStore('identities')
await promisify(store.put(stored))
await promisify(tx)
} finally {
db.close()
}
}

View File

@ -0,0 +1,46 @@
import { open, promisify } from './index'
describe('db', () => {
const TEST_DB = 'TEST_DB'
// async function getError(promise: Promise<unknown>): Promise<Error> {
// let error: Error
// try {
// await promise
// } catch (err) {
// error = err
// }
// expect(error!).toBeTruthy()
// return error!
// }
afterEach(async () => {
db && db.close()
await promisify(window.indexedDB.deleteDatabase(TEST_DB))
})
let db: IDBDatabase
describe('open', () => {
it('can use a custom upgrade function', async () => {
let called = false
db = await open(TEST_DB, 1, ev => {
called = true
})
expect(called).toBe(true)
})
it('opens a new database and upgrades it', async () => {
db = await open(TEST_DB, 1)
const tx = db.transaction('identities', 'readwrite')
const store = tx.objectStore('identities')
await promisify(store.put({id: 'test'}))
const value = await promisify(store.get('test'))
expect(value).toEqual({id: 'test'})
await promisify(tx)
})
})
})

3
src/client/db/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { promisify } from './promisify'
export { open } from './open'
export * from './identities'

12
src/client/db/open.ts Normal file
View File

@ -0,0 +1,12 @@
import { promisify } from './promisify'
import { Upgrade, upgrade } from './upgrade'
export async function open(
name: string,
version: number,
doUpgrade: Upgrade = upgrade,
) {
const request = window.indexedDB.open(name, version)
request.onupgradeneeded = doUpgrade
return promisify(request)
}

View File

@ -0,0 +1,16 @@
export async function promisify(request: IDBTransaction): Promise<void>
export async function promisify<T>(request: IDBRequest<T>): Promise<T>
export async function promisify<T>(request: IDBRequest<T> | IDBTransaction) {
if ('oncomplete' in request) {
// this is a transaction
return new Promise<void>((resolve, reject) => {
request.oncomplete = () => resolve()
request.onerror = err => reject(err)
})
}
// this is an IDBRequest
return new Promise<T>((resolve, reject) => {
request.onsuccess = () => resolve(request.result)
request.onerror = err => reject(err)
})
}

12
src/client/db/upgrade.ts Normal file
View File

@ -0,0 +1,12 @@
export type Upgrade =
(this: IDBOpenDBRequest, event: IDBVersionChangeEvent) => void
export function upgrade(this: IDBOpenDBRequest, event: IDBVersionChangeEvent) {
const db = this.result
switch (event.oldVersion) {
case 0:
db.createObjectStore('identities', {
keyPath: 'id',
})
}
}