Skip to content

Commit b57b432

Browse files
authored
Include hkdf implementation for elliptic curve (#39)
* Include hkdf implementation for elliptic curve * clear commented unneeded code * use connectPlatform instead * connectPlatform some more
1 parent 7039f51 commit b57b432

File tree

8 files changed

+7742
-4300
lines changed

8 files changed

+7742
-4300
lines changed

keeperapi/package-lock.json

Lines changed: 7541 additions & 4271 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

keeperapi/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@keeper-security/keeperapi",
33
"description": "Keeper API Javascript SDK",
4-
"version": "16.0.49",
4+
"version": "16.0.50",
55
"browser": "dist/index.es.js",
66
"main": "dist/index.cjs.js",
77
"types": "dist/node/index.d.ts",
@@ -25,11 +25,14 @@
2525
"node-rsa": "^1.0.8"
2626
},
2727
"devDependencies": {
28+
"@babel/preset-env": "^7.23.5",
29+
"@babel/preset-typescript": "^7.23.3",
2830
"@rollup/plugin-commonjs": "^22.0.1",
2931
"@rollup/plugin-node-resolve": "^13.3.0",
3032
"@types/jest": "^24.0.15",
31-
"@types/node": "^12.12.31",
33+
"@types/node": "^20.9.1",
3234
"jest": "^29.6.1",
35+
"jest-environment-jsdom": "^29.7.0",
3336
"protobufjs": "^7.2.4",
3437
"protobufjs-cli": "^1.1.1",
3538
"rollup": "^2.77.1",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
// @ts-ignore
6+
import crypto from 'crypto'
7+
import {nodePlatform} from "../node/platform";
8+
import {browserPlatform} from "../browser/platform"
9+
import {publicKey, privateKey} from "./ecies-test-vectors";
10+
import {TextEncoder, TextDecoder} from 'util';
11+
import type {Platform} from "../platform";
12+
import {connectPlatform, platform} from "../platform";
13+
14+
Object.assign(global, {TextDecoder, TextEncoder})
15+
16+
Object.defineProperty(global.self, 'crypto', {
17+
value: {
18+
subtle: crypto.webcrypto.subtle,
19+
getRandomValues: (array: any) => crypto.randomBytes(array.length)
20+
}
21+
})
22+
23+
describe('crypto test', () => {
24+
it('node API encrypts a message under EC and then decrypts it (test key pair)', async () => {
25+
connectPlatform(nodePlatform)
26+
await ecEncryptionTest(publicKey, privateKey)
27+
})
28+
it('node API encrypts a message under EC and then decrypts it (generated key pair)', async () => {
29+
connectPlatform(nodePlatform)
30+
const kp = await platform.generateECKeyPair()
31+
await ecEncryptionTest(kp.publicKey, kp.privateKey)
32+
})
33+
it('browser API encrypts a message under EC and then decrypts it (test key pair)', async () => {
34+
connectPlatform(browserPlatform)
35+
await ecEncryptionTest(publicKey, privateKey)
36+
})
37+
it('browser API encrypts a message under EC and then decrypts it (generated key pair)', async () => {
38+
connectPlatform(browserPlatform)
39+
const kp = await platform.generateECKeyPair()
40+
await ecEncryptionTest(kp.publicKey, kp.privateKey)
41+
})
42+
it('node API encrypts a message with HKDF under EC and then decrypts it (test key pair)', async () => {
43+
connectPlatform(nodePlatform)
44+
await ecWithHkdfEncryptionTest(publicKey, privateKey)
45+
})
46+
it('node API encrypts a message with HKDF under EC and then decrypts it (generated key pair)', async () => {
47+
connectPlatform(nodePlatform)
48+
const kp = await platform.generateECKeyPair()
49+
await ecWithHkdfEncryptionTest(kp.publicKey, kp.privateKey)
50+
})
51+
it('browser API encrypts a message with HKDF under EC and then decrypts it (test key pair)', async () => {
52+
connectPlatform(browserPlatform)
53+
await ecWithHkdfEncryptionTest(publicKey, privateKey)
54+
})
55+
it('browser API encrypts a message with HKDF under EC and then decrypts it (generated key pair)', async () => {
56+
connectPlatform(browserPlatform)
57+
const kp = await platform.generateECKeyPair()
58+
await ecWithHkdfEncryptionTest(kp.publicKey, kp.privateKey)
59+
})
60+
})
61+
62+
async function ecEncryptionTest(publicKey: Uint8Array, privateKey: Uint8Array) {
63+
const message = 'test'
64+
const cipher = await platform.publicEncryptEC(platform.stringToBytes(message), publicKey, Buffer.from([]))
65+
expect(cipher).toBeTruthy()
66+
const decryptedBuffer = await platform.privateDecryptEC(cipher, privateKey, publicKey, undefined)
67+
const decryptedMsg = platform.bytesToString(decryptedBuffer)
68+
expect(decryptedMsg).toEqual(message)
69+
}
70+
71+
async function ecWithHkdfEncryptionTest(publicKey: Uint8Array, privateKey: Uint8Array) {
72+
const message = 'test'
73+
const cipher = await platform.publicEncryptECWithHKDF(message, publicKey, Buffer.from([]))
74+
expect(cipher).toBeTruthy()
75+
const decryptedBuffer = await platform.privateDecryptEC(cipher, privateKey, publicKey, undefined, true)
76+
const decryptedMsg = platform.bytesToString(decryptedBuffer)
77+
expect(decryptedMsg).toEqual(message)
78+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const publicKey = new Uint8Array(Buffer.from('BHkCO8yY00I3_4W1iwWGnLSE3DTaQnoLBrTYD1nSjyKF1QFIZgGKZhgofxWE9Ss4OrZV24Oyl080377FGd_Iv_I', 'base64url'))
2+
3+
export const privateKey = new Uint8Array(Buffer.from('Rew7EMRzZcnMM3vvhN8tjdk6x2V4tHuzHKTM5z2QYpo', 'base64url'))
4+
5+
export const id = 'keeper-unit-test'

keeperapi/src/browser/platform.ts

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
1-
import {EncryptionType, KeyStorage, KeyWrapper, LogOptions, Platform, UnwrappedKeyType, CryptoWorkerOptions, UnwrapKeyMap} from '../platform'
1+
import {
2+
CryptoWorkerOptions,
3+
EncryptionType,
4+
KeyStorage,
5+
KeyWrapper,
6+
LogOptions,
7+
Platform,
8+
UnwrapKeyMap,
9+
UnwrappedKeyType
10+
} from '../platform'
211
import {_asnhex_getHexOfV_AtObj, _asnhex_getPosArrayOfChildren_AtObj} from "./asn1hex";
312
import {RSAKey} from "./rsa";
413
import {getKeeperKeys} from "../transmissionKeys";
514
import {normal64, normal64Bytes, webSafe64FromBytes} from "../utils";
615
import {SocketProxy, socketSendMessage} from '../socket'
716
import * as asmCrypto from 'asmcrypto.js'
817
import type {KeeperHttpResponse} from "../commands";
9-
import {CryptoWorker, CryptoWorkerMessage, CryptoWorkerPool, CryptoWorkerPoolConfig, CryptoResults } from '../cryptoWorker';
18+
import {
19+
CryptoResults,
20+
CryptoWorker,
21+
CryptoWorkerMessage,
22+
CryptoWorkerPool,
23+
CryptoWorkerPoolConfig
24+
} from '../cryptoWorker';
1025

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

393+
static async publicEncryptECWithHKDF(message: string | Uint8Array, pubKey: Uint8Array, id: Uint8Array): Promise<Uint8Array> {
394+
const messageBytes = typeof message === "string" ? this.stringToBytes(message) : message
395+
return await this.mainPublicEncryptEC(messageBytes, pubKey, id, true)
396+
}
397+
378398
static publicEncrypt(data: Uint8Array, key: string): Uint8Array {
379399
let publicKeyHex = base64ToHex(key);
380400
const pos = _asnhex_getPosArrayOfChildren_AtObj(publicKeyHex, 0);
@@ -387,23 +407,48 @@ export const browserPlatform: Platform = class {
387407
return hexToBytes(encryptedBinary);
388408
}
389409

390-
static async publicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
410+
static async mainPublicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array, useHKDF?: boolean) {
391411
const ephemeralKeyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits'])
392412
const ephemeralPublicKey = await crypto.subtle.exportKey('raw', ephemeralKeyPair.publicKey)
393413
const recipientPublicKey = await crypto.subtle.importKey('raw', key, { name: 'ECDH', namedCurve: 'P-256' }, true, [])
394414
const sharedSecret = await crypto.subtle.deriveBits({ name: 'ECDH', public: recipientPublicKey }, ephemeralKeyPair.privateKey, 256)
395415
const idBytes = id || new Uint8Array()
396-
const sharedSecretCombined = new Uint8Array(sharedSecret.byteLength + idBytes.byteLength)
397-
sharedSecretCombined.set(new Uint8Array(sharedSecret), 0)
398-
sharedSecretCombined.set(idBytes, sharedSecret.byteLength)
399-
const symmetricKey = await crypto.subtle.digest('SHA-256', sharedSecretCombined)
416+
let symmetricKey: ArrayBuffer
417+
if (!useHKDF) {
418+
const sharedSecretCombined = new Uint8Array(sharedSecret.byteLength + idBytes.byteLength)
419+
sharedSecretCombined.set(new Uint8Array(sharedSecret), 0)
420+
sharedSecretCombined.set(idBytes, sharedSecret.byteLength)
421+
symmetricKey = await crypto.subtle.digest('SHA-256', sharedSecretCombined)
422+
} else {
423+
const hkdfKey = await crypto.subtle.importKey(
424+
'raw',
425+
sharedSecret,
426+
'HKDF',
427+
false,
428+
['deriveBits']
429+
)
430+
symmetricKey = await crypto.subtle.deriveBits(
431+
{
432+
name: 'HKDF',
433+
hash: 'SHA-256',
434+
salt: new Uint8Array(),
435+
info: id
436+
},
437+
hkdfKey,
438+
256
439+
)
440+
}
400441
const cipherText = await this.aesGcmEncrypt(data, new Uint8Array(symmetricKey))
401442
const result = new Uint8Array(ephemeralPublicKey.byteLength + cipherText.byteLength)
402443
result.set(new Uint8Array(ephemeralPublicKey), 0)
403444
result.set(new Uint8Array(cipherText), ephemeralPublicKey.byteLength)
404445
return result
405446
}
406447

448+
static async publicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
449+
return await this.mainPublicEncryptEC(data, key, id)
450+
}
451+
407452
static privateDecrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
408453
let pkh = bytesToHex(key);
409454
const rsa = new RSAKey();
@@ -413,14 +458,14 @@ export const browserPlatform: Platform = class {
413458
return hexToBytes(decryptedBinary);
414459
}
415460

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

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

423-
return this.privateDecryptECWebCrypto(data, privateKeyImport, id)
468+
return this.privateDecryptECWebCrypto(data, privateKeyImport, id, useHKDF)
424469
}
425470

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

450-
static async deriveSharedSecretKey(ephemeralPublicKey: Uint8Array, privateKey: CryptoKey, id?: Uint8Array): Promise<CryptoKey> {
495+
static async deriveSharedSecretKey(ephemeralPublicKey: Uint8Array, privateKey: CryptoKey, id?: Uint8Array, useHKDF?: boolean): Promise<CryptoKey> {
451496
const pubCryptoKey = await crypto.subtle.importKey('raw', ephemeralPublicKey, { name: 'ECDH', namedCurve: 'P-256' }, true, [])
452497
const sharedSecret = await crypto.subtle.deriveBits({ name: 'ECDH', public: pubCryptoKey }, privateKey, 256)
453-
let sharedSecretCombined = new Uint8Array(sharedSecret.byteLength + (id?.byteLength ?? 0))
454-
sharedSecretCombined.set(new Uint8Array(sharedSecret), 0)
455-
if (id) {
456-
sharedSecretCombined.set(id, sharedSecret.byteLength)
498+
if (!useHKDF) {
499+
let sharedSecretCombined = new Uint8Array(sharedSecret.byteLength + (id?.byteLength ?? 0))
500+
sharedSecretCombined.set(new Uint8Array(sharedSecret), 0)
501+
if (id) {
502+
sharedSecretCombined.set(id, sharedSecret.byteLength)
503+
}
504+
const symmetricKeyBuffer = await crypto.subtle.digest('SHA-256', sharedSecretCombined)
505+
return this.aesGcmImportKey(new Uint8Array(symmetricKeyBuffer), false)
506+
} else {
507+
const hkdfKey = await crypto.subtle.importKey(
508+
'raw',
509+
sharedSecret,
510+
'HKDF',
511+
false,
512+
['deriveBits']
513+
)
514+
515+
const symmetricKeyBuffer = await crypto.subtle.deriveBits(
516+
{
517+
name: 'HKDF',
518+
hash: 'SHA-256',
519+
salt: new Uint8Array(),
520+
info: id ?? new Uint8Array()
521+
},
522+
hkdfKey,
523+
256
524+
)
525+
return this.aesGcmImportKey(new Uint8Array(symmetricKeyBuffer), false)
457526
}
458-
const symmetricKeyBuffer = await crypto.subtle.digest('SHA-256', sharedSecretCombined)
459-
return this.aesGcmImportKey(new Uint8Array(symmetricKeyBuffer), false)
460527
}
461528

462-
static async privateDecryptECWebCrypto(data: Uint8Array, privateKey: CryptoKey, id?: Uint8Array): Promise<Uint8Array> {
529+
static async privateDecryptECWebCrypto(data: Uint8Array, privateKey: CryptoKey, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array> {
463530
const message = data.slice(ECC_PUB_KEY_LENGTH)
464531
const ephemeralPublicKey = data.slice(0, ECC_PUB_KEY_LENGTH)
465532

466-
const symmetricKey = await this.deriveSharedSecretKey(ephemeralPublicKey, privateKey, id)
533+
const symmetricKey = await this.deriveSharedSecretKey(ephemeralPublicKey, privateKey, id, useHKDF)
467534

468535
return await this.aesGcmDecryptWebCrypto(message, symmetricKey)
469536
}

keeperapi/src/node/platform.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import * as crypto from "crypto";
2-
import {createECDH} from "crypto";
2+
import {createECDH, hkdfSync} from "crypto";
33
import * as https from "https";
44
import * as FormData from "form-data"
55
import * as NodeRSA from 'node-rsa';
66
import * as WebSocket from 'faye-websocket'
77

8-
import {EncryptionType, KeyStorage, KeyWrapper, LogOptions, Platform, UnwrappedKeyType, UnwrapKeyMap} from "../platform";
8+
import {
9+
EncryptionType,
10+
KeyStorage,
11+
KeyWrapper,
12+
LogOptions,
13+
Platform,
14+
UnwrapKeyMap,
15+
UnwrappedKeyType
16+
} from "../platform";
917
import {RSA_PKCS1_PADDING} from "constants";
1018
import {getKeeperKeys} from "../transmissionKeys";
1119
import {SocketProxy, socketSendMessage} from '../socket'
12-
import { normal64 } from "../utils";
20+
import {normal64} from "../utils";
1321
import type {KeeperHttpResponse} from "../commands";
1422

1523
const base64ToBytes = (data: string): Uint8Array => {
@@ -150,6 +158,11 @@ export const nodePlatform: Platform = class {
150158
})
151159
}
152160

161+
static async publicEncryptECWithHKDF(message: string | Uint8Array, pubKey: Uint8Array, id: Uint8Array): Promise<Uint8Array> {
162+
const messageBytes = typeof message === "string" ? this.stringToBytes(message) : message
163+
return await this.mainPublicEncryptEC(messageBytes, pubKey, id, true)
164+
}
165+
153166
static async encrypt(data: Uint8Array, keyId: string, encryptionType: EncryptionType, storage?: KeyStorage): Promise<Uint8Array> {
154167
const key = await loadKey(keyId, storage)
155168
if (!key) {
@@ -194,17 +207,21 @@ export const nodePlatform: Platform = class {
194207
}, data)
195208
}
196209

197-
static async publicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
210+
static async mainPublicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array> {
198211
const ecdh = createECDH('prime256v1')
199212
ecdh.generateKeys()
200213
const ephemeralPublicKey = ecdh.getPublicKey()
201214
const sharedSecret = ecdh.computeSecret(key)
202215
const sharedSecretCombined = Buffer.concat([sharedSecret, id || new Uint8Array()])
203-
const symmetricKey = crypto.createHash("SHA256").update(sharedSecretCombined).digest()
216+
const symmetricKey = !useHKDF ? crypto.createHash("SHA256").update(sharedSecretCombined).digest() : Buffer.from(hkdfSync('sha256', sharedSecret, new Uint8Array(), id ?? Buffer.from([]), 32))
204217
const encryptedData = await this.aesGcmEncrypt(data, symmetricKey)
205218
return Buffer.concat([ephemeralPublicKey, encryptedData])
206219
}
207220

221+
static async publicEncryptEC(data: Uint8Array, key: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
222+
return await this.mainPublicEncryptEC(data, key, id)
223+
}
224+
208225
static privateDecrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
209226
return crypto.privateDecrypt({
210227
key: crypto.createPrivateKey({
@@ -216,14 +233,14 @@ export const nodePlatform: Platform = class {
216233
}, data);
217234
}
218235

219-
static async privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array): Promise<Uint8Array> {
236+
static async privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array> {
220237
const ecdh = createECDH('prime256v1')
221238
ecdh.setPrivateKey(privateKey)
222239
const publicKeyLength = 65
223240
const ephemeralPublicKey = data.slice(0, publicKeyLength)
224241
const sharedSecret = ecdh.computeSecret(ephemeralPublicKey)
225242
const sharedSecretCombined = Buffer.concat([sharedSecret, id || new Uint8Array()])
226-
const symmetricKey = crypto.createHash("SHA256").update(sharedSecretCombined).digest()
243+
const symmetricKey = !useHKDF ? crypto.createHash("SHA256").update(sharedSecretCombined).digest() : Buffer.from(hkdfSync('sha256', sharedSecret, new Uint8Array(), id ?? Buffer.from([]), 32))
227244
const encryptedData = data.slice(publicKeyLength)
228245
return await this.aesGcmDecrypt(encryptedData, symmetricKey)
229246
}

keeperapi/src/platform.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ export interface Platform {
3939

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

42+
publicEncryptECWithHKDF(message: string | Uint8Array, pubKey: Uint8Array, id: Uint8Array): Promise<Uint8Array>
43+
4244
publicEncrypt(data: Uint8Array, key: string): Uint8Array;
4345

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

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

48-
privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array): Promise<Uint8Array>
50+
privateDecryptEC(data: Uint8Array, privateKey: Uint8Array, publicKey?: Uint8Array, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array>
4951

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

keeperapi/src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {KeyWrapper, LogOptions, platform} from "./platform";
1+
import {KeyWrapper, LogOptions, Platform, platform} from "./platform";
22
import type {KeeperHost, TransmissionKey} from './configuration';
33
import { AllowedNumbers } from "./transmissionKeys";
44

0 commit comments

Comments
 (0)