From f09949ad0a019fc731d03fe876d0a187359603a2 Mon Sep 17 00:00:00 2001 From: Tyler Carson Date: Mon, 27 Apr 2026 17:11:04 -0700 Subject: [PATCH] Process recordRotations in syncDown Adds processRecordRotations to the syncDown loop so that PAM rotation data (schedule, lastRotation, configurationUid, etc.) is passed through VaultStorage.put() instead of being silently ignored. --- keeperapi/package-lock.json | 4 +- keeperapi/package.json | 2 +- .../src/__tests__/SyncDownResponseBuilder.ts | 5 + keeperapi/src/__tests__/vault.test.ts | 98 +++++++++++++++++++ keeperapi/src/vault.ts | 54 ++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) diff --git a/keeperapi/package-lock.json b/keeperapi/package-lock.json index 45bcdf5..75fab9f 100644 --- a/keeperapi/package-lock.json +++ b/keeperapi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@keeper-security/keeperapi", - "version": "17.1.1", + "version": "17.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@keeper-security/keeperapi", - "version": "17.1.1", + "version": "17.1.3", "license": "ISC", "dependencies": { "@noble/post-quantum": "^0.5.2", diff --git a/keeperapi/package.json b/keeperapi/package.json index 68d5bde..ee4b576 100644 --- a/keeperapi/package.json +++ b/keeperapi/package.json @@ -1,7 +1,7 @@ { "name": "@keeper-security/keeperapi", "description": "Keeper API Javascript SDK", - "version": "17.1.2", + "version": "17.1.3", "browser": "dist/index.es.js", "main": "dist/index.cjs.js", "types": "dist/node/index.d.ts", diff --git a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts index 82f5998..aee3cbb 100644 --- a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts +++ b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts @@ -77,9 +77,14 @@ export class SyncDownResponseBuilder { removedSharedFolderFolderRecords: [], removedSharedFolders: [], removedUsers: [], + recordRotations: [], } } + addRecordRotation(recordRotation: Vault.IRecordRotation) { + this.data.recordRotations?.push(recordRotation) + } + addUserFolderRecord(userFolderRecord: Vault.IUserFolderRecord) { this.data.userFolderRecords?.push(userFolderRecord) } diff --git a/keeperapi/src/__tests__/vault.test.ts b/keeperapi/src/__tests__/vault.test.ts index 4514478..cfc23c9 100644 --- a/keeperapi/src/__tests__/vault.test.ts +++ b/keeperapi/src/__tests__/vault.test.ts @@ -2145,4 +2145,102 @@ describe('Sync Down', () => { it('does nothing when a linked record is deleted - shared linked record', async () => {}) }) }) + describe('Record Rotations', () => { + it('saves the rotation data when record rotations are included in the sync', async () => { + const enabledRecordUid = platform.getRandomBytes(16) + const enabledRecordUidStr = webSafe64FromBytes(enabledRecordUid) + const enabledConfigurationUid = platform.getRandomBytes(16) + const enabledResourceUid = platform.getRandomBytes(16) + const enabledRevision = Date.now() + const enabledSchedule = '[{"type":"DAILY","time":"02:00:00","tz":"America/Los_Angeles","intervalCount":1}]' + syncDownResponseBuilder.addRecordRotation({ + recordUid: enabledRecordUid, + revision: enabledRevision, + configurationUid: enabledConfigurationUid, + resourceUid: enabledResourceUid, + schedule: enabledSchedule, + lastRotation: 1700000000, + lastRotationStatus: Vault.RecordRotationStatus.RRST_SUCCESS, + disabled: false, + }) + const disabledRecordUid = platform.getRandomBytes(16) + const disabledRecordUidStr = webSafe64FromBytes(disabledRecordUid) + const disabledConfigurationUid = platform.getRandomBytes(16) + const disabledResourceUid = platform.getRandomBytes(16) + const disabledRevision = Date.now() + const disabledSchedule = + '[{"type":"WEEKLY","weekday":"MONDAY","intervalCount":2,"time":"17:00:00","tz":"Europe/Amsterdam"},{"type":"WEEKLY","weekday":"FRIDAY","intervalCount":2,"time":"17:00:00","tz":"Europe/Amsterdam"}]' + syncDownResponseBuilder.addRecordRotation({ + recordUid: disabledRecordUid, + revision: disabledRevision, + configurationUid: disabledConfigurationUid, + resourceUid: disabledResourceUid, + schedule: disabledSchedule, + lastRotation: 1700000001, + lastRotationStatus: Vault.RecordRotationStatus.RRST_FAILURE, + disabled: true, + }) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ auth, storage }) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + kind: 'record_rotation', + uid: enabledRecordUidStr, + revision: enabledRevision, + configurationUid: webSafe64FromBytes(enabledConfigurationUid), + resourceUid: webSafe64FromBytes(enabledResourceUid), + schedule: enabledSchedule, + lastRotation: 1700000000, + lastRotationStatus: Vault.RecordRotationStatus.RRST_SUCCESS, + disabled: false, + }) + ) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + kind: 'record_rotation', + uid: disabledRecordUidStr, + revision: disabledRevision, + configurationUid: webSafe64FromBytes(disabledConfigurationUid), + resourceUid: webSafe64FromBytes(disabledResourceUid), + schedule: disabledSchedule, + lastRotation: 1700000001, + lastRotationStatus: Vault.RecordRotationStatus.RRST_FAILURE, + disabled: true, + }) + ) + }) + it('decrypts pwdComplexity when present', async () => { + const recordUid = platform.getRandomBytes(16) + const recordUidStr = webSafe64FromBytes(recordUid) + const recordKey = platform.getRandomBytes(32) + await platform.importKey(recordUidStr, recordKey) + const pwdComplexity = { + length: 20, + caps: 4, + lowercase: 4, + digits: 4, + special: 4, + } + const encryptedPwdComplexity = await platform.aesGcmEncrypt( + platform.stringToBytes(JSON.stringify(pwdComplexity)), + recordKey + ) + syncDownResponseBuilder.addRecordRotation({ + recordUid, + revision: Date.now(), + schedule: '[{"type":"CRON","cron":"0 0 0 1 1/3 ?","tz":"America/Los_Angeles"}]', + pwdComplexity: encryptedPwdComplexity, + disabled: false, + }) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ auth, storage }) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + kind: 'record_rotation', + uid: recordUidStr, + pwdComplexity, + }) + ) + }) + }) }) diff --git a/keeperapi/src/vault.ts b/keeperapi/src/vault.ts index 7f164a1..9e08c86 100644 --- a/keeperapi/src/vault.ts +++ b/keeperapi/src/vault.ts @@ -55,6 +55,7 @@ export type VaultStorageData = | DBWSecurityData | DSecurityScoreData | DUser + | DRecordRotation export type VaultStorageKind = | 'profilePic' @@ -75,6 +76,7 @@ export type VaultStorageKind = | 'bw_security_data' | 'security_score_data' | 'user' + | 'record_rotation' export type VaultStorageResult = | (T extends 'continuationToken' ? DContinuationToken : T extends 'record' ? DRecord : never) @@ -249,6 +251,28 @@ export type DUser = { username: string } +export type DPwdComplexity = { + length: number + caps: number + lowercase: number + digits: number + special: number + specialChars?: string +} + +export type DRecordRotation = { + kind: 'record_rotation' + uid: string + revision: number + configurationUid: string + resourceUid: string + schedule: string + lastRotation?: number + lastRotationStatus?: Vault.RecordRotationStatus + pwdComplexity?: DPwdComplexity + disabled: boolean +} + export type DContinuationToken = { kind?: 'continuationToken' token: string @@ -1093,6 +1117,34 @@ export type SyncDownOptions = { controller?: SyncController } +const processRecordRotations = async (rotations: Vault.IRecordRotation[] | null | undefined, storage: VaultStorage) => { + if (!rotations?.length) return + for (const r of rotations) { + if (!r.recordUid) continue + const uid = webSafe64FromBytes(r.recordUid) + try { + const pwdComplexityData = r.pwdComplexity?.byteLength + ? await platform.decrypt(r.pwdComplexity, uid, 'gcm', storage) + : undefined + const pwdComplexity = pwdComplexityData ? JSON.parse(platform.bytesToString(pwdComplexityData)) : undefined + await storage.put({ + kind: 'record_rotation', + uid, + revision: r.revision ? Number(r.revision) : 0, + configurationUid: r.configurationUid ? webSafe64FromBytes(r.configurationUid) : '', + resourceUid: r.resourceUid ? webSafe64FromBytes(r.resourceUid) : '', + schedule: r.schedule || '', + lastRotation: r.lastRotation ? Number(r.lastRotation) : undefined, + lastRotationStatus: r.lastRotationStatus ?? undefined, + pwdComplexity, + disabled: r.disabled === true, + }) + } catch { + console.error(`The record rotation ${uid} could not be processed`) + } + } +} + export class SyncController { aborted: boolean = false @@ -1196,6 +1248,8 @@ export const syncDown = async (options: SyncDownOptions): Promise => await processRecords(resp.records, storage) + await processRecordRotations(resp.recordRotations, storage) + await processNonSharedData(resp.nonSharedData, storage) await processSharedFolderFolders(resp.sharedFolderFolders, storage, dependencies)