Skip to content

Commit baf17f5

Browse files
committed
HPKE wrapped transmission key
- add `useHpkeForTransmissionKey` config option to opt in - [ ] remaining ML-KEM public keys - [ ] unit tests?
1 parent 31e58bd commit baf17f5

File tree

18 files changed

+1410
-43
lines changed

18 files changed

+1410
-43
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/src/browser/platform.ts

Lines changed: 7 additions & 2 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);
@@ -808,7 +809,11 @@ export const browserPlatform: Platform = class {
808809
}
809810

810811
static async calcAuthVerifier(key: Uint8Array): Promise<Uint8Array> {
811-
let digest = await crypto.subtle.digest("SHA-256", key);
812+
return this.sha256(key);
813+
}
814+
815+
static async sha256(data: Uint8Array): Promise<Uint8Array> {
816+
const digest = await crypto.subtle.digest("SHA-256", data);
812817
return new Uint8Array(digest);
813818
}
814819

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 {

keeperapi/src/endpoint.ts

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {platform} from './platform'
44
import {
55
formatTimeDiff,
66
generateTransmissionKey,
7+
generateHpkeTransmissionKey,
78
getKeeperUrl,
89
isTwoFactorResultCode, log,
910
normal64Bytes,
@@ -26,7 +27,7 @@ import WssClientResponse = Push.WssClientResponse;
2627
import WssConnectionRequest = Push.WssConnectionRequest;
2728
import SsoCloudResponse = SsoCloud.SsoCloudResponse;
2829
import {KeeperHttpResponse, RestCommand} from './commands'
29-
import { AllowedNumbers, isAllowedNumber } from './transmissionKeys'
30+
import {AllowedEcKeyIds, AllowedMlKemKeyIds, isAllowedEcKeyId, isAllowedMlKemKeyId} from './transmissionKeys'
3031

3132
export class KeeperEndpoint {
3233
private _transmissionKey?: TransmissionKey
@@ -37,21 +38,29 @@ export class KeeperEndpoint {
3738
private onsitePrivateKey: Uint8Array | null = null
3839
private onsitePublicKey: Uint8Array | null = null
3940

40-
constructor(private options: ClientConfigurationInternal) {
41+
private useHpkeForTransmissionKey: boolean = false
42+
43+
constructor(private options: ClientConfigurationInternal) {
4144
if (options.deviceToken) {
4245
this.deviceToken = options.deviceToken
43-
}
46+
}
4447
if (options.locale) {
4548
this.locale = options.locale
4649
}
50+
if (options.useHpkeForTransmissionKey) {
51+
this.useHpkeForTransmissionKey = true
52+
}
4753
}
4854

4955
async getTransmissionKey():Promise<TransmissionKey> {
50-
const deviceConfigTransmissionKeyId = this.options.deviceConfig.transmissionKeyId || 7
51-
if(!this._transmissionKey && isAllowedNumber(deviceConfigTransmissionKeyId)){
52-
this._transmissionKey = await generateTransmissionKey(deviceConfigTransmissionKeyId)
56+
const DEFAULT_PROD_EC_KEY_ID = 10
57+
const DEFAULT_PROD_ML_KEM_KEY_ID = 100
58+
const deviceConfigTransmissionKeyId = this.options.deviceConfig.transmissionKeyId || DEFAULT_PROD_EC_KEY_ID
59+
const deviceConfigMlKemKeyId = this.options.deviceConfig.mlKemPublicKeyId || DEFAULT_PROD_ML_KEM_KEY_ID
60+
if(!this._transmissionKey && isAllowedEcKeyId(deviceConfigTransmissionKeyId) && isAllowedMlKemKeyId(deviceConfigMlKemKeyId)){
61+
this._transmissionKey = await generateTransmissionKey(deviceConfigTransmissionKeyId, deviceConfigMlKemKeyId)
5362
} else if(!this._transmissionKey){
54-
this._transmissionKey = await generateTransmissionKey(7)
63+
this._transmissionKey = await generateTransmissionKey(DEFAULT_PROD_EC_KEY_ID, DEFAULT_PROD_ML_KEM_KEY_ID)
5564
}
5665

5766
return this._transmissionKey
@@ -159,7 +168,15 @@ export class KeeperEndpoint {
159168
while (true) {
160169
const payload = 'toBytes' in message ? message.toBytes() : new Uint8Array()
161170
const apiVersion = message.apiVersion || 0
162-
const request = await this.prepareRequest(payload, sessionToken, apiVersion)
171+
172+
const request = await prepareApiRequest({
173+
payload,
174+
transmissionKey: this._transmissionKey,
175+
sessionToken,
176+
locale: this.locale,
177+
apiVersion,
178+
useHpkeForTransmissionKey: this.useHpkeForTransmissionKey
179+
})
163180
log(`Calling REST URL: ${this.getUrl(message.path)}`, 'noCR');
164181
const startTime = Date.now()
165182
const response = await platform.post(this.getUrl(message.path), request)
@@ -183,9 +200,14 @@ export class KeeperEndpoint {
183200
const errorObj: KeeperError = JSON.parse(errorMessage)
184201
switch (errorObj.error) {
185202
case 'key':
186-
if(isAllowedNumber(errorObj.key_id!)){
187-
await this.updateTransmissionKey(errorObj.key_id!)
188-
} else {
203+
if (errorObj.qrc_ec_key_id && isAllowedEcKeyId(errorObj.qrc_ec_key_id) && isAllowedMlKemKeyId(errorObj.key_id!)){
204+
// Rotate EC key and ML-KEM key
205+
await this.updateTransmissionKey(errorObj.qrc_ec_key_id!, errorObj.key_id!)
206+
} else if (errorObj.key_id && isAllowedEcKeyId(errorObj.key_id) && this._transmissionKey) {
207+
// Rotate EC key
208+
await this.updateTransmissionKey(errorObj.key_id, this._transmissionKey.mlKemKeyId as AllowedMlKemKeyIds)
209+
}
210+
else {
189211
throw new Error('Incorrect Transmission Key ID being used.')
190212
}
191213
continue
@@ -245,18 +267,26 @@ export class KeeperEndpoint {
245267
return platform.get(this.getUrl(path), {})
246268
}
247269

248-
public async updateTransmissionKey(keyNumber: AllowedNumbers) {
249-
this._transmissionKey = await generateTransmissionKey(keyNumber)
270+
public async updateTransmissionKey(ecKeyId: AllowedEcKeyIds, mlKemKeyId: AllowedMlKemKeyIds) {
271+
this._transmissionKey = await generateTransmissionKey(ecKeyId, mlKemKeyId)
250272

251-
this.options.deviceConfig.transmissionKeyId = keyNumber
273+
this.options.deviceConfig.transmissionKeyId = ecKeyId
274+
this.options.deviceConfig.mlKemPublicKeyId = mlKemKeyId
252275
if (this.options.onDeviceConfig) {
253276
await this.options.onDeviceConfig(this.options.deviceConfig, this.options.host);
254277
}
255278
}
256279

257280
public async prepareRequest(payload: Uint8Array | unknown, sessionToken?: string, apiVersion?: number): Promise<Uint8Array> {
258281
this._transmissionKey = await this.getTransmissionKey()
259-
return prepareApiRequest(payload, this._transmissionKey, sessionToken, this.locale, apiVersion)
282+
return prepareApiRequest({
283+
payload,
284+
transmissionKey: this._transmissionKey,
285+
sessionToken,
286+
locale: this.locale,
287+
apiVersion,
288+
useHpkeForTransmissionKey: this.useHpkeForTransmissionKey
289+
})
260290
}
261291

262292
async decryptPushMessage(pushMessageData: Uint8Array): Promise<WssClientResponse> {
@@ -281,7 +311,12 @@ export class KeeperEndpoint {
281311
"idpSessionId": idpSessionId,
282312
"username": username
283313
}
284-
const request = await prepareApiRequest(SsoCloud.SsoCloudRequest.encode(payload).finish(), this._transmissionKey, undefined, this.locale)
314+
const request = await prepareApiRequest({
315+
payload: SsoCloud.SsoCloudRequest.encode(payload).finish(),
316+
transmissionKey: this._transmissionKey,
317+
locale: this.locale,
318+
useHpkeForTransmissionKey: this.useHpkeForTransmissionKey
319+
})
285320
return webSafe64FromBytes(request)
286321
}
287322

@@ -325,11 +360,32 @@ export async function getPushConnectionRequest(messageSessionUid: Uint8Array, tr
325360
deviceTimeStamp: new Date().getTime()
326361
})
327362
const connectionRequestBytes = WssConnectionRequest.encode(connectionRequest).finish()
328-
const apiRequest = await prepareApiRequest(connectionRequestBytes, transmissionKey, undefined, locale)
363+
const apiRequest = await prepareApiRequest({
364+
payload: connectionRequestBytes,
365+
transmissionKey,
366+
locale,
367+
useHpkeForTransmissionKey: false // HPKE not currently supported for push
368+
})
329369
return webSafe64FromBytes(apiRequest)
330370
}
331371

332-
export async function prepareApiRequest(payload: Uint8Array | unknown, transmissionKey: TransmissionKey, sessionToken?: string, locale?: string, apiVersion?: number): Promise<Uint8Array> {
372+
type PrepareApiRequestParams = {
373+
payload: Uint8Array | unknown,
374+
transmissionKey: TransmissionKey,
375+
sessionToken?: string,
376+
locale?: string,
377+
apiVersion?: number
378+
useHpkeForTransmissionKey?: boolean
379+
}
380+
381+
export async function prepareApiRequest({
382+
payload,
383+
transmissionKey,
384+
sessionToken,
385+
locale,
386+
apiVersion,
387+
useHpkeForTransmissionKey
388+
}: PrepareApiRequestParams): Promise<Uint8Array> {
333389
const requestPayload = ApiRequestPayload.create()
334390
if (payload) {
335391
requestPayload.payload = payload instanceof Uint8Array
@@ -340,14 +396,31 @@ export async function prepareApiRequest(payload: Uint8Array | unknown, transmiss
340396
requestPayload.encryptedSessionToken = normal64Bytes(sessionToken);
341397
}
342398
requestPayload.apiVersion = apiVersion || 0
343-
let requestPayloadBytes = ApiRequestPayload.encode(requestPayload).finish()
344-
let encryptedRequestPayload = await platform.aesGcmEncrypt(requestPayloadBytes, transmissionKey.key)
345-
let apiRequest = ApiRequest.create({
346-
encryptedTransmissionKey: transmissionKey.encryptedKey,
347-
encryptedPayload: encryptedRequestPayload,
348-
publicKeyId: transmissionKey.publicKeyId,
349-
locale: locale || 'en_US'
350-
})
399+
const requestPayloadBytes = ApiRequestPayload.encode(requestPayload).finish()
400+
const encryptedRequestPayload = await platform.aesGcmEncrypt(requestPayloadBytes, transmissionKey.key)
401+
let apiRequest: Authentication.IApiRequest
402+
403+
if (useHpkeForTransmissionKey) {
404+
const hpkeTransmissionKey = await generateHpkeTransmissionKey(
405+
transmissionKey,
406+
true // use optional data
407+
)
408+
apiRequest = ApiRequest.create({
409+
qrcMessageKey: hpkeTransmissionKey.qrcMessageKey,
410+
encryptedPayload: encryptedRequestPayload,
411+
publicKeyId: hpkeTransmissionKey.mlKemKeyId,
412+
encryptedTransmissionKey: hpkeTransmissionKey.optionalData || null,
413+
locale: locale || 'en_US'
414+
});
415+
} else {
416+
apiRequest = ApiRequest.create({
417+
encryptedTransmissionKey: transmissionKey.ecEncryptedKey,
418+
encryptedPayload: encryptedRequestPayload,
419+
publicKeyId: transmissionKey.ecKeyId,
420+
locale: locale || 'en_US'
421+
})
422+
}
423+
351424
return ApiRequest.encode(apiRequest).finish()
352425
}
353426

0 commit comments

Comments
 (0)