Skip to content

Commit 2257dc6

Browse files
committed
HPKE wrapped transmission key
- add `useHpkeForTransmissionKey` config option to opt in - [ ] remaining ML-KEM public keys - [ ] update protobufs once merged into master - [ ] unit tests?
1 parent 31e58bd commit 2257dc6

File tree

18 files changed

+1255
-64
lines changed

18 files changed

+1255
-64
lines changed

keeperapi/package-lock.json

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

keeperapi/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"publish-to-npm": "npm publish"
2020
},
2121
"dependencies": {
22+
"@noble/post-quantum": "^0.5.2",
2223
"asmcrypto.js": "^2.3.2",
2324
"faye-websocket": "^0.11.3",
2425
"form-data": "^4.0.4",

keeperapi/rollup.config.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export default [
2020
],
2121
external: [
2222
...Object.keys(pkg.dependencies || {}),
23-
"protobufjs/minimal"
23+
"protobufjs/minimal",
24+
"@noble/post-quantum/ml-kem.js"
2425
],
2526
plugins: [
2627
typescript({
@@ -56,7 +57,8 @@ export default [
5657
],
5758
external: [
5859
...Object.keys(pkg.dependencies || {}),
59-
"crypto", "constants", "https", "protobufjs/minimal"
60+
"crypto", "constants", "https", "protobufjs/minimal",
61+
"@noble/post-quantum/ml-kem.js"
6062
],
6163
plugins: [
6264
typescript({

keeperapi/src/browser/platform.ts

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '../platform'
1111
import {_asnhex_getHexOfV_AtObj, _asnhex_getPosArrayOfChildren_AtObj} from "./asn1hex";
1212
import {RSAKey} from "./rsa";
13-
import {getKeeperKeys} from "../transmissionKeys";
13+
import {getKeeperKeys, getKeeperMlKemKeys} from "../transmissionKeys";
1414
import {normal64, normal64Bytes, webSafe64FromBytes} from "../utils";
1515
import {SocketProxy, socketSendMessage} from '../socket'
1616
import * as asmCrypto from 'asmcrypto.js'
@@ -45,6 +45,7 @@ export const browserPlatform: Platform = class {
4545
}
4646

4747
static keys = getKeeperKeys(this.normal64Bytes);
48+
static mlKemKeys = getKeeperMlKemKeys(this.base64ToBytes);
4849

4950
static getRandomBytes(length: number): Uint8Array {
5051
let data = new Uint8Array(length);
@@ -540,6 +541,27 @@ export const browserPlatform: Platform = class {
540541
return await crypto.subtle.importKey('jwk', jwk, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits'])
541542
}
542543

544+
static async ecdhComputeSharedSecret(
545+
senderPrivateKey: Uint8Array,
546+
recipientPublicKey: Uint8Array,
547+
senderPublicKey: Uint8Array
548+
): Promise<Uint8Array> {
549+
const senderPrivateCryptoKey = await this.importPrivateKeyEC(senderPrivateKey, senderPublicKey)
550+
const recipientPublicCryptoKey = await crypto.subtle.importKey(
551+
'raw',
552+
recipientPublicKey,
553+
{ name: 'ECDH', namedCurve: 'P-256' },
554+
false,
555+
[]
556+
)
557+
const sharedSecretBuffer = await crypto.subtle.deriveBits(
558+
{ name: 'ECDH', public: recipientPublicCryptoKey },
559+
senderPrivateCryptoKey,
560+
256
561+
)
562+
return new Uint8Array(sharedSecretBuffer)
563+
}
564+
543565
static async deriveSharedSecretKey(ephemeralPublicKey: Uint8Array, privateKey: CryptoKey, id?: Uint8Array, useHKDF?: boolean): Promise<CryptoKey> {
544566
const pubCryptoKey = await crypto.subtle.importKey('raw', ephemeralPublicKey, { name: 'ECDH', namedCurve: 'P-256' }, true, [])
545567
const sharedSecret = await crypto.subtle.deriveBits({ name: 'ECDH', public: pubCryptoKey }, privateKey, 256)
@@ -552,28 +574,36 @@ export const browserPlatform: Platform = class {
552574
const symmetricKeyBuffer = await crypto.subtle.digest('SHA-256', sharedSecretCombined)
553575
return this.aesGcmImportKey(new Uint8Array(symmetricKeyBuffer), false)
554576
} else {
555-
const hkdfKey = await crypto.subtle.importKey(
556-
'raw',
557-
sharedSecret,
558-
'HKDF',
559-
false,
560-
['deriveBits']
561-
)
562-
563-
const symmetricKeyBuffer = await crypto.subtle.deriveBits(
564-
{
565-
name: 'HKDF',
566-
hash: 'SHA-256',
567-
salt: new Uint8Array(),
568-
info: id ?? new Uint8Array()
569-
},
570-
hkdfKey,
571-
256
572-
)
573-
return this.aesGcmImportKey(new Uint8Array(symmetricKeyBuffer), false)
577+
const symmetricKey = await this.hkdf(new Uint8Array(), new Uint8Array(sharedSecret), id ?? new Uint8Array(), 32)
578+
return this.aesGcmImportKey(symmetricKey, false)
574579
}
575580
}
576581

582+
static async hkdf(salt: Uint8Array, ikm: Uint8Array, info: Uint8Array, length: number): Promise<Uint8Array> {
583+
// Import IKM as HKDF key
584+
const hkdfKey = await crypto.subtle.importKey(
585+
'raw',
586+
ikm,
587+
'HKDF',
588+
false,
589+
['deriveBits']
590+
);
591+
592+
// Derive bits using HKDF
593+
const derivedBits = await crypto.subtle.deriveBits(
594+
{
595+
name: 'HKDF',
596+
hash: 'SHA-256',
597+
salt: salt,
598+
info: info
599+
},
600+
hkdfKey,
601+
length * 8 // bits
602+
);
603+
604+
return new Uint8Array(derivedBits);
605+
}
606+
577607
static async privateDecryptECWebCrypto(data: Uint8Array, privateKey: CryptoKey, id?: Uint8Array, useHKDF?: boolean): Promise<Uint8Array> {
578608
const message = data.slice(ECC_PUB_KEY_LENGTH)
579609
const ephemeralPublicKey = data.slice(0, ECC_PUB_KEY_LENGTH)
@@ -808,7 +838,11 @@ export const browserPlatform: Platform = class {
808838
}
809839

810840
static async calcAuthVerifier(key: Uint8Array): Promise<Uint8Array> {
811-
let digest = await crypto.subtle.digest("SHA-256", key);
841+
return this.sha256(key);
842+
}
843+
844+
static async sha256(data: Uint8Array): Promise<Uint8Array> {
845+
const digest = await crypto.subtle.digest("SHA-256", data);
812846
return new Uint8Array(digest);
813847
}
814848

keeperapi/src/configuration.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface ClientConfiguration {
2626
useSessionResumption?: boolean
2727
iterations?: number
2828
salt?: Uint8Array
29+
useHpkeForTransmissionKey?: boolean
2930
}
3031
export interface ClientConfigurationInternal extends ClientConfiguration {
3132
deviceConfig: DeviceConfig // v15+ device config
@@ -39,6 +40,7 @@ export type KeeperError = {
3940
path?: string
4041
result_code?: string
4142
key_id?: number
43+
qrc_ec_key_id?: number
4244
region_host?: string
4345
}
4446

@@ -48,6 +50,7 @@ export interface DeviceConfig {
4850
privateKey?: Uint8Array
4951
publicKey?: Uint8Array
5052
transmissionKeyId?: number
53+
mlKemPublicKeyId?: number
5154
}
5255

5356
export interface SessionStorage {
@@ -74,8 +77,24 @@ export interface VendorConfiguration {
7477

7578
export interface TransmissionKey {
7679
key: Uint8Array
77-
publicKeyId: number
78-
encryptedKey: Uint8Array
80+
ecKeyId: number
81+
ecEncryptedKey: Uint8Array
82+
mlKemKeyId: number
83+
}
84+
85+
export interface QrcMessageKey {
86+
clientEcPublicKey: Uint8Array
87+
mlKemEncapsulatedKey: Uint8Array
88+
data: Uint8Array
89+
msgVersion: number
90+
ecKeyId: number
91+
}
92+
93+
export interface TransmissionKeyHpke {
94+
key: Uint8Array
95+
mlKemKeyId: number
96+
qrcMessageKey: QrcMessageKey
97+
optionalData?: Uint8Array
7998
}
8099

81100
export interface AuthUI {

0 commit comments

Comments
 (0)