Skip to content

Commit

Permalink
Include hkdf implementation for elliptic curve (#39)
Browse files Browse the repository at this point in the history
* Include hkdf implementation for elliptic curve

* clear commented unneeded code

* use connectPlatform instead

* connectPlatform some more
  • Loading branch information
brianwphamSF authored Dec 6, 2023
1 parent 7039f51 commit b57b432
Show file tree
Hide file tree
Showing 8 changed files with 7,742 additions and 4,300 deletions.
11,812 changes: 7,541 additions & 4,271 deletions keeperapi/package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions keeperapi/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@keeper-security/keeperapi",
"description": "Keeper API Javascript SDK",
"version": "16.0.49",
"version": "16.0.50",
"browser": "dist/index.es.js",
"main": "dist/index.cjs.js",
"types": "dist/node/index.d.ts",
Expand All @@ -25,11 +25,14 @@
"node-rsa": "^1.0.8"
},
"devDependencies": {
"@babel/preset-env": "^7.23.5",
"@babel/preset-typescript": "^7.23.3",
"@rollup/plugin-commonjs": "^22.0.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@types/jest": "^24.0.15",
"@types/node": "^12.12.31",
"@types/node": "^20.9.1",
"jest": "^29.6.1",
"jest-environment-jsdom": "^29.7.0",
"protobufjs": "^7.2.4",
"protobufjs-cli": "^1.1.1",
"rollup": "^2.77.1",
Expand Down
78 changes: 78 additions & 0 deletions keeperapi/src/__tests__/crypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* @jest-environment jsdom
*/

// @ts-ignore
import crypto from 'crypto'
import {nodePlatform} from "../node/platform";
import {browserPlatform} from "../browser/platform"
import {publicKey, privateKey} from "./ecies-test-vectors";
import {TextEncoder, TextDecoder} from 'util';
import type {Platform} from "../platform";
import {connectPlatform, platform} from "../platform";

Object.assign(global, {TextDecoder, TextEncoder})

Object.defineProperty(global.self, 'crypto', {
value: {
subtle: crypto.webcrypto.subtle,
getRandomValues: (array: any) => crypto.randomBytes(array.length)
}
})

describe('crypto test', () => {
it('node API encrypts a message under EC and then decrypts it (test key pair)', async () => {
connectPlatform(nodePlatform)
await ecEncryptionTest(publicKey, privateKey)
})
it('node API encrypts a message under EC and then decrypts it (generated key pair)', async () => {
connectPlatform(nodePlatform)
const kp = await platform.generateECKeyPair()
await ecEncryptionTest(kp.publicKey, kp.privateKey)
})
it('browser API encrypts a message under EC and then decrypts it (test key pair)', async () => {
connectPlatform(browserPlatform)
await ecEncryptionTest(publicKey, privateKey)
})
it('browser API encrypts a message under EC and then decrypts it (generated key pair)', async () => {
connectPlatform(browserPlatform)
const kp = await platform.generateECKeyPair()
await ecEncryptionTest(kp.publicKey, kp.privateKey)
})
it('node API encrypts a message with HKDF under EC and then decrypts it (test key pair)', async () => {
connectPlatform(nodePlatform)
await ecWithHkdfEncryptionTest(publicKey, privateKey)
})
it('node API encrypts a message with HKDF under EC and then decrypts it (generated key pair)', async () => {
connectPlatform(nodePlatform)
const kp = await platform.generateECKeyPair()
await ecWithHkdfEncryptionTest(kp.publicKey, kp.privateKey)
})
it('browser API encrypts a message with HKDF under EC and then decrypts it (test key pair)', async () => {
connectPlatform(browserPlatform)
await ecWithHkdfEncryptionTest(publicKey, privateKey)
})
it('browser API encrypts a message with HKDF under EC and then decrypts it (generated key pair)', async () => {
connectPlatform(browserPlatform)
const kp = await platform.generateECKeyPair()
await ecWithHkdfEncryptionTest(kp.publicKey, kp.privateKey)
})
})

async function ecEncryptionTest(publicKey: Uint8Array, privateKey: Uint8Array) {
const message = 'test'
const cipher = await platform.publicEncryptEC(platform.stringToBytes(message), publicKey, Buffer.from([]))
expect(cipher).toBeTruthy()
const decryptedBuffer = await platform.privateDecryptEC(cipher, privateKey, publicKey, undefined)
const decryptedMsg = platform.bytesToString(decryptedBuffer)
expect(decryptedMsg).toEqual(message)
}

async function ecWithHkdfEncryptionTest(publicKey: Uint8Array, privateKey: Uint8Array) {
const message = 'test'
const cipher = await platform.publicEncryptECWithHKDF(message, publicKey, Buffer.from([]))
expect(cipher).toBeTruthy()
const decryptedBuffer = await platform.privateDecryptEC(cipher, privateKey, publicKey, undefined, true)
const decryptedMsg = platform.bytesToString(decryptedBuffer)
expect(decryptedMsg).toEqual(message)
}
5 changes: 5 additions & 0 deletions keeperapi/src/__tests__/ecies-test-vectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const publicKey = new Uint8Array(Buffer.from('BHkCO8yY00I3_4W1iwWGnLSE3DTaQnoLBrTYD1nSjyKF1QFIZgGKZhgofxWE9Ss4OrZV24Oyl080377FGd_Iv_I', 'base64url'))

export const privateKey = new Uint8Array(Buffer.from('Rew7EMRzZcnMM3vvhN8tjdk6x2V4tHuzHKTM5z2QYpo', 'base64url'))

export const id = 'keeper-unit-test'
103 changes: 85 additions & 18 deletions keeperapi/src/browser/platform.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import {EncryptionType, KeyStorage, KeyWrapper, LogOptions, Platform, UnwrappedKeyType, CryptoWorkerOptions, UnwrapKeyMap} from '../platform'
import {
CryptoWorkerOptions,
EncryptionType,
KeyStorage,
KeyWrapper,
LogOptions,
Platform,
UnwrapKeyMap,
UnwrappedKeyType
} from '../platform'
import {_asnhex_getHexOfV_AtObj, _asnhex_getPosArrayOfChildren_AtObj} from "./asn1hex";
import {RSAKey} from "./rsa";
import {getKeeperKeys} from "../transmissionKeys";
import {normal64, normal64Bytes, webSafe64FromBytes} from "../utils";
import {SocketProxy, socketSendMessage} from '../socket'
import * as asmCrypto from 'asmcrypto.js'
import type {KeeperHttpResponse} from "../commands";
import {CryptoWorker, CryptoWorkerMessage, CryptoWorkerPool, CryptoWorkerPoolConfig, CryptoResults } from '../cryptoWorker';
import {
CryptoResults,
CryptoWorker,
CryptoWorkerMessage,
CryptoWorkerPool,
CryptoWorkerPoolConfig
} from '../cryptoWorker';

const rsaAlgorithmName: string = "RSASSA-PKCS1-v1_5";
const CBC_IV_LENGTH = 16
Expand Down Expand Up @@ -375,6 +390,11 @@ export const browserPlatform: Platform = class {
return { publicKey: new Uint8Array(publicKey), privateKey: normal64Bytes(privateKey.d!) }
}

static async publicEncryptECWithHKDF(message: string | Uint8Array, pubKey: Uint8Array, id: Uint8Array): Promise<Uint8Array> {
const messageBytes = typeof message === "string" ? this.stringToBytes(message) : message
return await this.mainPublicEncryptEC(messageBytes, pubKey, id, true)
}

static publicEncrypt(data: Uint8Array, key: string): Uint8Array {
let publicKeyHex = base64ToHex(key);
const pos = _asnhex_getPosArrayOfChildren_AtObj(publicKeyHex, 0);
Expand All @@ -387,23 +407,48 @@ export const browserPlatform: Platform = class {
return hexToBytes(encryptedBinary);
}

static async publicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
static async mainPublicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array, useHKDF?: boolean) {
const ephemeralKeyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits'])
const ephemeralPublicKey = await crypto.subtle.exportKey('raw', ephemeralKeyPair.publicKey)
const recipientPublicKey = await crypto.subtle.importKey('raw', key, { name: 'ECDH', namedCurve: 'P-256' }, true, [])
const sharedSecret = await crypto.subtle.deriveBits({ name: 'ECDH', public: recipientPublicKey }, ephemeralKeyPair.privateKey, 256)
const idBytes = id || new Uint8Array()
const sharedSecretCombined = new Uint8Array(sharedSecret.byteLength + idBytes.byteLength)
sharedSecretCombined.set(new Uint8Array(sharedSecret), 0)
sharedSecretCombined.set(idBytes, sharedSecret.byteLength)
const symmetricKey = await crypto.subtle.digest('SHA-256', sharedSecretCombined)
let symmetricKey: ArrayBuffer
if (!useHKDF) {
const sharedSecretCombined = new Uint8Array(sharedSecret.byteLength + idBytes.byteLength)
sharedSecretCombined.set(new Uint8Array(sharedSecret), 0)
sharedSecretCombined.set(idBytes, sharedSecret.byteLength)
symmetricKey = await crypto.subtle.digest('SHA-256', sharedSecretCombined)
} else {
const hkdfKey = await crypto.subtle.importKey(
'raw',
sharedSecret,
'HKDF',
false,
['deriveBits']
)
symmetricKey = await crypto.subtle.deriveBits(
{
name: 'HKDF',
hash: 'SHA-256',
salt: new Uint8Array(),
info: id
},
hkdfKey,
256
)
}
const cipherText = await this.aesGcmEncrypt(data, new Uint8Array(symmetricKey))
const result = new Uint8Array(ephemeralPublicKey.byteLength + cipherText.byteLength)
result.set(new Uint8Array(ephemeralPublicKey), 0)
result.set(new Uint8Array(cipherText), ephemeralPublicKey.byteLength)
return result
}

static async publicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
return await this.mainPublicEncryptEC(data, key, id)
}

static privateDecrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
let pkh = bytesToHex(key);
const rsa = new RSAKey();
Expand All @@ -413,14 +458,14 @@ export const browserPlatform: Platform = class {
return hexToBytes(decryptedBinary);
}

static async privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
static async privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array> {
if (!publicKey) {
throw Error('Public key is required for EC decryption')
}

const privateKeyImport = await this.importPrivateKeyEC(privateKey, publicKey)

return this.privateDecryptECWebCrypto(data, privateKeyImport, id)
return this.privateDecryptECWebCrypto(data, privateKeyImport, id, useHKDF)
}

static async importPrivateKeyEC(privateKey: Uint8Array, publicKey: Uint8Array) {
Expand All @@ -447,23 +492,45 @@ export const browserPlatform: Platform = class {
return await crypto.subtle.importKey('jwk', jwk, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits'])
}

static async deriveSharedSecretKey(ephemeralPublicKey: Uint8Array, privateKey: CryptoKey, id?: Uint8Array): Promise<CryptoKey> {
static async deriveSharedSecretKey(ephemeralPublicKey: Uint8Array, privateKey: CryptoKey, id?: Uint8Array, useHKDF?: boolean): Promise<CryptoKey> {
const pubCryptoKey = await crypto.subtle.importKey('raw', ephemeralPublicKey, { name: 'ECDH', namedCurve: 'P-256' }, true, [])
const sharedSecret = await crypto.subtle.deriveBits({ name: 'ECDH', public: pubCryptoKey }, privateKey, 256)
let sharedSecretCombined = new Uint8Array(sharedSecret.byteLength + (id?.byteLength ?? 0))
sharedSecretCombined.set(new Uint8Array(sharedSecret), 0)
if (id) {
sharedSecretCombined.set(id, sharedSecret.byteLength)
if (!useHKDF) {
let sharedSecretCombined = new Uint8Array(sharedSecret.byteLength + (id?.byteLength ?? 0))
sharedSecretCombined.set(new Uint8Array(sharedSecret), 0)
if (id) {
sharedSecretCombined.set(id, sharedSecret.byteLength)
}
const symmetricKeyBuffer = await crypto.subtle.digest('SHA-256', sharedSecretCombined)
return this.aesGcmImportKey(new Uint8Array(symmetricKeyBuffer), false)
} else {
const hkdfKey = await crypto.subtle.importKey(
'raw',
sharedSecret,
'HKDF',
false,
['deriveBits']
)

const symmetricKeyBuffer = await crypto.subtle.deriveBits(
{
name: 'HKDF',
hash: 'SHA-256',
salt: new Uint8Array(),
info: id ?? new Uint8Array()
},
hkdfKey,
256
)
return this.aesGcmImportKey(new Uint8Array(symmetricKeyBuffer), false)
}
const symmetricKeyBuffer = await crypto.subtle.digest('SHA-256', sharedSecretCombined)
return this.aesGcmImportKey(new Uint8Array(symmetricKeyBuffer), false)
}

static async privateDecryptECWebCrypto(data: Uint8Array, privateKey: CryptoKey, id?: Uint8Array): Promise<Uint8Array> {
static async privateDecryptECWebCrypto(data: Uint8Array, privateKey: CryptoKey, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array> {
const message = data.slice(ECC_PUB_KEY_LENGTH)
const ephemeralPublicKey = data.slice(0, ECC_PUB_KEY_LENGTH)

const symmetricKey = await this.deriveSharedSecretKey(ephemeralPublicKey, privateKey, id)
const symmetricKey = await this.deriveSharedSecretKey(ephemeralPublicKey, privateKey, id, useHKDF)

return await this.aesGcmDecryptWebCrypto(message, symmetricKey)
}
Expand Down
31 changes: 24 additions & 7 deletions keeperapi/src/node/platform.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import * as crypto from "crypto";
import {createECDH} from "crypto";
import {createECDH, hkdfSync} from "crypto";
import * as https from "https";
import * as FormData from "form-data"
import * as NodeRSA from 'node-rsa';
import * as WebSocket from 'faye-websocket'

import {EncryptionType, KeyStorage, KeyWrapper, LogOptions, Platform, UnwrappedKeyType, UnwrapKeyMap} from "../platform";
import {
EncryptionType,
KeyStorage,
KeyWrapper,
LogOptions,
Platform,
UnwrapKeyMap,
UnwrappedKeyType
} from "../platform";
import {RSA_PKCS1_PADDING} from "constants";
import {getKeeperKeys} from "../transmissionKeys";
import {SocketProxy, socketSendMessage} from '../socket'
import { normal64 } from "../utils";
import {normal64} from "../utils";
import type {KeeperHttpResponse} from "../commands";

const base64ToBytes = (data: string): Uint8Array => {
Expand Down Expand Up @@ -150,6 +158,11 @@ export const nodePlatform: Platform = class {
})
}

static async publicEncryptECWithHKDF(message: string | Uint8Array, pubKey: Uint8Array, id: Uint8Array): Promise<Uint8Array> {
const messageBytes = typeof message === "string" ? this.stringToBytes(message) : message
return await this.mainPublicEncryptEC(messageBytes, pubKey, id, true)
}

static async encrypt(data: Uint8Array, keyId: string, encryptionType: EncryptionType, storage?: KeyStorage): Promise<Uint8Array> {
const key = await loadKey(keyId, storage)
if (!key) {
Expand Down Expand Up @@ -194,17 +207,21 @@ export const nodePlatform: Platform = class {
}, data)
}

static async publicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
static async mainPublicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array> {
const ecdh = createECDH('prime256v1')
ecdh.generateKeys()
const ephemeralPublicKey = ecdh.getPublicKey()
const sharedSecret = ecdh.computeSecret(key)
const sharedSecretCombined = Buffer.concat([sharedSecret, id || new Uint8Array()])
const symmetricKey = crypto.createHash("SHA256").update(sharedSecretCombined).digest()
const symmetricKey = !useHKDF ? crypto.createHash("SHA256").update(sharedSecretCombined).digest() : Buffer.from(hkdfSync('sha256', sharedSecret, new Uint8Array(), id ?? Buffer.from([]), 32))
const encryptedData = await this.aesGcmEncrypt(data, symmetricKey)
return Buffer.concat([ephemeralPublicKey, encryptedData])
}

static async publicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
return await this.mainPublicEncryptEC(data, key, id)
}

static privateDecrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
return crypto.privateDecrypt({
key: crypto.createPrivateKey({
Expand All @@ -216,14 +233,14 @@ export const nodePlatform: Platform = class {
}, data);
}

static async privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
static async privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array> {
const ecdh = createECDH('prime256v1')
ecdh.setPrivateKey(privateKey)
const publicKeyLength = 65
const ephemeralPublicKey = data.slice(0, publicKeyLength)
const sharedSecret = ecdh.computeSecret(ephemeralPublicKey)
const sharedSecretCombined = Buffer.concat([sharedSecret, id || new Uint8Array()])
const symmetricKey = crypto.createHash("SHA256").update(sharedSecretCombined).digest()
const symmetricKey = !useHKDF ? crypto.createHash("SHA256").update(sharedSecretCombined).digest() : Buffer.from(hkdfSync('sha256', sharedSecret, new Uint8Array(), id ?? Buffer.from([]), 32))
const encryptedData = data.slice(publicKeyLength)
return await this.aesGcmDecrypt(encryptedData, symmetricKey)
}
Expand Down
4 changes: 3 additions & 1 deletion keeperapi/src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ export interface Platform {

generateECKeyPair(): Promise<{privateKey: Uint8Array; publicKey: Uint8Array}>

publicEncryptECWithHKDF(message: string | Uint8Array, pubKey: Uint8Array, id: Uint8Array): Promise<Uint8Array>

publicEncrypt(data: Uint8Array, key: string): Uint8Array;

publicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array): Promise<Uint8Array>

privateDecrypt(data: Uint8Array, key: Uint8Array): Uint8Array;

privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array): Promise<Uint8Array>
privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array>

privateSign(data: Uint8Array, key: string): Promise<Uint8Array>;

Expand Down
2 changes: 1 addition & 1 deletion keeperapi/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {KeyWrapper, LogOptions, platform} from "./platform";
import {KeyWrapper, LogOptions, Platform, platform} from "./platform";
import type {KeeperHost, TransmissionKey} from './configuration';
import { AllowedNumbers } from "./transmissionKeys";

Expand Down

0 comments on commit b57b432

Please sign in to comment.