Skip to content

Commit d247a69

Browse files
committed
ML-KEM wrapped transmission key MVP
1 parent 31e58bd commit d247a69

File tree

18 files changed

+1351
-37
lines changed

18 files changed

+1351
-37
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: 20 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+
// TODO: add option here to opt in to use HPKE for transmission key exchange
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,23 @@ export interface VendorConfiguration {
7477

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

8199
export interface AuthUI {

keeperapi/src/endpoint.ts

Lines changed: 42 additions & 20 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,24 @@ export class KeeperEndpoint {
3738
private onsitePrivateKey: Uint8Array | null = null
3839
private onsitePublicKey: Uint8Array | null = null
3940

40-
constructor(private options: ClientConfigurationInternal) {
41+
constructor(private options: ClientConfigurationInternal) {
4142
if (options.deviceToken) {
4243
this.deviceToken = options.deviceToken
43-
}
44+
}
4445
if (options.locale) {
4546
this.locale = options.locale
4647
}
4748
}
4849

4950
async getTransmissionKey():Promise<TransmissionKey> {
50-
const deviceConfigTransmissionKeyId = this.options.deviceConfig.transmissionKeyId || 7
51-
if(!this._transmissionKey && isAllowedNumber(deviceConfigTransmissionKeyId)){
52-
this._transmissionKey = await generateTransmissionKey(deviceConfigTransmissionKeyId)
51+
const DEFAULT_PROD_EC_KEY_ID = 10
52+
const DEFAULT_PROD_ML_KEM_KEY_ID = 100
53+
const deviceConfigTransmissionKeyId = this.options.deviceConfig.transmissionKeyId || DEFAULT_PROD_EC_KEY_ID
54+
const deviceConfigMlKemKeyId = this.options.deviceConfig.mlKemPublicKeyId || DEFAULT_PROD_ML_KEM_KEY_ID
55+
if(!this._transmissionKey && isAllowedEcKeyId(deviceConfigTransmissionKeyId) && isAllowedMlKemKeyId(deviceConfigMlKemKeyId)){
56+
this._transmissionKey = await generateTransmissionKey(deviceConfigTransmissionKeyId, deviceConfigMlKemKeyId)
5357
} else if(!this._transmissionKey){
54-
this._transmissionKey = await generateTransmissionKey(7)
58+
this._transmissionKey = await generateTransmissionKey(DEFAULT_PROD_EC_KEY_ID, DEFAULT_PROD_ML_KEM_KEY_ID)
5559
}
5660

5761
return this._transmissionKey
@@ -159,7 +163,8 @@ export class KeeperEndpoint {
159163
while (true) {
160164
const payload = 'toBytes' in message ? message.toBytes() : new Uint8Array()
161165
const apiVersion = message.apiVersion || 0
162-
const request = await this.prepareRequest(payload, sessionToken, apiVersion)
166+
167+
const request = await prepareApiRequest(payload, this._transmissionKey, sessionToken, this.locale, apiVersion)
163168
log(`Calling REST URL: ${this.getUrl(message.path)}`, 'noCR');
164169
const startTime = Date.now()
165170
const response = await platform.post(this.getUrl(message.path), request)
@@ -183,8 +188,9 @@ export class KeeperEndpoint {
183188
const errorObj: KeeperError = JSON.parse(errorMessage)
184189
switch (errorObj.error) {
185190
case 'key':
186-
if(isAllowedNumber(errorObj.key_id!)){
187-
await this.updateTransmissionKey(errorObj.key_id!)
191+
// NOTE: I believe this is subject to change, because the new logic is not backward compatible
192+
if(isAllowedEcKeyId(errorObj.qrc_ec_key_id!) && isAllowedMlKemKeyId(errorObj.key_id!)){
193+
await this.updateTransmissionKey(errorObj.qrc_ec_key_id!, errorObj.key_id!)
188194
} else {
189195
throw new Error('Incorrect Transmission Key ID being used.')
190196
}
@@ -245,10 +251,11 @@ export class KeeperEndpoint {
245251
return platform.get(this.getUrl(path), {})
246252
}
247253

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

251-
this.options.deviceConfig.transmissionKeyId = keyNumber
257+
this.options.deviceConfig.transmissionKeyId = ecKeyId
258+
this.options.deviceConfig.mlKemPublicKeyId = mlKemKeyId
252259
if (this.options.onDeviceConfig) {
253260
await this.options.onDeviceConfig(this.options.deviceConfig, this.options.host);
254261
}
@@ -329,7 +336,13 @@ export async function getPushConnectionRequest(messageSessionUid: Uint8Array, tr
329336
return webSafe64FromBytes(apiRequest)
330337
}
331338

332-
export async function prepareApiRequest(payload: Uint8Array | unknown, transmissionKey: TransmissionKey, sessionToken?: string, locale?: string, apiVersion?: number): Promise<Uint8Array> {
339+
export async function prepareApiRequest(
340+
payload: Uint8Array | unknown,
341+
transmissionKey: TransmissionKey,
342+
sessionToken?: string,
343+
locale?: string,
344+
apiVersion?: number
345+
): Promise<Uint8Array> {
333346
const requestPayload = ApiRequestPayload.create()
334347
if (payload) {
335348
requestPayload.payload = payload instanceof Uint8Array
@@ -340,14 +353,23 @@ export async function prepareApiRequest(payload: Uint8Array | unknown, transmiss
340353
requestPayload.encryptedSessionToken = normal64Bytes(sessionToken);
341354
}
342355
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,
356+
const requestPayloadBytes = ApiRequestPayload.encode(requestPayload).finish()
357+
const encryptedRequestPayload = await platform.aesGcmEncrypt(requestPayloadBytes, transmissionKey.key)
358+
359+
// Use QRC to wrap the transmission key
360+
const hpkeTransmissionKey = await generateHpkeTransmissionKey(
361+
transmissionKey,
362+
true // use optional data
363+
)
364+
365+
const apiRequest = ApiRequest.create({
366+
qrcMessageKey: hpkeTransmissionKey.qrcMessageKey,
347367
encryptedPayload: encryptedRequestPayload,
348-
publicKeyId: transmissionKey.publicKeyId,
368+
publicKeyId: hpkeTransmissionKey.publicKeyId, // ML-KEM key ID
369+
encryptedTransmissionKey: hpkeTransmissionKey.optionalData || null, // Optional data or empty
349370
locale: locale || 'en_US'
350-
})
371+
});
372+
351373
return ApiRequest.encode(apiRequest).finish()
352374
}
353375

keeperapi/src/node/platform.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
UnwrappedKeyType
1616
} from "../platform";
1717
import {RSA_PKCS1_PADDING} from "constants";
18-
import {getKeeperKeys} from "../transmissionKeys";
18+
import {getKeeperKeys, getKeeperMlKemKeys} from "../transmissionKeys";
1919
import {SocketProxy, socketSendMessage} from '../socket'
2020
import {normal64} from "../utils";
2121
import type {KeeperHttpResponse} from "../commands";
@@ -35,7 +35,8 @@ export const nodePlatform: Platform = class {
3535
}
3636

3737
static keys = getKeeperKeys(this.normal64Bytes);
38-
38+
static mlKemKeys = getKeeperMlKemKeys(this.base64ToBytes);
39+
3940
static getRandomBytes(length: number): Uint8Array {
4041
return crypto.randomBytes(length);
4142
}
@@ -305,7 +306,11 @@ export const nodePlatform: Platform = class {
305306
}
306307

307308
static calcAuthVerifier(key: Uint8Array): Promise<Uint8Array> {
308-
return Promise.resolve(crypto.createHash("SHA256").update(key).digest());
309+
return this.sha256(key);
310+
}
311+
312+
static sha256(data: Uint8Array): Promise<Uint8Array> {
313+
return Promise.resolve(crypto.createHash("SHA256").update(data).digest());
309314
}
310315

311316
static get(

keeperapi/src/platform.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {CryptoWorkerPool, CryptoWorkerPoolConfig} from './cryptoWorker';
44

55
export interface Platform {
66
keys: Uint8Array[];
7+
mlKemKeys: Uint8Array[]; // ML-KEM public keys for QRC encryption
78

89
supportsConcurrency: boolean
910

@@ -75,6 +76,8 @@ export interface Platform {
7576

7677
calcAuthVerifier(key: Uint8Array): Promise<Uint8Array>;
7778

79+
sha256(data: Uint8Array): Promise<Uint8Array>;
80+
7881
get(url: string, headers: any): Promise<KeeperHttpResponse>;
7982

8083
post(url: string, request: Uint8Array, headers?: any): Promise<KeeperHttpResponse>;

0 commit comments

Comments
 (0)