@veramo/kms-web3
Version:
Veramo KMS implementation backed by web3 wallets
137 lines (123 loc) • 4.68 kB
text/typescript
import { JsonRpcSigner, BrowserProvider, toUtf8String } from 'ethers'
import { TKeyType, IKey, ManagedKeyInfo, MinimalImportableKey } from '@veramo/core-types'
import { AbstractKeyManagementSystem, Eip712Payload } from '@veramo/key-manager'
/**
* This is a {@link @veramo/key-manager#AbstractKeyManagementSystem | KMS} implementation that uses the addresses of a
* web3 wallet as key identifiers, and calls the respective wallet for signing operations.
* @beta
*/
export class Web3KeyManagementSystem extends AbstractKeyManagementSystem {
/**
*
* @param providers - the key can be any unique name.
* Example `{ metamask: metamaskProvider, walletConnect: walletConnectProvider }`
*/
constructor(private providers: Record<string, BrowserProvider>) {
super()
}
createKey({ type }: { type: TKeyType }): Promise<ManagedKeyInfo> {
throw Error('not_supported: Web3KeyManagementSystem cannot create new keys')
}
async importKey(args: Omit<MinimalImportableKey, 'kms'>): Promise<ManagedKeyInfo> {
// throw Error('Not implemented')
return args as any as ManagedKeyInfo
}
async listKeys(): Promise<ManagedKeyInfo[]> {
const keys: ManagedKeyInfo[] = []
for (const provider in this.providers) {
const accounts = await this.providers[provider].listAccounts()
for (const account of accounts) {
const key: ManagedKeyInfo = {
kid: `${provider}-${account}`,
type: 'Secp256k1',
publicKeyHex: '',
kms: '',
meta: {
account,
provider,
algorithms: ['eth_signMessage', 'eth_signTypedData'],
},
}
keys.push(key)
}
}
return keys
}
async sharedSecret(args: {
myKeyRef: Pick<IKey, 'kid'>
theirKey: Pick<IKey, 'type' | 'publicKeyHex'>
}): Promise<string> {
throw Error('not_implemented: Web3KeyManagementSystem sharedSecret')
}
async deleteKey(args: { kid: string }) {
// this kms doesn't need to delete keys
return true
}
// keyRef should be in this format '{providerName-account}
// example: 'metamask-0xf3beac30c498d9e26865f34fcaa57dbb935b0d74'
private async getAccountAndSignerByKeyRef(keyRef: Pick<IKey, 'kid'>): Promise<{ account: string; signer: JsonRpcSigner }> {
const [providerName, account] = keyRef.kid.split('-')
if (!this.providers[providerName]) {
throw Error(`not_available: provider ${providerName}`)
}
const signer = await this.providers[providerName].getSigner(account)
return { account, signer }
}
async sign({
keyRef,
algorithm,
data,
}: {
keyRef: Pick<IKey, 'kid'>
algorithm?: string
data: Uint8Array
}): Promise<string> {
if (algorithm) {
if (algorithm === 'eth_signMessage') {
return await this.eth_signMessage(keyRef, data)
} else if (['eth_signTypedData', 'EthereumEip712Signature2021'].includes(algorithm)) {
return await this.eth_signTypedData(keyRef, data)
}
}
throw Error(`not_supported: Cannot sign ${algorithm} `)
}
/**
* @returns a `0x` prefixed hex string representing the signed EIP712 data
*/
private async eth_signTypedData(keyRef: Pick<IKey, 'kid'>, data: Uint8Array) {
let msg, msgDomain, msgTypes, msgPrimaryType
const serializedData = toUtf8String(data)
try {
const jsonData = JSON.parse(serializedData) as Eip712Payload
if (typeof jsonData.domain === 'object' && typeof jsonData.types === 'object') {
const { domain, types, message, primaryType } = jsonData
msg = message
msgDomain = domain
msgTypes = types
msgPrimaryType = primaryType
} else {
// next check will throw since the data couldn't be parsed
}
} catch (e) {
// next check will throw since the data couldn't be parsed
}
if (typeof msgDomain !== 'object' || typeof msgTypes !== 'object' || typeof msg !== 'object') {
throw Error(
`invalid_arguments: Cannot sign typed data. 'domain', 'types', and 'message' must be provided`,
)
}
delete msgTypes.EIP712Domain
const { signer } = await this.getAccountAndSignerByKeyRef(keyRef)
const signature = await signer.signTypedData(msgDomain, msgTypes, msg)
return signature
}
/**
* @returns a `0x` prefixed hex string representing the signed message
*/
private async eth_signMessage(keyRef: Pick<IKey, 'kid'>, rawMessageBytes: Uint8Array) {
const { signer } = await this.getAccountAndSignerByKeyRef(keyRef)
const signature = await signer.signMessage(rawMessageBytes)
// HEX encoded string, 0x prefixed
return signature
}
}