diff --git a/.changeset/young-pianos-tickle.md b/.changeset/young-pianos-tickle.md
new file mode 100644
index 000000000..7de212ed5
--- /dev/null
+++ b/.changeset/young-pianos-tickle.md
@@ -0,0 +1,6 @@
+---
+'@lit-protocol/wrapped-keys': minor
+'@lit-protocol/e2e': minor
+---
+
+Wrapped-keys now supports updating ciphertext/ACCs via a new PUT endpoint, returns version history when requested.
diff --git a/docs/docs.json b/docs/docs.json
index fa5be3419..7890c39d0 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -133,6 +133,7 @@
"sdk/sdk-reference/wrapped-keys/functions/exportPrivateKey",
"sdk/sdk-reference/wrapped-keys/functions/importPrivateKey",
"sdk/sdk-reference/wrapped-keys/functions/getEncryptedKey",
+ "sdk/sdk-reference/wrapped-keys/functions/updateEncryptedKey",
"sdk/sdk-reference/wrapped-keys/functions/listEncryptedKeyMetadata",
"sdk/sdk-reference/wrapped-keys/functions/storeEncryptedKey",
"sdk/sdk-reference/wrapped-keys/functions/storeEncryptedKeyBatch",
@@ -246,4 +247,4 @@
"discord": "https://litgateway.com/discord"
}
}
-}
\ No newline at end of file
+}
diff --git a/docs/sdk/sdk-reference/wrapped-keys/functions/getEncryptedKey.mdx b/docs/sdk/sdk-reference/wrapped-keys/functions/getEncryptedKey.mdx
index e923152d7..fd5bc0e8e 100644
--- a/docs/sdk/sdk-reference/wrapped-keys/functions/getEncryptedKey.mdx
+++ b/docs/sdk/sdk-reference/wrapped-keys/functions/getEncryptedKey.mdx
@@ -22,6 +22,10 @@ Fetches the encrypted ciphertext and metadata for a stored wrapped key without d
Identifier of the wrapped key to retrieve.
+
+ Optional flag to return the `versions` array of prior states.
+
+
Optional price ceiling for the Lit Action.
@@ -29,7 +33,7 @@ Fetches the encrypted ciphertext and metadata for a stored wrapped key without d
## Returns
- Includes ciphertext, `dataToEncryptHash`, memo, key type, public key, and PKP address.
+ Includes ciphertext, `dataToEncryptHash`, memo, key type, public key, PKP address, and (when requested) `versions` + `updatedAt`.
## Example
@@ -39,5 +43,6 @@ const storedKey = await wrappedKeysApi.getEncryptedKey({
pkpSessionSigs,
litClient,
id,
+ includeVersions: true, // optional
});
```
diff --git a/docs/sdk/sdk-reference/wrapped-keys/functions/updateEncryptedKey.mdx b/docs/sdk/sdk-reference/wrapped-keys/functions/updateEncryptedKey.mdx
new file mode 100644
index 000000000..90ef948b9
--- /dev/null
+++ b/docs/sdk/sdk-reference/wrapped-keys/functions/updateEncryptedKey.mdx
@@ -0,0 +1,65 @@
+---
+title: updateEncryptedKey
+---
+
+# Function
+
+> **updateEncryptedKey**(`params`)
+
+Re-encrypts an existing wrapped key and appends the previous state to the `versions` array.
+
+## Parameters
+
+
+ Session signatures identifying the PKP that owns the wrapped key.
+
+
+
+ Lit client instance used to talk to the wrapped-keys service.
+
+
+
+ Identifier of the wrapped key to update.
+
+
+
+ New base64-encrypted ciphertext to store.
+
+
+
+ Optional updated memo for the wrapped key.
+
+
+
+ Optional updated ACCs for EVM contract conditions.
+
+
+
+ Optional price ceiling for the Lit Action.
+
+
+## Returns
+
+
+ Returns `id`, `pkpAddress`, and `updatedAt` of the updated wrapped key.
+
+
+## Example
+
+```ts
+const res = await wrappedKeysApi.updateEncryptedKey({
+ pkpSessionSigs,
+ litClient,
+ id,
+ ciphertext: 'base64-new',
+ memo: 'rotated memo',
+});
+
+const withHistory = await wrappedKeysApi.getEncryptedKey({
+ pkpSessionSigs,
+ litClient,
+ id,
+ includeVersions: true,
+});
+console.log(withHistory.versions); // [{ ciphertext: 'old', ... }]
+```
diff --git a/packages/e2e/src/test-helpers/executeJs/wrappedKeys.ts b/packages/e2e/src/test-helpers/executeJs/wrappedKeys.ts
index a61c52e5e..eabfe70fd 100644
--- a/packages/e2e/src/test-helpers/executeJs/wrappedKeys.ts
+++ b/packages/e2e/src/test-helpers/executeJs/wrappedKeys.ts
@@ -57,6 +57,21 @@ namespace TestHelper {
export const randomHash = (input: string) =>
createHash('sha256').update(input).digest('hex');
+ export const createEvmContractConditions = () =>
+ JSON.stringify([
+ {
+ contractAddress: ZERO_ADDRESS,
+ standardContractType: 'ERC20',
+ chain: EVM_CHAIN,
+ method: 'balanceOf',
+ parameters: [':userAddress'],
+ returnValueTest: {
+ comparator: '>=',
+ value: '0',
+ },
+ },
+ ]);
+
export const createStorePayload = (memo = randomMemo('store')) => {
const ciphertext = randomCiphertext();
return {
@@ -341,6 +356,109 @@ export const registerWrappedKeysTests = () => {
expect(storedKey.dataToEncryptHash).toBeTruthy();
});
+ test('updateEncryptedKey rotates ciphertext and returns version history', async () => {
+ const pkpSessionSigs = await TestHelper.createPkpSessionSigs({
+ testEnv,
+ alice,
+ delegationAuthSig: aliceDelegationAuthSig,
+ });
+
+ const initialPayload = TestHelper.createStorePayload(
+ TestHelper.randomMemo('update-before')
+ );
+
+ const { id } = await wrappedKeysApi.storeEncryptedKey({
+ pkpSessionSigs,
+ litClient: testEnv.litClient,
+ ...initialPayload,
+ });
+
+ const newCiphertext = TestHelper.randomCiphertext();
+ const newMemo = TestHelper.randomMemo('update-after');
+
+ const updateResult = await wrappedKeysApi.updateEncryptedKey({
+ pkpSessionSigs,
+ litClient: testEnv.litClient,
+ id,
+ ciphertext: newCiphertext,
+ memo: newMemo,
+ });
+
+ expect(updateResult.id).toBe(id);
+ expect(updateResult.pkpAddress).toBe(alice.pkp!.ethAddress);
+ expect(updateResult.updatedAt).toBeTruthy();
+
+ const fetched = await wrappedKeysApi.getEncryptedKey({
+ pkpSessionSigs,
+ litClient: testEnv.litClient,
+ id,
+ includeVersions: true,
+ });
+
+ expect(fetched.ciphertext).toBe(newCiphertext);
+ expect(fetched.memo).toBe(newMemo);
+ expect(fetched.updatedAt).toBeTruthy();
+ expect(fetched.versions).toBeDefined();
+ expect(fetched.versions?.length).toBe(1);
+ expect(fetched.versions?.[0].ciphertext).toBe(
+ initialPayload.ciphertext
+ );
+ expect(fetched.versions?.[0].memo).toBe(initialPayload.memo);
+ });
+
+ test('updateEncryptedKey with evmContractConditions stores conditions in version history', async () => {
+ const pkpSessionSigs = await TestHelper.createPkpSessionSigs({
+ testEnv,
+ alice,
+ delegationAuthSig: aliceDelegationAuthSig,
+ });
+
+ const initialPayload = TestHelper.createStorePayload(
+ TestHelper.randomMemo('update-evm-before')
+ );
+
+ const { id } = await wrappedKeysApi.storeEncryptedKey({
+ pkpSessionSigs,
+ litClient: testEnv.litClient,
+ ...initialPayload,
+ });
+
+ const newCiphertext = TestHelper.randomCiphertext();
+ const newMemo = TestHelper.randomMemo('update-evm-after');
+ const evmContractConditions = TestHelper.createEvmContractConditions();
+
+ const updateResult = await wrappedKeysApi.updateEncryptedKey({
+ pkpSessionSigs,
+ litClient: testEnv.litClient,
+ id,
+ ciphertext: newCiphertext,
+ memo: newMemo,
+ evmContractConditions,
+ });
+
+ expect(updateResult.id).toBe(id);
+ expect(updateResult.pkpAddress).toBe(alice.pkp!.ethAddress);
+ expect(updateResult.updatedAt).toBeTruthy();
+
+ const fetched = await wrappedKeysApi.getEncryptedKey({
+ pkpSessionSigs,
+ litClient: testEnv.litClient,
+ id,
+ includeVersions: true,
+ });
+
+ expect(fetched.ciphertext).toBe(newCiphertext);
+ expect(fetched.memo).toBe(newMemo);
+ expect(fetched.updatedAt).toBeTruthy();
+ expect(fetched.versions).toBeDefined();
+ expect(fetched.versions?.length).toBe(1);
+ expect(fetched.versions?.[0].ciphertext).toBe(
+ initialPayload.ciphertext
+ );
+ expect(fetched.versions?.[0].memo).toBe(initialPayload.memo);
+ expect(fetched.versions?.[0].evmContractConditions).toBeUndefined();
+ });
+
test('importPrivateKey persists an externally generated key', async () => {
const pkpSessionSigs = await TestHelper.createPkpSessionSigs({
testEnv,
diff --git a/packages/wrapped-keys/src/index.ts b/packages/wrapped-keys/src/index.ts
index f94419f6f..213a10f95 100644
--- a/packages/wrapped-keys/src/index.ts
+++ b/packages/wrapped-keys/src/index.ts
@@ -9,6 +9,7 @@ import {
signTransactionWithEncryptedKey,
storeEncryptedKey,
storeEncryptedKeyBatch,
+ updateEncryptedKey,
} from './lib/api';
import {
CHAIN_ETHEREUM,
@@ -43,6 +44,7 @@ export const api = {
storeEncryptedKey,
storeEncryptedKeyBatch,
batchGeneratePrivateKeys,
+ updateEncryptedKey,
};
export const config = {
@@ -76,6 +78,9 @@ export type {
StoreEncryptedKeyResult,
StoredKeyData,
StoredKeyMetadata,
+ UpdateEncryptedKeyParams,
+ UpdateEncryptedKeyResult,
+ WrappedKeyVersion,
} from './lib/types';
export type {
diff --git a/packages/wrapped-keys/src/lib/api/get-encrypted-key.ts b/packages/wrapped-keys/src/lib/api/get-encrypted-key.ts
index a8d21fc65..a87fa8873 100644
--- a/packages/wrapped-keys/src/lib/api/get-encrypted-key.ts
+++ b/packages/wrapped-keys/src/lib/api/get-encrypted-key.ts
@@ -15,7 +15,7 @@ import { GetEncryptedKeyDataParams, StoredKeyData } from '../types';
export async function getEncryptedKey(
params: GetEncryptedKeyDataParams
): Promise {
- const { pkpSessionSigs, litClient, id } = params;
+ const { pkpSessionSigs, litClient, id, includeVersions } = params;
const sessionSig = getFirstSessionSig(pkpSessionSigs);
const pkpAddress = getPkpAddressFromSessionSig(sessionSig);
@@ -26,5 +26,6 @@ export async function getEncryptedKey(
id,
sessionSig,
litNetwork,
+ includeVersions,
});
}
diff --git a/packages/wrapped-keys/src/lib/api/index.ts b/packages/wrapped-keys/src/lib/api/index.ts
index 2eebfbfc5..7c2d314d6 100644
--- a/packages/wrapped-keys/src/lib/api/index.ts
+++ b/packages/wrapped-keys/src/lib/api/index.ts
@@ -8,6 +8,7 @@ import { signMessageWithEncryptedKey } from './sign-message-with-encrypted-key';
import { signTransactionWithEncryptedKey } from './sign-transaction-with-encrypted-key';
import { storeEncryptedKey } from './store-encrypted-key';
import { storeEncryptedKeyBatch } from './store-encrypted-key-batch';
+import { updateEncryptedKey } from './update-encrypted-key';
export {
listEncryptedKeyMetadata,
@@ -20,4 +21,5 @@ export {
storeEncryptedKeyBatch,
getEncryptedKey,
batchGeneratePrivateKeys,
+ updateEncryptedKey,
};
diff --git a/packages/wrapped-keys/src/lib/api/update-encrypted-key.ts b/packages/wrapped-keys/src/lib/api/update-encrypted-key.ts
new file mode 100644
index 000000000..217c498a8
--- /dev/null
+++ b/packages/wrapped-keys/src/lib/api/update-encrypted-key.ts
@@ -0,0 +1,40 @@
+import { updatePrivateKey } from '../service-client';
+import { UpdateEncryptedKeyParams, UpdateEncryptedKeyResult } from '../types';
+import {
+ getFirstSessionSig,
+ getLitNetworkFromClient,
+ getPkpAddressFromSessionSig,
+} from './utils';
+
+/**
+ * Updates an existing wrapped key and appends the previous state to versions.
+ *
+ * @param { UpdateEncryptedKeyParams } params Parameters required to update the encrypted private key
+ * @returns { Promise } An object containing the id, pkpAddress, and updatedAt timestamp of the updated key
+ */
+export async function updateEncryptedKey(
+ params: UpdateEncryptedKeyParams
+): Promise {
+ const {
+ pkpSessionSigs,
+ litClient,
+ id,
+ ciphertext,
+ evmContractConditions,
+ memo,
+ } = params;
+
+ const sessionSig = getFirstSessionSig(pkpSessionSigs);
+ const pkpAddress = getPkpAddressFromSessionSig(sessionSig);
+ const litNetwork = getLitNetworkFromClient(litClient);
+
+ return updatePrivateKey({
+ pkpAddress,
+ id,
+ sessionSig,
+ ciphertext,
+ evmContractConditions,
+ memo,
+ litNetwork,
+ });
+}
diff --git a/packages/wrapped-keys/src/lib/service-client/client.ts b/packages/wrapped-keys/src/lib/service-client/client.ts
index 319a1ce9b..966a65bc4 100644
--- a/packages/wrapped-keys/src/lib/service-client/client.ts
+++ b/packages/wrapped-keys/src/lib/service-client/client.ts
@@ -3,6 +3,7 @@ import {
ListKeysParams,
StoreKeyBatchParams,
StoreKeyParams,
+ UpdateKeyParams,
} from './types';
import { generateRequestId, getBaseRequestParams, makeRequest } from './utils';
import {
@@ -10,6 +11,7 @@ import {
StoredKeyMetadata,
StoreEncryptedKeyBatchResult,
StoreEncryptedKeyResult,
+ UpdateEncryptedKeyResult,
} from '../types';
/** Fetches previously stored private key metadata from the wrapped keys service.
@@ -48,7 +50,7 @@ export async function listPrivateKeyMetadata(
export async function fetchPrivateKey(
params: FetchKeyParams
): Promise {
- const { litNetwork, sessionSig, id, pkpAddress } = params;
+ const { litNetwork, sessionSig, id, pkpAddress, includeVersions } = params;
const requestId = generateRequestId();
const { baseUrl, initParams } = getBaseRequestParams({
@@ -58,8 +60,9 @@ export async function fetchPrivateKey(
requestId,
});
+ const query = includeVersions ? '?includeVersions=true' : '';
return makeRequest({
- url: `${baseUrl}/${pkpAddress}/${id}`,
+ url: `${baseUrl}/${pkpAddress}/${id}${query}`,
init: initParams,
requestId,
});
@@ -124,3 +127,45 @@ export async function storePrivateKeyBatch(
return { pkpAddress, ids };
}
+
+/** Updates an existing wrapped key and appends prior state to versions.
+ *
+ * @param { UpdateKeyParams } params Parameters required to update the private key metadata
+ * @returns { Promise } id/pkpAddress/updatedAt on successful update
+ */
+export async function updatePrivateKey(
+ params: UpdateKeyParams
+): Promise {
+ const {
+ litNetwork,
+ sessionSig,
+ pkpAddress,
+ id,
+ ciphertext,
+ evmContractConditions,
+ memo,
+ } = params;
+
+ const requestId = generateRequestId();
+ const { baseUrl, initParams } = getBaseRequestParams({
+ litNetwork,
+ sessionSig,
+ method: 'PUT',
+ requestId,
+ });
+
+ return makeRequest({
+ url: `${baseUrl}/${pkpAddress}/${id}`,
+ init: {
+ ...initParams,
+ body: JSON.stringify({
+ ciphertext,
+ ...(evmContractConditions !== undefined
+ ? { evmContractConditions }
+ : {}),
+ ...(memo !== undefined ? { memo } : {}),
+ }),
+ },
+ requestId,
+ });
+}
diff --git a/packages/wrapped-keys/src/lib/service-client/index.ts b/packages/wrapped-keys/src/lib/service-client/index.ts
index 68842e14a..a64c6b0a8 100644
--- a/packages/wrapped-keys/src/lib/service-client/index.ts
+++ b/packages/wrapped-keys/src/lib/service-client/index.ts
@@ -3,6 +3,7 @@ import {
storePrivateKey,
storePrivateKeyBatch,
listPrivateKeyMetadata,
+ updatePrivateKey,
} from './client';
export {
@@ -10,4 +11,5 @@ export {
storePrivateKey,
storePrivateKeyBatch,
listPrivateKeyMetadata,
+ updatePrivateKey,
};
diff --git a/packages/wrapped-keys/src/lib/service-client/types.ts b/packages/wrapped-keys/src/lib/service-client/types.ts
index 1e7291ef4..3efbb198d 100644
--- a/packages/wrapped-keys/src/lib/service-client/types.ts
+++ b/packages/wrapped-keys/src/lib/service-client/types.ts
@@ -11,6 +11,7 @@ interface BaseApiParams {
export type FetchKeyParams = BaseApiParams & {
pkpAddress: string;
id: string;
+ includeVersions?: boolean;
};
export type ListKeysParams = BaseApiParams & { pkpAddress: string };
@@ -34,9 +35,17 @@ export interface StoreKeyBatchParams extends BaseApiParams {
>[];
}
+export interface UpdateKeyParams extends BaseApiParams {
+ pkpAddress: string;
+ id: string;
+ ciphertext: string;
+ evmContractConditions?: string;
+ memo?: string;
+}
+
export interface BaseRequestParams {
sessionSig: AuthSig;
- method: 'GET' | 'POST';
+ method: 'GET' | 'POST' | 'PUT';
litNetwork: LIT_NETWORKS_KEYS;
requestId: string;
}
diff --git a/packages/wrapped-keys/src/lib/types.ts b/packages/wrapped-keys/src/lib/types.ts
index 2351975c1..bfe1a12bf 100644
--- a/packages/wrapped-keys/src/lib/types.ts
+++ b/packages/wrapped-keys/src/lib/types.ts
@@ -44,9 +44,11 @@ export type ListEncryptedKeyMetadataParams = BaseApiParams;
* @extends BaseApiParams
*
* @property { string } id The unique identifier (UUID V4) of the encrypted private key
+ * @property { boolean } [includeVersions] Optional flag to include version history in the response
*/
export type GetEncryptedKeyDataParams = BaseApiParams & {
id: string;
+ includeVersions?: boolean;
};
/** Metadata for a key that has been stored, encrypted, on the wrapped keys backend service
@@ -60,6 +62,8 @@ export type GetEncryptedKeyDataParams = BaseApiParams & {
* @property { string } memo A (typically) user-provided descriptor for the encrypted private key
* @property { string } id The unique identifier (UUID V4) of the encrypted private key
* @property { LIT_NETWORKS_KEYS } litNetwork The LIT network that the client who stored the key was connected to
+ * @property { string } [updatedAt] ISO 8601 timestamp of when the key was last updated
+ * @property { WrappedKeyVersion[] } [versions] Array of historical versions of this key after update operations
*/
export interface StoredKeyMetadata {
publicKey: string;
@@ -68,6 +72,8 @@ export interface StoredKeyMetadata {
litNetwork: LIT_NETWORKS_KEYS;
memo: string;
id: string;
+ updatedAt?: string;
+ versions?: WrappedKeyVersion[];
}
/** Complete encrypted private key data, including the `ciphertext` and `dataToEncryptHash` necessary to decrypt the key
@@ -81,6 +87,34 @@ export interface StoredKeyData extends StoredKeyMetadata {
dataToEncryptHash: string;
}
+/** Represents a historical version of a wrapped key after an update operation
+ *
+ * @typedef WrappedKeyVersion
+ * @property { string } id The unique identifier (UUID V4) of this key version
+ * @property { string } ciphertext The base64 encoded, salted & encrypted private key at this version
+ * @property { string } dataToEncryptHash SHA-256 of the ciphertext for this version
+ * @property { string } keyType The type of key that was encrypted -- e.g. ed25519, K256, etc.
+ * @property { LIT_NETWORKS_KEYS } litNetwork The LIT network that the client was connected to
+ * @property { string } memo The descriptor for the encrypted private key at this version
+ * @property { string } publicKey The public key of the encrypted private key
+ * @property { string } [evmContractConditions] Optional EVM contract conditions for access control at this version
+ * @property { string } updatedAt ISO 8601 timestamp of when this version was created
+ */
+export interface WrappedKeyVersion
+ extends Pick<
+ StoredKeyData,
+ | 'ciphertext'
+ | 'dataToEncryptHash'
+ | 'keyType'
+ | 'litNetwork'
+ | 'memo'
+ | 'publicKey'
+ > {
+ id: string;
+ evmContractConditions?: string;
+ updatedAt: string;
+}
+
/** Properties required to persist an encrypted key into the wrapped-keys backend storage service
*
* @typedef StoreEncryptedKeyParams
@@ -130,6 +164,36 @@ export interface StoreEncryptedKeyBatchResult {
pkpAddress: string;
}
+/** Properties required to update an existing encrypted key in the wrapped-keys backend storage service
+ *
+ * @typedef UpdateEncryptedKeyParams
+ * @extends BaseApiParams
+ *
+ * @property { string } id The unique identifier (UUID V4) of the encrypted private key to update
+ * @property { string } ciphertext The new base64 encoded, salted & encrypted private key
+ * @property { string } [evmContractConditions] Optional EVM contract conditions for access control
+ * @property { string } [memo] Optional descriptor for the encrypted private key
+ */
+export type UpdateEncryptedKeyParams = BaseApiParams & {
+ id: string;
+ ciphertext: string;
+ evmContractConditions?: string;
+ memo?: string;
+};
+
+/** Result of updating a private key in the wrapped keys backend service
+ *
+ * @typedef UpdateEncryptedKeyResult
+ * @property { string } id The unique identifier (UUID V4) of the encrypted private key that was updated
+ * @property { string } pkpAddress The LIT PKP Address that the key was linked to; this is derived from the provided pkpSessionSigs
+ * @property { string } updatedAt ISO 8601 timestamp of when the key was updated
+ */
+export interface UpdateEncryptedKeyResult {
+ id: string;
+ pkpAddress: string;
+ updatedAt: string;
+}
+
/** Exporting a previously persisted key only requires valid pkpSessionSigs and a LIT Node Client instance configured for the appropriate network.
*
* @typedef ExportPrivateKeyParams