Compare commits

...

5 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
1110cb0dfb Sign .drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
2020-01-07 11:01:36 +01:00
4ed5265e95 Add .drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
2020-01-07 10:22:22 +01:00
7 changed files with 166 additions and 14 deletions

16
.drone.yml Normal file
View File

@ -0,0 +1,16 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: build
image: node:12
commands:
- npm install
- npm run ci
---
kind: signature
hmac: a49a1e7c428472d0237bb2ba73511965607384f45114941b869c6a9eff7aef70
...

View File

@ -1,9 +1,15 @@
import _debug from 'debug'
import socket from '../socket' import socket from '../socket'
import { Dispatch, ThunkResult } from '../store' import { Dispatch, ThunkResult } from '../store'
import { callId } from '../window' import { callId } from '../window'
import { ClientSocket } from '../socket' import { ClientSocket } from '../socket'
import * as NotifyActions from './NotifyActions' import * as NotifyActions from './NotifyActions'
import * as SocketActions from './SocketActions' 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 { export interface InitAction {
type: 'INIT' type: 'INIT'
@ -22,6 +28,11 @@ export const init = (): ThunkResult<Promise<void>> =>
async (dispatch, getState) => { async (dispatch, getState) => {
const socket = await dispatch(connect()) const socket = await dispatch(connect())
const keyPair = await dispatch(findOrCreateEncryptionKeys())
const publicKey = keyPair && await exportKey(keyPair.publicKey)
// TODO use publicKey
publicKey
dispatch(SocketActions.handshake({ dispatch(SocketActions.handshake({
socket, socket,
roomName: callId, 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

@ -1,4 +1,4 @@
import { exportPublicKey, encrypt, decrypt, generateECDHKeyPair, deriveECDHKey, hasWebCryptoAPI, importPublicKey } from './index' import { exportKey, encrypt, decrypt, generateECDHKeyPair, deriveECDHKey, hasWebCryptoAPI, importKey } from './index'
describe('crypto', () => { describe('crypto', () => {
@ -27,11 +27,11 @@ describe('crypto', () => {
}) })
}) })
describe('exportPublicKey and importPublicKey', () => { describe('exportKey and importKey', () => {
it('exports public key', async () => { it('exports public key', async () => {
const value = await exportPublicKey(keypair1.publicKey) const value = await exportKey(keypair1.publicKey)
console.log(value) console.log(value)
const key = await importPublicKey(value) const key = await importKey(value)
expect(key).toBeTruthy() expect(key).toBeTruthy()
expect(key).toEqual(keypair1.publicKey) expect(key).toEqual(keypair1.publicKey)
}) })

View File

@ -13,7 +13,7 @@ export async function generateECDHKeyPair() {
name: 'ECDH', name: 'ECDH',
namedCurve: 'P-256', namedCurve: 'P-256',
}, },
/* extractable */ false, /* extractable */ true,
['deriveKey'], ['deriveKey'],
) )
return key return key
@ -34,7 +34,7 @@ export async function deriveECDHKey(params: {
name: 'AES-CTR', name: 'AES-CTR',
length: 256, length: 256,
}, },
/* extractable */ false, /* extractable */ true,
['encrypt', 'decrypt'], ['encrypt', 'decrypt'],
) )
@ -68,13 +68,8 @@ export async function decrypt(key: CryptoKey, data: string): Promise<string> {
return ab2str(decrypted) return ab2str(decrypted)
} }
export async function exportPublicKey(key: CryptoKey) { export async function exportKey(key: CryptoKey) {
return await window.crypto.subtle.exportKey('jwk', key) return await window.crypto.subtle.exportKey('jwk', key)
// const pkcs8 = await window.crypto.subtle.exportKey('pkcs8', key)
// const base64 = window.btoa(ab2str(pkcs8, Uint8Array))
// const value =
// `-----BEGIN PUBLIC KEY-----\n${base64}\n-----END PUBLIC KEY-----`
// return value
} }
function ab2str( function ab2str(
@ -85,7 +80,7 @@ function ab2str(
null, new ArrayType(buf) as unknown as number[]) null, new ArrayType(buf) as unknown as number[])
} }
export async function importPublicKey(keyData: JsonWebKey) { export async function importKey(keyData: JsonWebKey) {
if (keyData.kty !== 'EC' || keyData.crv !== 'P-256') { if (keyData.kty !== 'EC' || keyData.crv !== 'P-256') {
throw new Error(`Unsupported key type: ${keyData.kty}, crv: ${keyData.crv}`) throw new Error(`Unsupported key type: ${keyData.kty}, crv: ${keyData.crv}`)
} }
@ -97,7 +92,7 @@ export async function importPublicKey(keyData: JsonWebKey) {
namedCurve: keyData.crv, namedCurve: keyData.crv,
}, },
/* extractable */ true, /* extractable */ true,
[], keyData.key_ops || [],
) )
return key return key
} }

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

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