Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions clients/js/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ module.exports = {
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
},
};
21 changes: 7 additions & 14 deletions clients/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,14 @@
"src"
],
"sideEffects": false,
"main": "./lib/cjs/index.cjs",
"module": "./lib/esm/index.js",
"types": "./lib/cjs/index.d.ts",
"exports": {
"node": {
"import": "./lib/esm/index.js",
"require": "./lib/cjs/index.cjs"
},
"default": "./lib/esm/index.js"
".": "./lib/index.js",
"./compat/viem": "./lib/compat/viem.js"
},
"scripts": {
"lint": "prettier --cache --check . && eslint --ignore-path .gitignore .",
"format": "prettier --cache --write . && eslint --ignore-path .gitignore --fix .",
"build": "npm run build:esm && npm run build:cjs",
"build:esm": "tsc -p ./tsconfig.json",
"build:cjs": "tsc -p ./tsconfig.cjs.json && node scripts/rename-cjs",
"build": "tsc -b",
"test": "jest",
"coverage": "jest --coverage",
"prepublishOnly": "npm run build"
Expand All @@ -47,12 +39,13 @@
"@ethersproject/bytes": "^5.7.0",
"@ethersproject/providers": "^5.7.1",
"@ethersproject/rlp": "^5.7.0",
"@noble/hashes": "^1.3.1",
Copy link
Copy Markdown
Contributor

@aefhm aefhm Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that this is consolidated/merged.

"@oasisprotocol/deoxysii": "^0.0.5",
"cborg": "^1.9.5",
"ethers6": "npm:ethers@^6.6.1",
"js-sha512": "^0.8.0",
"tweetnacl": "^1.0.3",
"type-fest": "^2.19.0"
"type-fest": "^2.19.0",
"viem": "^2.0.3"
},
"devDependencies": {
"@ethersproject/transactions": "^5.7.0",
Expand All @@ -70,6 +63,6 @@
"prettier": "^2.7.1",
"ts-jest": "^28.0.8",
"typedoc": "^0.25.1",
"typescript": "^4.8.3"
"typescript": "^5.1.6"
}
}
52 changes: 30 additions & 22 deletions clients/js/src/cipher.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import {
BytesLike,
arrayify,
hexlify,
isBytesLike,
} from '@ethersproject/bytes';
import * as cbor from 'cborg';
import { hmac } from '@noble/hashes/hmac';
import { sha512_256 } from '@noble/hashes/sha512';
import deoxysii from '@oasisprotocol/deoxysii';
// @ts-expect-error
import * as cbor from 'cborg';
import { IncomingMessage } from 'http';
import { sha512_256 } from 'js-sha512';
import nacl, { BoxKeyPair } from 'tweetnacl';
import { Promisable } from 'type-fest';
import { isBytes, isHex, toBytes, toHex } from 'viem';

import { CallError, NETWORKS, OASIS_CALL_DATA_PUBLIC_KEY } from './index.js';

Expand Down Expand Up @@ -52,16 +49,16 @@ export abstract class Cipher {
): Promise<Uint8Array>;

/** Encrypts the plaintext and encodes it for sending. */
public async encryptEncode(plaintext?: BytesLike): Promise<string> {
public async encryptEncode(plaintext?: BytesLike): Promise<`0x${string}`> {
const envelope = await this.encryptEnvelope(plaintext);
return envelope ? hexlify(cbor.encode(envelope)) : '';
return envelope ? toHex(cbor.encode(envelope)) : '0x';
}

/** Encrypts the plaintext and formats it into an envelope. */
public async encryptEnvelope(
plaintext?: BytesLike,
): Promise<Envelope | undefined> {
if (plaintext === undefined) return;
if (plaintext === undefined || plaintext === '0x') return;
if (!isBytesLike(plaintext)) {
throw new Error('Attempted to sign tx having non-byteslike data.');
}
Expand Down Expand Up @@ -113,7 +110,7 @@ export abstract class Cipher {

/** Decrypts the data contained within a hex-encoded serialized envelope. */
public async decryptEncoded(callResult: BytesLike): Promise<string> {
return hexlify(
return toHex(
await this.decryptCallResult(cbor.decode(arrayify(callResult))),
);
}
Expand Down Expand Up @@ -184,10 +181,7 @@ export class X25519DeoxysII extends Cipher {
/** Creates a new cipher using an ephemeral keypair stored in memory. */
static ephemeral(peerPublicKey: BytesLike): X25519DeoxysII {
const keypair = nacl.box.keyPair();
return new X25519DeoxysII(
keypair,
arrayify(peerPublicKey, { allowMissingPrefix: true }),
);
return new X25519DeoxysII(keypair, arrayify(peerPublicKey));
}

static fromSecretKey(
Expand All @@ -202,11 +196,10 @@ export class X25519DeoxysII extends Cipher {
super();
this.publicKey = keypair.publicKey;
// Derive a shared secret using X25519 (followed by hashing to remove ECDH bias).
const keyBytes = sha512_256.hmac
.create('MRAE_Box_Deoxys-II-256-128')
this.key = hmac
.create(sha512_256, 'MRAE_Box_Deoxys-II-256-128')
.update(nacl.scalarMult(keypair.secretKey, peerPublicKey))
.arrayBuffer();
this.key = new Uint8Array(keyBytes);
.digest();
this.cipher = new deoxysii.AEAD(new Uint8Array(this.key)); // deoxysii owns the input
}

Expand Down Expand Up @@ -245,8 +238,7 @@ export class Mock extends Cipher {
nonce: Uint8Array,
ciphertext: Uint8Array,
): Promise<Uint8Array> {
if (hexlify(nonce) !== hexlify(Mock.NONCE))
throw new Error('incorrect nonce');
if (toHex(nonce) !== toHex(Mock.NONCE)) throw new Error('incorrect nonce');
return ciphertext;
}
}
Expand Down Expand Up @@ -352,3 +344,19 @@ function makeCallDataPublicKeyBody(): string {
params: [],
});
}

type BytesLike = string | Uint8Array;

function isBytesLike(byteslike: any): byteslike is BytesLike {
return isHex(byteslike) || isBytes(byteslike);
}

function arrayify(byteslike: BytesLike): Uint8Array {
if (isBytes(byteslike)) {
return byteslike;
} else if (isHex(byteslike)) {
return toBytes(byteslike.startsWith('0x') ? `0x${byteslike}` : byteslike);
} else {
throw new Error('attempted to decode non-byteslike data');
}
}
7 changes: 4 additions & 3 deletions clients/js/src/compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TransactionRequest,
Web3Provider,
} from '@ethersproject/providers';
// @ts-expect-error
import * as cbor from 'cborg';
import { ethers as ethers6 } from 'ethers6';
import { RequireAtLeastOne } from 'type-fest';
Expand Down Expand Up @@ -351,7 +352,7 @@ function hookEthers5Call(
return provider[method](
{
...callP,
data: cipher.encryptEncode(await callP.data),
data: cipher.encryptEncode(arrayify((await callP.data) ?? '0x')),
},
blockTag,
);
Expand Down Expand Up @@ -428,7 +429,7 @@ function hookEthers6Call(
function hookEthers5Send(send: Ethers5Call, cipher: Cipher): Ethers5Call {
return async (tx: Deferrable<TransactionRequest>, ...rest) => {
const data = await tx.data;
tx.data = cipher.encryptEncode(data);
tx.data = cipher.encryptEncode(arrayify(data ?? '0x'));
return send(tx, ...rest);
};
}
Expand Down Expand Up @@ -598,7 +599,7 @@ function envelopeFormatOk(
): boolean {
if (Object.keys(extra).length > 0) return false;
if (!body) return false;
if (format && format !== CipherKind.Plain) {
if (format && (format as CipherKind) !== CipherKind.Plain) {
if (isBytesLike(body) || !isBytesLike(body.data)) return false;
}
return true;
Expand Down
28 changes: 28 additions & 0 deletions clients/js/src/compat/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Cipher } from '../cipher.js';

const SAPPHIRE_PROP = 'sapphire';

export type SapphireAnnex = {
[SAPPHIRE_PROP]: {
cipher: Cipher;
};
};

export type Hooks<T> = {
[K in keyof T]?: T[K];
};

export function makeProxy<U extends object>(
upstream: U,
cipher: Cipher,
hooks: Hooks<U>,
): U & SapphireAnnex {
return new Proxy(upstream, {
get(upstream, prop) {
if (prop === SAPPHIRE_PROP) return { cipher };
if (prop in hooks) return Reflect.get(hooks, prop);
const value = Reflect.get(upstream, prop);
return typeof value === 'function' ? value.bind(upstream) : value;
},
}) as U & SapphireAnnex;
}
110 changes: 110 additions & 0 deletions clients/js/src/compat/viem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
Chain,
EIP1193Provider,
Hex,
PublicClient,
WalletClient,
toBytes,
} from 'viem';

import {
Cipher,
X25519DeoxysII,
fetchRuntimePublicKeyByChainId,
lazy as lazyCipher,
} from '../cipher.js';
import { Hooks, makeProxy } from './utils.js';

export function wrapPublicClient<U extends PublicClient>(
upstream: U,
overrides?: Partial<{
cipher: Cipher;
transport: { request: EIP1193Provider['request'] };
}>,
): U {
const transport = overrides?.transport ?? upstream.transport;
if (!transport)
throw new Error(
'unknown transport. please configure one on the wallet client or pass it as an override',
);
upstream.transport.request;
const cipher =
overrides?.cipher ??
lazyCipher(async () => {
const rtPubKey = await fetchRuntimePublicKey(transport, upstream.chain);
return X25519DeoxysII.ephemeral(rtPubKey);
});
return makeProxy(upstream, cipher, {
async call(req) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that we extend publicClient strictly for signed queries.

return upstream.call({
...req,
data: await cipher.encryptEncode(req.data),
});
},
} as Hooks<U>);
}

export function wrapWalletClient<U extends WalletClient>(
upstream: U,
overrides?: Partial<{
cipher: Cipher;
transport: { request: EIP1193Provider['request'] };
}>,
): U {
const transport = overrides?.transport ?? upstream.transport;
if (!transport)
throw new Error(
'unknown transport. please configure one on the wallet client or pass it as an override',
);
upstream.transport.request;
const cipher =
overrides?.cipher ??
lazyCipher(async () => {
const rtPubKey = await fetchRuntimePublicKey(transport, upstream.chain);
return X25519DeoxysII.ephemeral(rtPubKey);
});

return makeProxy<U>(upstream, cipher, {
async sendTransaction(req) {
req.data = await cipher.encryptEncode(req.data);
return upstream.sendTransaction(req);
},
async signTransaction(req) {
req.data = await cipher.encryptEncode(req.data);
return upstream.signTransaction(req);
},
} as Hooks<U>);
}

export async function getDefaultCipher(pc: PublicClient): Promise<Cipher> {
return X25519DeoxysII.ephemeral(
await fetchRuntimePublicKey(pc.transport, pc.chain),
);
}

async function fetchRuntimePublicKey(
{
request,
}: {
request: EIP1193Provider['request'];
},
chain?: Chain,
): Promise<Uint8Array> {
try {
const resp: any = await request({
method: 'oasis_callDataPublicKey' as any,
args: [],
});
if (resp && 'key' in resp) {
return toBytes(resp.key);
}
} catch (e: any) {
console.error(
'failed to fetch runtime public key using upstream transport:',
e,
);
}
if (!chain)
throw new Error('unable to fetch runtime public key. chain not provided');
return fetchRuntimePublicKeyByChainId(chain.id);
}
1 change: 1 addition & 0 deletions clients/js/src/signed_calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@ethersproject/abstract-signer';
import { BigNumber, BigNumberish } from '@ethersproject/bignumber';
import { BytesLike } from '@ethersproject/bytes';
// @ts-expect-error
import * as cbor from 'cborg';
import { ethers } from 'ethers6';
import type {
Expand Down
7 changes: 0 additions & 7 deletions clients/js/tsconfig.cjs.json

This file was deleted.

9 changes: 5 additions & 4 deletions clients/js/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
"declarationMap": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"module": "es2020",
"moduleResolution": "node",
"outDir": "lib/esm",
"module": "node16",
"moduleResolution": "node16",
"outDir": "lib",
"paths": {
"@oasisprotocol/sapphire-paratime": ["src/index"],
"@oasisprotocol/sapphire-paratime/*": ["src/*"]
},
"sourceMap": true,
"skipLibCheck": true,
"strict": true,
"target": "es6"
"target": "es2022"
},
"typedocOptions": {
"entryPoints": ["src/index.ts"],
Expand Down
Loading