Skip to content

Commit 7b5fa9d

Browse files
committed
ML-KEM wrapped transmission key MVP
1 parent e8962f7 commit 7b5fa9d

File tree

17 files changed

+1512
-50
lines changed

17 files changed

+1512
-50
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: 2 additions & 1 deletion
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);

keeperapi/src/configuration.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface DeviceConfig {
4848
privateKey?: Uint8Array
4949
publicKey?: Uint8Array
5050
transmissionKeyId?: number
51+
mlKemPublicKeyId?: number // ML-KEM key ID for QRC encryption (optional)
5152
}
5253

5354
export interface SessionStorage {
@@ -78,6 +79,21 @@ export interface TransmissionKey {
7879
encryptedKey: Uint8Array
7980
}
8081

82+
export interface QrcMessageKey {
83+
clientEcPublicKey: Uint8Array
84+
mlKemEncapsulatedKey: Uint8Array
85+
data: Uint8Array
86+
msgVersion: number
87+
ecKeyId: number
88+
}
89+
90+
export interface TransmissionKeyQrc {
91+
key: Uint8Array
92+
publicKeyId: number // ML-KEM key ID
93+
qrcMessageKey: QrcMessageKey
94+
optionalData?: Uint8Array
95+
}
96+
8197
export interface AuthUI {
8298
getTwoFactorCode(errorMessage?: string): Promise<string>;
8399
displayDialog(): Promise<boolean>;

keeperapi/src/endpoint.ts

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {platform} from './platform'
44
import {
55
formatTimeDiff,
66
generateTransmissionKey,
7+
generateTransmissionKeyQrc,
78
getKeeperUrl,
89
isTwoFactorResultCode, log,
910
normal64Bytes,
@@ -17,7 +18,7 @@ import {
1718
registerDeviceInRegionMessage,
1819
updateDeviceMessage
1920
} from './restMessages'
20-
import {ClientConfigurationInternal, TransmissionKey} from './configuration';
21+
import {ClientConfigurationInternal, TransmissionKey, TransmissionKeyQrc} from './configuration';
2122
import ApiRequestPayload = Authentication.ApiRequestPayload;
2223
import ApiRequest = Authentication.ApiRequest;
2324
import IDeviceResponse = Authentication.IDeviceResponse;
@@ -37,10 +38,10 @@ 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
}
@@ -159,7 +160,35 @@ export class KeeperEndpoint {
159160
while (true) {
160161
const payload = 'toBytes' in message ? message.toBytes() : new Uint8Array()
161162
const apiVersion = message.apiVersion || 0
162-
const request = await this.prepareRequest(payload, sessionToken, apiVersion)
163+
164+
// Determine whether to use QRC or traditional EC encryption
165+
const mlKemPublicKeyId = this.options.deviceConfig.mlKemPublicKeyId || 100
166+
167+
// Use QRC to wrap the transmission key
168+
const ecKeyId = this.options.deviceConfig.transmissionKeyId || 7
169+
if (!isAllowedNumber(ecKeyId)) {
170+
throw new Error(`Invalid EC key ID: ${ecKeyId}`)
171+
}
172+
173+
const serverEcPublicKey = platform.keys[ecKeyId]
174+
const serverMlKemPublicKey = platform.mlKemKeys[mlKemPublicKeyId]
175+
176+
if (!serverEcPublicKey) {
177+
throw new Error(`EC public key not found for ID: ${ecKeyId}`)
178+
}
179+
if (!serverMlKemPublicKey) {
180+
throw new Error(`ML-KEM public key not found for ID: ${mlKemPublicKeyId}`)
181+
}
182+
183+
const requestTransmissionKey = await generateTransmissionKeyQrc(
184+
mlKemPublicKeyId,
185+
serverEcPublicKey,
186+
serverMlKemPublicKey,
187+
ecKeyId,
188+
true // use optional data
189+
)
190+
191+
const request = await prepareApiRequest(payload, requestTransmissionKey, sessionToken, this.locale, apiVersion)
163192
log(`Calling REST URL: ${this.getUrl(message.path)}`, 'noCR');
164193
const startTime = Date.now()
165194
const response = await platform.post(this.getUrl(message.path), request)
@@ -174,7 +203,7 @@ export class KeeperEndpoint {
174203
console.log("Response code:", response.statusCode);
175204
}
176205
try {
177-
const decrypted = await platform.aesGcmDecrypt(response.data, this._transmissionKey.key)
206+
const decrypted = await platform.aesGcmDecrypt(response.data, requestTransmissionKey.key)
178207
if ('fromBytes' in message) return message.fromBytes(decrypted)
179208
return
180209
} catch {
@@ -256,7 +285,41 @@ export class KeeperEndpoint {
256285

257286
public async prepareRequest(payload: Uint8Array | unknown, sessionToken?: string, apiVersion?: number): Promise<Uint8Array> {
258287
this._transmissionKey = await this.getTransmissionKey()
259-
return prepareApiRequest(payload, this._transmissionKey, sessionToken, this.locale, apiVersion)
288+
289+
// Determine whether to use QRC or traditional EC encryption
290+
const mlKemPublicKeyId = this.options.deviceConfig.mlKemPublicKeyId
291+
let requestTransmissionKey: TransmissionKey | TransmissionKeyQrc
292+
293+
if (mlKemPublicKeyId) {
294+
// Use QRC to wrap the transmission key
295+
const ecKeyId = this.options.deviceConfig.transmissionKeyId || 7
296+
if (!isAllowedNumber(ecKeyId)) {
297+
throw new Error(`Invalid EC key ID: ${ecKeyId}`)
298+
}
299+
300+
const serverEcPublicKey = platform.keys[ecKeyId]
301+
const serverMlKemPublicKey = platform.mlKemKeys[mlKemPublicKeyId]
302+
303+
if (!serverEcPublicKey) {
304+
throw new Error(`EC public key not found for ID: ${ecKeyId}`)
305+
}
306+
if (!serverMlKemPublicKey) {
307+
throw new Error(`ML-KEM public key not found for ID: ${mlKemPublicKeyId}`)
308+
}
309+
310+
requestTransmissionKey = await generateTransmissionKeyQrc(
311+
mlKemPublicKeyId,
312+
serverEcPublicKey,
313+
serverMlKemPublicKey,
314+
ecKeyId,
315+
true // use optional data
316+
)
317+
} else {
318+
// Use traditional EC encryption
319+
requestTransmissionKey = this._transmissionKey
320+
}
321+
322+
return prepareApiRequest(payload, requestTransmissionKey, sessionToken, this.locale, apiVersion)
260323
}
261324

262325
async decryptPushMessage(pushMessageData: Uint8Array): Promise<WssClientResponse> {
@@ -329,7 +392,13 @@ export async function getPushConnectionRequest(messageSessionUid: Uint8Array, tr
329392
return webSafe64FromBytes(apiRequest)
330393
}
331394

332-
export async function prepareApiRequest(payload: Uint8Array | unknown, transmissionKey: TransmissionKey, sessionToken?: string, locale?: string, apiVersion?: number): Promise<Uint8Array> {
395+
export async function prepareApiRequest(
396+
payload: Uint8Array | unknown,
397+
transmissionKey: TransmissionKey | TransmissionKeyQrc,
398+
sessionToken?: string,
399+
locale?: string,
400+
apiVersion?: number
401+
): Promise<Uint8Array> {
333402
const requestPayload = ApiRequestPayload.create()
334403
if (payload) {
335404
requestPayload.payload = payload instanceof Uint8Array
@@ -342,12 +411,37 @@ export async function prepareApiRequest(payload: Uint8Array | unknown, transmiss
342411
requestPayload.apiVersion = apiVersion || 0
343412
let requestPayloadBytes = ApiRequestPayload.encode(requestPayload).finish()
344413
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-
})
414+
415+
// Check if this is a QRC transmission key
416+
const isQrc = 'qrcMessageKey' in transmissionKey;
417+
418+
let apiRequest: Authentication.IApiRequest;
419+
if (isQrc) {
420+
// Use QRC encryption
421+
const qrcKey = transmissionKey as TransmissionKeyQrc;
422+
apiRequest = ApiRequest.create({
423+
qrcMessageKey: {
424+
clientEcPublicKey: qrcKey.qrcMessageKey.clientEcPublicKey,
425+
mlKemEncapsulatedKey: qrcKey.qrcMessageKey.mlKemEncapsulatedKey,
426+
data: qrcKey.qrcMessageKey.data,
427+
msgVersion: qrcKey.qrcMessageKey.msgVersion,
428+
ecKeyId: qrcKey.qrcMessageKey.ecKeyId
429+
},
430+
encryptedPayload: encryptedRequestPayload,
431+
publicKeyId: qrcKey.publicKeyId, // ML-KEM key ID
432+
encryptedTransmissionKey: qrcKey.optionalData || new Uint8Array(0), // Optional data or empty
433+
locale: locale || 'en_US'
434+
});
435+
} else {
436+
// Use traditional EC encryption
437+
apiRequest = ApiRequest.create({
438+
encryptedTransmissionKey: transmissionKey.encryptedKey,
439+
encryptedPayload: encryptedRequestPayload,
440+
publicKeyId: transmissionKey.publicKeyId,
441+
locale: locale || 'en_US'
442+
});
443+
}
444+
351445
return ApiRequest.encode(apiRequest).finish()
352446
}
353447

keeperapi/src/node/platform.ts

Lines changed: 3 additions & 2 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
}

keeperapi/src/platform.ts

Lines changed: 1 addition & 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

0 commit comments

Comments
 (0)