From 979bad5dde3716b0266f37759da83adb3483a9b3 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 20 Jul 2026 10:48:19 +0530 Subject: [PATCH 1/3] Added support for Enterprise role commands --- keeperapi/src/browser/platform.ts | 50 +++++++++--- keeperapi/src/commands.ts | 121 ++++++++++++++++++++++++++++++ keeperapi/src/vault.ts | 7 +- 3 files changed, 162 insertions(+), 16 deletions(-) diff --git a/keeperapi/src/browser/platform.ts b/keeperapi/src/browser/platform.ts index c3a00dfa..92f1e75b 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) @@ -209,9 +210,34 @@ export const browserPlatform: Platform = class { const key = await this.loadCryptoKey(keyId, keyType, storage) cryptoKeysCache[keyType][keyId] = key + // AES folder/record keys are imported as a pair; hydrate the sibling cache entry too. + if (keyType === 'cbc' || keyType === 'gcm') { + const sibling: EncryptionType = keyType === 'cbc' ? 'gcm' : 'cbc' + if (!cryptoKeysCache[sibling][keyId]) { + try { + cryptoKeysCache[sibling][keyId] = await this.loadCryptoKey(keyId, sibling, storage) + } catch { + // sibling may be unavailable for legacy key material + } + } + } 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 +314,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 +322,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 +389,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 +419,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..b2eee47e 100644 --- a/keeperapi/src/commands.ts +++ b/keeperapi/src/commands.ts @@ -232,6 +232,117 @@ 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') + +/** Role AES key encrypted for an admin user (TRANSFER_ACCOUNT backward compat). */ +export type ManagedNodeRoleKey = { + enterprise_user_id: number + role_key: string + tree_key_type?: string | number +} + +/** MSP tree key for MANAGE_COMPANIES privilege. */ +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 +490,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/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 80741428e5f03caa8f4bcf61eff7ca51bacce408 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 20 Jul 2026 17:44:36 +0530 Subject: [PATCH 2/3] added support for enterprise node commands --- keeperapi/src/browser/platform.ts | 11 ----------- keeperapi/src/commands.ts | 19 +++++++++++++++++++ keeperapi/src/restMessages.ts | 5 +++++ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/keeperapi/src/browser/platform.ts b/keeperapi/src/browser/platform.ts index 92f1e75b..fc1b55b1 100644 --- a/keeperapi/src/browser/platform.ts +++ b/keeperapi/src/browser/platform.ts @@ -210,17 +210,6 @@ export const browserPlatform: Platform = class { const key = await this.loadCryptoKey(keyId, keyType, storage) cryptoKeysCache[keyType][keyId] = key - // AES folder/record keys are imported as a pair; hydrate the sibling cache entry too. - if (keyType === 'cbc' || keyType === 'gcm') { - const sibling: EncryptionType = keyType === 'cbc' ? 'gcm' : 'cbc' - if (!cryptoKeysCache[sibling][keyId]) { - try { - cryptoKeysCache[sibling][keyId] = await this.loadCryptoKey(keyId, sibling, storage) - } catch { - // sibling may be unavailable for legacy key material - } - } - } return key } diff --git a/keeperapi/src/commands.ts b/keeperapi/src/commands.ts index b2eee47e..54eeb596 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 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 => From f472188a7eb07220772354f2e9601b36fb983fd7 Mon Sep 17 00:00:00 2001 From: sgaddala-ks Date: Mon, 20 Jul 2026 17:46:49 +0530 Subject: [PATCH 3/3] role mc changes --- keeperapi/src/commands.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/keeperapi/src/commands.ts b/keeperapi/src/commands.ts index 54eeb596..eb43a8dc 100644 --- a/keeperapi/src/commands.ts +++ b/keeperapi/src/commands.ts @@ -314,14 +314,12 @@ export const roleManagedNodeRemoveCommand = ( request: RoleManagedNodeRemoveRequest ): RestCommand => createCommand(request, 'role_managed_node_remove') -/** Role AES key encrypted for an admin user (TRANSFER_ACCOUNT backward compat). */ export type ManagedNodeRoleKey = { enterprise_user_id: number role_key: string tree_key_type?: string | number } -/** MSP tree key for MANAGE_COMPANIES privilege. */ export type ManagedNodeMspKey = { mc_enterprise_id: number tree_key: string