From b5bebdf8c4e9e4ea3e5dc3845cc4f76932a26852 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 20 Jul 2026 20:45:43 +0530 Subject: [PATCH 1/2] Added support for Enterprise role and node commands --- keeperapi/src/browser/platform.ts | 39 ++++++--- keeperapi/src/commands.ts | 138 ++++++++++++++++++++++++++++++ keeperapi/src/restMessages.ts | 5 ++ keeperapi/src/vault.ts | 7 +- 4 files changed, 173 insertions(+), 16 deletions(-) diff --git a/keeperapi/src/browser/platform.ts b/keeperapi/src/browser/platform.ts index c3a00dfa..fc1b55b1 100644 --- a/keeperapi/src/browser/platform.ts +++ b/keeperapi/src/browser/platform.ts @@ -86,7 +86,7 @@ export const browserPlatform: Platform = class { static async importKey(keyId: string, key: Uint8Array, storage?: KeyStorage, canExport?: boolean): Promise { // An AES key for one of our Keeper objects can be used for either CBC or GCM operations. // Since CryptoKeys are bound to a particular algorithm, we need to keep a copy for each. - const extractable = !!canExport + const extractable = typeof storage?.saveKeyBytes === 'function' ? true : !!canExport const cbcKey = await this.aesCbcImportKey(key, extractable) const gcmKey = await this.aesGcmImportKey(key, extractable) cryptoKeysCache['cbc'][keyId] = cbcKey @@ -96,7 +96,8 @@ export const browserPlatform: Platform = class { if (storage.saveObject) { await storage.saveObject(this.getStorageKeyId(keyId, 'cbc'), cbcKey) await storage.saveObject(this.getStorageKeyId(keyId, 'gcm'), gcmKey) - } else { + } + if (storage.saveKeyBytes) { await storage.saveKeyBytes(keyId, key) } } @@ -114,7 +115,8 @@ export const browserPlatform: Platform = class { if (storage) { if (storage.saveObject) { await storage.saveObject(this.getStorageKeyId(keyId, 'ecc'), key) - } else { + } + if (storage.saveKeyBytes) { const jwk = await crypto.subtle.exportKey('jwk', key) const keyBytes = this.stringToBytes(JSON.stringify(jwk)) await storage.saveKeyBytes(keyId, keyBytes) @@ -168,10 +170,9 @@ export const browserPlatform: Platform = class { if (storage?.getObject) { const storageKeyId = this.getStorageKeyId(keyId, keyType) const storedKey = await storage.getObject(storageKeyId) - if (!storedKey) { - throw new Error('Unable to load crypto key ' + keyId) + if (storedKey) { + return storedKey } - return storedKey } const keyBytes = await this.loadKeyBytes(keyId, storage) @@ -212,6 +213,20 @@ export const browserPlatform: Platform = class { return key } + static async ensureAesKeyLoaded(keyId: string, storage?: KeyStorage): Promise { + if (cryptoKeysCache['cbc'][keyId] && cryptoKeysCache['gcm'][keyId]) { + return true + } + if (storage?.getKeyBytes) { + const keyBytes = await storage.getKeyBytes(keyId) + if (keyBytes) { + await this.importKey(keyId, keyBytes, storage, true) + return true + } + } + return false + } + static async unwrapKeys(keys: UnwrapKeyMap, storage?: KeyStorage): Promise { if (workerPool) { try { @@ -288,9 +303,7 @@ export const browserPlatform: Platform = class { await this.unwrapRSAKey(key, keyId, unwrappingKeyId, encryptionType, storage) break case 'aes': - if (cryptoKeysCache['gcm'][keyId]) { - // Keeperapp sometimes provides redundant key data, for example, like if you own a record in a shared folder, - // or if a record belongs to multiple shared folders. So, short circuit when possible for a performance improvement + if (await this.ensureAesKeyLoaded(keyId, storage)) { return } @@ -298,7 +311,7 @@ export const browserPlatform: Platform = class { break // TODO: add something like this, need to find pub/priv key pair case 'ecc': - if (cryptoKeysCache['gcm'][keyId]) { + if (cryptoKeysCache['ecc'][keyId]) { return } @@ -365,7 +378,8 @@ export const browserPlatform: Platform = class { break } - const canExtract: boolean = storage?.saveObject ? !!canExport : true + const mustPersistKeyBytes = typeof storage?.saveKeyBytes === 'function' + const canExtract: boolean = mustPersistKeyBytes ? true : storage?.saveObject ? !!canExport : true const keyUsages: KeyUsage[] = ['encrypt', 'decrypt', 'unwrapKey', 'wrapKey'] const gcmKey = await crypto.subtle.unwrapKey( @@ -394,7 +408,8 @@ export const browserPlatform: Platform = class { if (storage.saveObject) { await storage.saveObject(this.getStorageKeyId(keyId, 'cbc'), cbcKey) await storage.saveObject(this.getStorageKeyId(keyId, 'gcm'), gcmKey) - } else { + } + if (storage.saveKeyBytes) { const keyBuffer = await crypto.subtle.exportKey('raw', gcmKey) await storage.saveKeyBytes(keyId, new Uint8Array(keyBuffer)) } diff --git a/keeperapi/src/commands.ts b/keeperapi/src/commands.ts index 6e566b34..eb43a8dc 100644 --- a/keeperapi/src/commands.ts +++ b/keeperapi/src/commands.ts @@ -211,6 +211,25 @@ export const enterpriseAllocateIdsCommand = ( ): RestCommand => createCommand(request, 'enterprise_allocate_ids') +export type NodeEditRequest = { + node_id: number + parent_id?: number + encrypted_data: string +} + +export const nodeAddCommand = (request: NodeEditRequest): RestCommand => + createCommand(request, 'node_add') + +export const nodeUpdateCommand = (request: NodeEditRequest): RestCommand => + createCommand(request, 'node_update') + +export type NodeDeleteRequest = { + node_id: number +} + +export const nodeDeleteCommand = (request: NodeDeleteRequest): RestCommand => + createCommand(request, 'node_delete') + export type RoleEditRequest = { role_id: number node_id: number @@ -232,6 +251,115 @@ export type RoleDeleteRequest = { export const roleDeleteCommand = (request: RoleDeleteRequest): RestCommand => createCommand(request, 'role_delete') +export type RoleUserAddRequest = { + role_id: number + enterprise_user_id: number + tree_key?: string + tree_key_type?: string | number + role_admin_key?: string + role_admin_key_type?: string | number +} + +export const roleUserAddCommand = (request: RoleUserAddRequest): RestCommand => + createCommand(request, 'role_user_add') + +export type RoleUserRemoveRequest = { + role_id: number + enterprise_user_id: number +} + +export const roleUserRemoveCommand = ( + request: RoleUserRemoveRequest +): RestCommand => createCommand(request, 'role_user_remove') + +export type RoleManagedNodeTreeKey = { + enterprise_user_id: number + tree_key: string + tree_key_type?: string | number +} + +export type RoleManagedNodeAddResponse = KeeperResponse & { + missing_tree_keys?: Record + missing_ecc_tree_keys?: Record +} + +export type RoleManagedNodeAddRequest = { + role_id: number + managed_node_id: number + cascade_node_management?: boolean + tree_keys?: RoleManagedNodeTreeKey[] +} + +export const roleManagedNodeAddCommand = ( + request: RoleManagedNodeAddRequest +): RestCommand => createCommand(request, 'role_managed_node_add') + +export type RoleManagedNodeUpdateRequest = { + role_id: number + managed_node_id: number + cascade_node_management?: boolean + tree_keys?: RoleManagedNodeTreeKey[] +} + +export const roleManagedNodeUpdateCommand = ( + request: RoleManagedNodeUpdateRequest +): RestCommand => createCommand(request, 'role_managed_node_update') + +export type RoleManagedNodeRemoveRequest = { + role_id: number + managed_node_id: number +} + +export const roleManagedNodeRemoveCommand = ( + request: RoleManagedNodeRemoveRequest +): RestCommand => createCommand(request, 'role_managed_node_remove') + +export type ManagedNodeRoleKey = { + enterprise_user_id: number + role_key: string + tree_key_type?: string | number +} + +export type ManagedNodeMspKey = { + mc_enterprise_id: number + tree_key: string +} + +export type ManagedNodePrivilegeAddRequest = { + role_id: number + managed_node_id: number + privilege: string + role_key_enc_with_tree_key?: string + role_public_key?: string + role_private_key?: string + role_keys?: ManagedNodeRoleKey[] + msp_keys?: ManagedNodeMspKey[] +} + +export type ManagedNodePrivilegeAddResponse = KeeperResponse & { + status?: { + missing_keys?: Record + invalid_users?: number[] + cant_be_pending?: number[] + } +} + +export const managedNodePrivilegeAddCommand = ( + request: ManagedNodePrivilegeAddRequest +): RestCommand => + createCommand(request, 'managed_node_privilege_add') + +export type ManagedNodePrivilegeRemoveRequest = { + role_id: number + managed_node_id: number + privilege: string +} + +export const managedNodePrivilegeRemoveCommand = ( + request: ManagedNodePrivilegeRemoveRequest +): RestCommand => + createCommand(request, 'managed_node_privilege_remove') + export type MoveRequest = { to_type: 'user_folder' | 'shared_folder' | 'shared_folder_folder' to_uid?: string @@ -379,6 +507,16 @@ export const teamEnterpriseUserRemoveCommand = ( request: TeamUserCommandRequest ): RestCommand => createCommand(request, 'team_enterprise_user_remove') +export type TeamEnterpriseUserUpdateRequest = { + team_uid: string + enterprise_user_id: number + user_type: number +} + +export const teamEnterpriseUserUpdateCommand = ( + request: TeamEnterpriseUserUpdateRequest +): RestCommand => createCommand(request, 'team_enterprise_user_update') + export type TeamGetKeysRequest = { teams: string[] } diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index e8301747..7fbd629d 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -640,6 +640,11 @@ export const enterpriseUsersLockMessage = ( ): RestMessage => createMessage(data, 'enterprise/enterprise_users_lock', Enterprise.LockUsersRequest, Enterprise.LockUsersResponse) +export const disableTwoFaMessage = ( + data: Enterprise.IEnterpriseUserIds +): RestInMessage => + createInMessage(data, 'enterprise/disable_two_fa', Enterprise.EnterpriseUserIds) + export const setV2AlternatePasswordMessage = ( data: Authentication.IUserAuthRequest ): RestInMessage => diff --git a/keeperapi/src/vault.ts b/keeperapi/src/vault.ts index 9efb78f4..96723eb2 100644 --- a/keeperapi/src/vault.ts +++ b/keeperapi/src/vault.ts @@ -1597,15 +1597,14 @@ export const syncDown = async (options: SyncDownOptions): Promise => const dToken = await storage.get('continuationToken') let continuationToken = dToken ? platform.base64ToBytes(dToken.token) : undefined - await platform.importKey('data', auth.dataKey!, undefined, true) + await platform.importKey('data', auth.dataKey!, storage, true) await platform.importKeyEC( 'pk_ecc', new Uint8Array(auth.eccPrivateKey!), new Uint8Array(auth.eccPublicKey!), - undefined, - true + storage ) - await platform.importKeyRSA('pk_rsa', auth.privateKey!, undefined, true) + await platform.importKeyRSA('pk_rsa', auth.privateKey!, storage) while (true) { const msg = syncDownMessage({ From 3bbdce7490dfe0911ab8924ee470834ddb45d2a6 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Fri, 24 Jul 2026 17:55:25 +0530 Subject: [PATCH 2/2] fixed suggested PR changes --- keeperapi/src/browser/platform.ts | 32 ++++++++++--------------------- keeperapi/src/commands.ts | 3 +-- keeperapi/src/vault.ts | 7 ++++--- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/keeperapi/src/browser/platform.ts b/keeperapi/src/browser/platform.ts index fc1b55b1..1d6d42b2 100644 --- a/keeperapi/src/browser/platform.ts +++ b/keeperapi/src/browser/platform.ts @@ -86,7 +86,7 @@ export const browserPlatform: Platform = class { static async importKey(keyId: string, key: Uint8Array, storage?: KeyStorage, canExport?: boolean): Promise { // An AES key for one of our Keeper objects can be used for either CBC or GCM operations. // Since CryptoKeys are bound to a particular algorithm, we need to keep a copy for each. - const extractable = typeof storage?.saveKeyBytes === 'function' ? true : !!canExport + const extractable = !!canExport const cbcKey = await this.aesCbcImportKey(key, extractable) const gcmKey = await this.aesGcmImportKey(key, extractable) cryptoKeysCache['cbc'][keyId] = cbcKey @@ -97,7 +97,7 @@ export const browserPlatform: Platform = class { await storage.saveObject(this.getStorageKeyId(keyId, 'cbc'), cbcKey) await storage.saveObject(this.getStorageKeyId(keyId, 'gcm'), gcmKey) } - if (storage.saveKeyBytes) { + if (extractable && storage.saveKeyBytes) { await storage.saveKeyBytes(keyId, key) } } @@ -107,7 +107,8 @@ export const browserPlatform: Platform = class { keyId: string, privateKey: Uint8Array, publicKey: Uint8Array, - storage?: KeyStorage + storage?: KeyStorage, + canExport?: boolean ): Promise { const key = await this.importPrivateKeyEC(privateKey, publicKey) cryptoKeysCache['ecc'][keyId] = key @@ -116,7 +117,7 @@ export const browserPlatform: Platform = class { if (storage.saveObject) { await storage.saveObject(this.getStorageKeyId(keyId, 'ecc'), key) } - if (storage.saveKeyBytes) { + if (canExport && storage.saveKeyBytes) { const jwk = await crypto.subtle.exportKey('jwk', key) const keyBytes = this.stringToBytes(JSON.stringify(jwk)) await storage.saveKeyBytes(keyId, keyBytes) @@ -213,20 +214,6 @@ export const browserPlatform: Platform = class { return key } - static async ensureAesKeyLoaded(keyId: string, storage?: KeyStorage): Promise { - if (cryptoKeysCache['cbc'][keyId] && cryptoKeysCache['gcm'][keyId]) { - return true - } - if (storage?.getKeyBytes) { - const keyBytes = await storage.getKeyBytes(keyId) - if (keyBytes) { - await this.importKey(keyId, keyBytes, storage, true) - return true - } - } - return false - } - static async unwrapKeys(keys: UnwrapKeyMap, storage?: KeyStorage): Promise { if (workerPool) { try { @@ -303,7 +290,9 @@ export const browserPlatform: Platform = class { await this.unwrapRSAKey(key, keyId, unwrappingKeyId, encryptionType, storage) break case 'aes': - if (await this.ensureAesKeyLoaded(keyId, storage)) { + if (cryptoKeysCache['gcm'][keyId]) { + // Keeperapp sometimes provides redundant key data, for example, like if you own a record in a shared folder, + // or if a record belongs to multiple shared folders. So, short circuit when possible for a performance improvement return } @@ -378,8 +367,7 @@ export const browserPlatform: Platform = class { break } - const mustPersistKeyBytes = typeof storage?.saveKeyBytes === 'function' - const canExtract: boolean = mustPersistKeyBytes ? true : storage?.saveObject ? !!canExport : true + const canExtract: boolean = storage?.saveObject ? !!canExport : true const keyUsages: KeyUsage[] = ['encrypt', 'decrypt', 'unwrapKey', 'wrapKey'] const gcmKey = await crypto.subtle.unwrapKey( @@ -409,7 +397,7 @@ export const browserPlatform: Platform = class { await storage.saveObject(this.getStorageKeyId(keyId, 'cbc'), cbcKey) await storage.saveObject(this.getStorageKeyId(keyId, 'gcm'), gcmKey) } - if (storage.saveKeyBytes) { + if (canExtract && storage.saveKeyBytes) { const keyBuffer = await crypto.subtle.exportKey('raw', gcmKey) await storage.saveKeyBytes(keyId, new Uint8Array(keyBuffer)) } diff --git a/keeperapi/src/commands.ts b/keeperapi/src/commands.ts index eb43a8dc..ba50ba08 100644 --- a/keeperapi/src/commands.ts +++ b/keeperapi/src/commands.ts @@ -286,7 +286,7 @@ export type RoleManagedNodeAddResponse = KeeperResponse & { export type RoleManagedNodeAddRequest = { role_id: number managed_node_id: number - cascade_node_management?: boolean + cascade_node_management: boolean tree_keys?: RoleManagedNodeTreeKey[] } @@ -298,7 +298,6 @@ export type RoleManagedNodeUpdateRequest = { role_id: number managed_node_id: number cascade_node_management?: boolean - tree_keys?: RoleManagedNodeTreeKey[] } export const roleManagedNodeUpdateCommand = ( diff --git a/keeperapi/src/vault.ts b/keeperapi/src/vault.ts index 96723eb2..9efb78f4 100644 --- a/keeperapi/src/vault.ts +++ b/keeperapi/src/vault.ts @@ -1597,14 +1597,15 @@ export const syncDown = async (options: SyncDownOptions): Promise => const dToken = await storage.get('continuationToken') let continuationToken = dToken ? platform.base64ToBytes(dToken.token) : undefined - await platform.importKey('data', auth.dataKey!, storage, true) + await platform.importKey('data', auth.dataKey!, undefined, true) await platform.importKeyEC( 'pk_ecc', new Uint8Array(auth.eccPrivateKey!), new Uint8Array(auth.eccPublicKey!), - storage + undefined, + true ) - await platform.importKeyRSA('pk_rsa', auth.privateKey!, storage) + await platform.importKeyRSA('pk_rsa', auth.privateKey!, undefined, true) while (true) { const msg = syncDownMessage({