Skip to content

Commit

Permalink
clients/js: sapphire snap initial integration
Browse files Browse the repository at this point in the history
  • Loading branch information
CedarMist committed Oct 9, 2024
1 parent f708f39 commit 9d74f0f
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 11 deletions.
6 changes: 3 additions & 3 deletions clients/js/src/cipher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export class X25519DeoxysII extends Cipher {
public override readonly epoch: number | undefined;

private cipher: deoxysii.AEAD;
private key: Uint8Array; // Stored for curious users.
public secretKey: Uint8Array; // Stored for curious users.

/** Creates a new cipher using an ephemeral keypair stored in memory. */
static ephemeral(peerPublicKey: BytesLike, epoch?: number): X25519DeoxysII {
Expand Down Expand Up @@ -247,8 +247,8 @@ export class X25519DeoxysII extends Cipher {
.update(naclScalarMult(keypair.secretKey, peerPublicKey))
.digest().buffer;

this.key = new Uint8Array(keyBytes);
this.cipher = new deoxysii.AEAD(new Uint8Array(this.key)); // deoxysii owns the input
this.secretKey = new Uint8Array(keyBytes);
this.cipher = new deoxysii.AEAD(new Uint8Array(this.secretKey)); // deoxysii owns the input
}

public encrypt(plaintext: Uint8Array): {
Expand Down
80 changes: 73 additions & 7 deletions clients/js/src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: Apache-2.0

import { BytesLike } from './ethersutils.js';
import { BytesLike, hexlify } from './ethersutils.js';
import { KeyFetcher } from './calldatapublickey.js';
import { SUBCALL_ADDR, CALLDATAPUBLICKEY_CALLDATA } from './constants.js';
import { Cipher } from './cipher.js';

// -----------------------------------------------------------------------------
// https://eips.ethereum.org/EIPS/eip-2696#interface
Expand Down Expand Up @@ -43,6 +44,7 @@ export function isLegacyProvider<T extends object>(

export interface SapphireWrapOptions {
fetcher: KeyFetcher;
enableSapphireSnap: boolean | undefined
}

export function fillOptions(
Expand Down Expand Up @@ -80,10 +82,10 @@ export function isWrappedEthereumProvider<P extends EIP2696_EthereumProvider>(
* @param options (optional) Re-use parameters from other providers
* @returns Sapphire wrapped provider
*/
export function wrapEthereumProvider<P extends EIP2696_EthereumProvider>(
export async function wrapEthereumProvider<P extends EIP2696_EthereumProvider>(
upstream: P,
options?: SapphireWrapOptions,
): P {
): Promise<P> {
if (isWrappedEthereumProvider(upstream)) {
return upstream;
}
Expand All @@ -99,7 +101,7 @@ export function wrapEthereumProvider<P extends EIP2696_EthereumProvider>(
// if we do this, don't then re-wrap the send() function
// only wrap the send() function if there was a request() function

const request = makeSapphireRequestFn(upstream, filled_options);
const request = await makeSapphireRequestFn(upstream, filled_options);
const hooks: Record<string, unknown> = { request };

// We prefer a request() method, but a provider may expose a send() method
Expand All @@ -124,6 +126,62 @@ export function wrapEthereumProvider<P extends EIP2696_EthereumProvider>(
);
}

// -----------------------------------------------------------------------------
// Interact with the Sapphire MetaMask Snap to provide transaction insights
// This sends the encryption key on a per-transaction basis

interface SnapInfoT {
version: string;
id: string;
enabled: boolean;
blocked: boolean;
}

const SAPPHIRE_SNAP_PNPM_ID = 'npm:@oasisprotocol/sapphire-snap';

async function detectSapphireSnap (provider: EIP2696_EthereumProvider) {
try {
const installedSnaps = await provider.request({method: 'wallet_getSnaps'}) as Record<string,SnapInfoT>;
for( const snap of Object.values(installedSnaps) ) {
// TODO: if it's localhost, interrogate snap
if( snap.id == SAPPHIRE_SNAP_PNPM_ID ) {
return snap.id;
}
}
}
catch( e:any ) {
return undefined;
}
}

async function notifySapphireSnap(
snapId:string,
cipher:Cipher,
transactionData: BytesLike,
options:SapphireWrapOptions,
provider: EIP2696_EthereumProvider
) {
const secretKey = (cipher as any).secretKey as Uint8Array | undefined;
if( secretKey ) {
const peerPublicKey = await options.fetcher.fetch(provider);
await provider.request({
method: 'wallet_invokeSnap',
params: {
snapId: snapId,
request: {
method: 'setTransactionDecryptKeys',
params: {
id: transactionData,
ephemeralSecretKey: hexlify(secretKey),
peerPublicKey: peerPublicKey.key,
peerPublicKeyEpoch: peerPublicKey.epoch
}
}
}
});
}
}

const SAPPHIRE_EIP1193_REQUESTFN = '#SAPPHIRE_EIP1193_REQUESTFN' as const;

export function isWrappedRequestFn<
Expand All @@ -148,35 +206,43 @@ export function isCallDataPublicKeyQuery(params?: object | readonly unknown[]) {
* @param options
* @returns
*/
export function makeSapphireRequestFn(
export async function makeSapphireRequestFn(
provider: EIP2696_EthereumProvider,
options?: SapphireWrapOptions,
): EIP2696_EthereumProvider['request'] {
): Promise<EIP2696_EthereumProvider['request']> {
if (isWrappedRequestFn(provider.request)) {
return provider.request;
}

const filled_options = fillOptions(options);

const snapId = filled_options.enableSapphireSnap ? await detectSapphireSnap(provider) : undefined;

const f = async (args: EIP1193_RequestArguments) => {
const cipher = await filled_options.fetcher.cipher(provider);
const { method, params } = args;

let transactionData : BytesLike | undefined = undefined;
// Encrypt requests which can be encrypted
if (
params &&
Array.isArray(params) &&
/^eth_((send|sign)Transaction|call|estimateGas)$/.test(method) &&
params[0].data // Ignore balance transfers without calldata
) {
params[0].data = cipher.encryptCall(params[0].data);
transactionData = params[0].data = cipher.encryptCall(params[0].data);
}

const res = await provider.request({
method,
params: params ?? [],
});

if( snapId !== undefined && transactionData !== undefined ) {
// Run in background so as to not delay results
notifySapphireSnap(snapId, cipher, transactionData, filled_options, provider);
}

// Decrypt responses which return encrypted data
if (method === 'eth_call') {
// If it's an unencrypted core.CallDataPublicKey query, don't attempt to decrypt the response
Expand Down
2 changes: 1 addition & 1 deletion clients/js/test/cipher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('X25519DeoxysII', () => {
expect(hexlify(cipher.publicKey)).toEqual(
'0x3046db3fa70ce605457dc47c48837ebd8bd0a26abfde5994d033e1ced68e2576',
);
expect(hexlify(cipher['key'])).toEqual(
expect(hexlify(cipher['secretKey'])).toEqual(
'0xe69ac21066a8c2284e8fdc690e579af4513547b9b31dd144792c1904b45cf586',
);

Expand Down

0 comments on commit 9d74f0f

Please sign in to comment.