Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions keeperapi/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion keeperapi/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
5 changes: 5 additions & 0 deletions keeperapi/src/__tests__/SyncDownResponseBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
98 changes: 98 additions & 0 deletions keeperapi/src/__tests__/vault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
)
})
})
})
54 changes: 54 additions & 0 deletions keeperapi/src/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type VaultStorageData =
| DBWSecurityData
| DSecurityScoreData
| DUser
| DRecordRotation

export type VaultStorageKind =
| 'profilePic'
Expand All @@ -75,6 +76,7 @@ export type VaultStorageKind =
| 'bw_security_data'
| 'security_score_data'
| 'user'
| 'record_rotation'

export type VaultStorageResult<T extends VaultStorageKind> =
| (T extends 'continuationToken' ? DContinuationToken : T extends 'record' ? DRecord : never)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -1196,6 +1248,8 @@ export const syncDown = async (options: SyncDownOptions): Promise<SyncResult> =>

await processRecords(resp.records, storage)

await processRecordRotations(resp.recordRotations, storage)

await processNonSharedData(resp.nonSharedData, storage)

await processSharedFolderFolders(resp.sharedFolderFolders, storage, dependencies)
Expand Down
Loading