From db738e978a929c8eeb300723e405efdfa355d095 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Tue, 7 Jan 2020 18:12:00 +0100 Subject: [PATCH] Generate ECDH key pairs on application startup. Potential TODO: - share the keys with other peers. - think about if it makes sense to encrypt signal messages. --- src/client/actions/CallActions.ts | 53 +++++++++++++++++++++++++++++++ src/client/db/identities.test.ts | 34 ++++++++++++++++++++ src/client/db/identities.ts | 53 +++++++++++++++++++++++++++++++ src/client/db/index.ts | 1 + 4 files changed, 141 insertions(+) create mode 100644 src/client/db/identities.test.ts create mode 100644 src/client/db/identities.ts diff --git a/src/client/actions/CallActions.ts b/src/client/actions/CallActions.ts index 3c7aa14..aff4884 100644 --- a/src/client/actions/CallActions.ts +++ b/src/client/actions/CallActions.ts @@ -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> => 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 => { + + 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 +} diff --git a/src/client/db/identities.test.ts b/src/client/db/identities.test.ts new file mode 100644 index 0000000..0dbd6f7 --- /dev/null +++ b/src/client/db/identities.test.ts @@ -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) + }) + }) + +}) diff --git a/src/client/db/identities.ts b/src/client/db/identities.ts new file mode 100644 index 0000000..9cbac3a --- /dev/null +++ b/src/client/db/identities.ts @@ -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 { + 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 { + 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() + } +} diff --git a/src/client/db/index.ts b/src/client/db/index.ts index 34ddef4..676d3ba 100644 --- a/src/client/db/index.ts +++ b/src/client/db/index.ts @@ -1,2 +1,3 @@ export { promisify } from './promisify' export { open } from './open' +export * from './identities'