Custom Wrapped Keys
This guide provides an overview of how custom Lit Actions can be used to provide functionality, such as signing, with Wrapped Keys. For an overview of what a Wrapped Key is and what can be done with it, please go here.
Typically, you would want to implement a custom Lit Action to support signing with a Wrapped Key that has an underlying private key derived from an curve other than what's currently supported by the Wrapped Keys SDK.
However, you are able to provide arbitrary Lit Action code, so you can adapt Wrapped Keys to support your use case.
Provided Wrapped Keys Lit Actions
Currently the Wrapped Keys SDK includes Lit Action to support the following:
Wrapped Keys Derived from K256
Algorithm
For Wrapped Keys derived from the K256
algorithm (commonly known as ecdsa
):
- Generating
K256
(commonly known asecdsa
) private keys within a Lit Action- Uses the generateEncryptedEthereumPrivateKey Lit Action
- Signing arbitrary messages using Ethers.js' signMessage
- Uses the signMessageWithEthereumEncryptedKey Lit Action
- Signing Ethers.js transaction objects using signTransaction
- Uses the signTransactionWithEthereumEncryptedKey Lit Action
Wrapped Keys Derived ed25519
Algorithm
For Wrapped Keys derived from the ed25519
algorithm (used for Solana private key):
- Generating
ed25519
private keys within a Lit Action using the @solana/web3.js SDK- Uses the generateEncryptedSolanaPrivateKey Lit Action
- Signing arbitrary messages using the
@solana/web3.js
SDK- Uses the signMessageWithSolanaEncryptedKey Lit Action
- Signing Solana transaction objects using the
@solana/web3.js
SDK- uses the signTransactionWithSolanaEncryptedKey Lit Action
Implementing a Custom Lit Action
Implementing your own Lit Action allows you to do something different than what the provided Wrapped Key Lit Actions do. For example, supporting new functionality such as signing with a private key derived from an alternative curve to K256
or ed25519
.
You can use the Provided Wrapped Keys Lit Actions as guides on how to implement your custom Lit Action. Below we will be covering common functionality for Wrapped Key Lit Actions.
accessControlConditions
Access Control Conditions are provided to the Lit SDK when the underlying private key for the Wrapped Key is encrypted. If you imported or generated a Wrapped Key, then the default Access Control Conditions used to encrypt the private key are:
[
{
contractAddress: '',
standardContractType: '',
chain: CHAIN_ETHEREUM,
method: '',
parameters: [':userAddress'],
returnValueTest: {
comparator: '=',
value: pkpAddress,
},
},
];
If you manually encrypt your private key with custom Access Control Conditions, then you'll need to supply them yourself when calling your custom Lit Action.
Using A Third-party SDK Within Your Lit Action
The Ethers.js v5 SDK is already made available within a Lit Action. For any other SDK, you'll need to bundle the code with your Lit Action. Lit Actions execute within a Deno environment, so any APIs that aren't supported by Deno will need to be accounted for via polyfills and/or shims.
As a reference implementation, the Wrapped Keys SDK uses esbuild to bundle the @solana/web3.js
SDK with the Lit Action code, and provide the required shim. This is the esbuild.config.js
used and this is the shim used to include buffer within the bundled Lit Action code.
Then, as you can see in the Solana Wrapped Keys Lit Actions, various @solana/web3.js
exports are import
ed into the Lit Action code as usual.
Generating and Encrypting a Private Key
If you want to implementing generation and encryption of a private key using an alternative private key algorithm, then you should make use of the following Lit Actions SDK methods:
runOnce allows you to specify code that should only be ran by a single Lit node instead of having all Lit nodes run the same code. This is useful for executing the code that generates and encrypts the private key.
In addition to generating the private key within runOnce
, you'll use the encrypt method to encrypt the private key.
Running both the key generation and encryption code within runOnce
results in only a single Lit node having access to the clear text private key, as only what you return from runOnce
will be shared with the other Lit nodes.
To work with the Wrapped Keys SDK, your Lit Action should return the ciphertext
, dataToEncryptHash
, and the corresponding public key to the generated private key as a JSON.stringify
ed object. This can be done using the setResponse method like so:
(async () => {
const LIT_PREFIX = 'lit_';
const result = await Lit.Actions.runOnce(
{ waitForResponse: true, name: 'encryptedPrivateKey' },
async () => {
// Your private key generation logic...
const generatedPrivateKey = "your_private_key";
const utf8Encode = new TextEncoder();
const encodedPrivateKey = utf8Encode.encode(
`${LIT_PREFIX}${generatedPrivateKey}` // For enhanced security, you should prepend all generated private keys with "lit_"
);
const { ciphertext, dataToEncryptHash } = await Lit.Actions.encrypt({
accessControlConditions, // This should be passed into the Lit Action
to_encrypt: encodedPrivateKey,
});
return JSON.stringify({
ciphertext,
dataToEncryptHash,
// The following is pseudo code, but you need to return
// the public key for the generated private key as a string.
publicKey: generatedPrivateKey.publicKey.toString(),
});
}
);
// Any other code you'd like to run...
Lit.Actions.setResponse({
response: result,
});
})
Storing the Encryption Metadata
After generating and encrypting the private key, the resulting encryption metadata (ciphertext
, dataToEncryptHash
, and the publicKey
) should be stored in Lit's private instance of DynamoDB using the storeEncryptedKeyMetadata method included in the Wrapped Keys SDK.
Decrypting a Wrapped Key
Decrypting a Wrapped Key, typically to be used for signing, can be done using the Lit Action SDK's decryptToSingleNode method.
In order to call decryptToSingleNode
, you will need to provide the following arguments:
accessControlConditions
This value is the Access Control Conditions provided to the Lit SDK when the underlying private key for the Wrapped Key was encrypted. If you imported or generated a Wrapped Key, then the default Access Control Conditions used to encrypt the private key are:
[
{
contractAddress: '',
standardContractType: '',
chain: CHAIN_ETHEREUM,
method: '',
parameters: [':userAddress'],
returnValueTest: {
comparator: '=',
value: pkpAddress,
},
},
];
If you manually encrypted your private key with custom Access Control Conditions, then you'll need to supply them yourself when calling your custom Lit Action.
ciphertext
This is the encrypted form of the underlying private key for the Wrapped Key (encrypted using the Lit network's BLS public key). This value is returned by the encryption functions from the Lit SDK e.g. encryptString.
dataToEncryptHash
This is the SHA-256
hash of the underlying private key for the Wrapped Key that was encrypted using the Lit network's BLS public key. This value is returned by the encryption functions from the Lit SDK e.g. encryptString.
authSig
This value should be hardcoded as null
.
chain
This value should be hardcoded as ethereum
.
Fetching a Wrapped Key's Metadata
If you stored the encryption metadata in Lit's private instance of DynamoDB using the storeEncryptedKeyMetadata method, then you can retrieve the metadata using the getEncryptedKeyMetadata method included in the Wrapped Keys SDK.
Example Implementation
Once you have the required arguments, you can call decryptToSingleNode
like so:
(async () => {
const LIT_PREFIX = 'lit_';
let decryptedPrivateKey;
try {
decryptedPrivateKey = await Lit.Actions.decryptToSingleNode({
accessControlConditions,
chain: 'ethereum',
ciphertext,
dataToEncryptHash,
authSig: null,
});
} catch (error) {
Lit.Actions.setResponse({
response: `Error: When decrypting data to private key: ${error.message}`,
});
return;
}
if (!decryptedPrivateKey) {
// Exit the nodes which don't have the decryptedData
return;
}
// Here we're checking if LIT_PREFIX was prepended to the private key,
// and removing it if it exists before using the key.
const privateKey = decryptedPrivateKey.startsWith(LIT_PREFIX)
? decryptedPrivateKey.slice(LIT_PREFIX.length)
: decryptedPrivateKey;
// The rest of your Lit Action code...
})
After decrypting the private key, you'll have access to it's clear text form to be used for signing message, transaction, or for any other use case.
Executing Your Custom Lit Action
After implementing your custom Lit Action, you'll want to make use of Lit SDK's executeJs method to execute the Lit Action with any required arguments.
As a reference implementation, you can take a look at the following methods used by the Wrapped Keys SDK to call the Provided Wrapped Keys Lit Actions:
To call executeJs
, you'll need to pass the following arguments:
sessionSigs
If your Wrapped Key was encrypted using the default Access Control Conditions, this would be the case if your private key was imported or generated, then these Session Signatures will need to be generated by the same PKP used when import or generating the Wrapped Keys. This is because the Wrapped Key was encrypted with Access Control Conditions to only authorize decryption if the provided sessionSigs
were generated by the PKP that created the Wrapped Key.
If you implemented your own private key encryption logic, then sessionSigs
may not be relevant for decryption, but are still required to authenticate and authorize yourself with the Lit network to allow for usage of the network.
ipfsId
The Wrapped Keys SDK methods make use of this property so that the Lit Action code doesn't have to be bundled into the @lit-protocol/wrapped-keys
package. Additionally, because the Solana Wrapped Key Lit Actions code includes the @solana/web3.js
SDK, the resulting code is too large to pass as a string when calling executeJs
.
If you're bundling a third-party SDK with your Lit Action code, or just prefer this method, you should upload your code to IPFS and provide the generated Content Identifier (CID) as the value for this property.
IPFS Pinata is an easy to use service to upload and pin files to IPFS.
code
Alternatively, instead of using ipfsId
, if your Lit Action code is simple enough, you can directly provide your Lit Action code as a string
using this property.
jsParams
This is an object containing any parameters required by your Lit Action. The key for each property in this object will be the name of the variable within your Lit Action code.
Summary
This overview covers the basis of implementing custom Lit Actions to be used with Wrapped Keys. If you have any questions, or need help implementing custom Lit Actions for your Wrapped Keys use case, feel free to reach out to us on our Discord or Telegram for support.