Skip to content

Commit

Permalink
Message encryption for session validation (#41)
Browse files Browse the repository at this point in the history
* move/rename cipher stuff

* MWP message encryption

* ArrayBuffer can be transferred over postMessage

* inject KeyStorage

* cleanup

* cleanup

* cleanup

* separate out message conversion

* more granular processing so that host can reuse

* cleanup

* rename. handshake request to conform to Action

* host

* host implementation

* cleanup

* fix test

* fix errors
  • Loading branch information
bangtoven committed Feb 29, 2024
1 parent 126493b commit 3c353fc
Show file tree
Hide file tree
Showing 14 changed files with 180 additions and 99 deletions.
2 changes: 1 addition & 1 deletion packages/wallet-sdk/src/connector/ConnectorInterface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AddressString } from '../core/type';
import { RequestArguments } from ':wallet-sdk/src/provider/ProviderInterface';
import { RequestArguments } from '../provider/ProviderInterface';

export interface Connector {
handshake(): Promise<AddressString[]>;
Expand Down
136 changes: 102 additions & 34 deletions packages/wallet-sdk/src/connector/scw/SCWConnector.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,132 @@
import { standardErrors } from '../../core/error';
import { AddressString } from '../../core/type';
import { RequestArguments } from '../../provider/ProviderInterface';
import { PopUpCommunicator } from '../../transport/PopUpCommunicator';
import { LIB_VERSION } from '../../version';
import { Connector } from '../ConnectorInterface';
import { exportKeyToHexString, importKeyFromHexString } from './protocol/key/Cipher';
import { KeyStorage } from './protocol/key/KeyStorage';
import {
decryptContent,
encryptContent,
SCWRequestMessage,
SCWResponseMessage,
} from './protocol/SCWMessage';
import { Action, SupportedEthereumMethods } from './protocol/type/Action';
import { Request } from './protocol/type/Request';
import { AddressString } from ':wallet-sdk/src/core/type';
import { RequestArguments } from ':wallet-sdk/src/provider/ProviderInterface';
import { SCWResponse } from './protocol/type/Response';

export class SCWConnector implements Connector {
protected appName = '';
private appLogoUrl: string | null = null;
private appName: string;
private appLogoUrl: string | null;
// TODO: handle chainId
private activeChainId = 1;

private puc: PopUpCommunicator;
private keyStorage: KeyStorage;

constructor(options: { appName: string; appLogoUrl: string | null; puc: PopUpCommunicator }) {
constructor(options: {
appName: string;
appLogoUrl: string | null;
puc: PopUpCommunicator;
keyStorage: KeyStorage;
}) {
this.appName = options.appName;
this.appLogoUrl = options.appLogoUrl;
this.puc = options.puc;
this.keyStorage = options.keyStorage;
}

public async handshake() {
// first method called by provider, for now just returns ethereum accounts
// later: handle passing dapp metadata, storing session, etc.
// later: return spec-compliant errors for unsupported methods
public async handshake(): Promise<AddressString[]> {
// TODO
await this.puc.connect();

// request accounts
return await this.request<AddressString[]>({
method: 'eth_requestAccounts',
params: { appName: this.appName, appLogoUrl: this.appLogoUrl },
const handshakeMessage = await this.createRequestMessage({
handshake: {
method: SupportedEthereumMethods.EthRequestAccounts,
params: {
appName: this.appName,
appLogoUrl: this.appLogoUrl,
},
},
});
}

private _checkMethod(method: string): boolean {
return Object.values(SupportedEthereumMethods).includes(method as SupportedEthereumMethods);
const response = (await this.puc.request(handshakeMessage)) as SCWResponseMessage;

// throw protocol level error
if ('error' in response.content) {
throw response.content.error;
}

// take the peer's public key and store it
const peerPublicKey = await importKeyFromHexString('public', response.sender);
await this.keyStorage.setPeerPublicKey(peerPublicKey);

return this.decodeResponseMessage<AddressString[]>(response);
}

public async request<T>(request: RequestArguments): Promise<T> {
if (!this._checkMethod(request.method)) {
return Promise.reject(
standardErrors.provider.unsupportedMethod(
`${request.method} is not supported for SCW at this time`
)
);
}
// TODO: this check makes sense, but connected isn't set properly so it prevents
// need to investigate
// if (!this.puc.connected) {
await this.puc.connect();
// }

const pucRequest: Request = {
action: request as Action,
const sharedSecret = await this.keyStorage.getSharedSecret();
if (!sharedSecret) {
// TODO: better error
throw new Error('Invalid session');
}

const encrypted = await encryptContent(
{
action: request as Action,
chainId: this.activeChainId,
},
sharedSecret
);
const message = await this.createRequestMessage({ encrypted });

return this.puc
.request(message)
.then((response) => response as SCWResponseMessage)
.then(this.decodeResponseMessage<T>);
}

private async createRequestMessage(
content: SCWRequestMessage['content']
): Promise<SCWRequestMessage> {
const publicKey = await exportKeyToHexString('public', await this.keyStorage.getOwnPublicKey());
return {
type: 'scw',
id: crypto.randomUUID(),
sender: publicKey,
content,
version: LIB_VERSION,
timestamp: new Date(),
};
}

return this.puc.request<T>(pucRequest).then((response) => {
const result = response.content.result;
private async decodeResponseMessage<T>(message: SCWResponseMessage): Promise<T> {
const content = message.content;

if ('error' in result) {
throw result.error;
}
// throw protocol level error
if ('error' in content) {
throw content.error;
}

return result.value;
});
const sharedSecret = await this.keyStorage.getSharedSecret();
if (!sharedSecret) {
// TODO: better error
throw new Error('Invalid session');
}

const decrypted: SCWResponse<T> = await decryptContent(content.encrypted, sharedSecret);
const result = decrypted.result;

// check for ActionResult error
if ('error' in result) {
throw result.error;
}

return result.value;
}
}
57 changes: 57 additions & 0 deletions packages/wallet-sdk/src/connector/scw/protocol/SCWMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { UUID } from 'crypto';

import { Message } from '../../../transport/CrossDomainCommunicator';
import { decrypt, encrypt, EncryptedData } from './key/Cipher';
import { SupportedEthereumMethods } from './type/Action';
import { SCWRequest } from './type/Request';
import { SCWResponse } from './type/Response';

interface SCWMessage extends Message {
type: 'scw';
id: UUID;
sender: string; // hex encoded public key of the sender
content: unknown;
version: string;
timestamp: Date;
}

export interface SCWRequestMessage extends SCWMessage {
content:
| {
handshake: {
method: SupportedEthereumMethods.EthRequestAccounts;
params: {
appName: string;
appLogoUrl: string | null;
};
};
}
| {
encrypted: EncryptedData;
};
}

export interface SCWResponseMessage extends SCWMessage {
requestId: UUID;
content:
| {
encrypted: EncryptedData;
}
| {
error: Error;
};
}

export async function encryptContent<T>(
content: SCWRequest | SCWResponse<T>,
sharedSecret: CryptoKey
): Promise<EncryptedData> {
return encrypt(sharedSecret, JSON.stringify(content));
}

export async function decryptContent<R extends SCWRequest | SCWResponse<U>, U>(
encryptedData: EncryptedData,
sharedSecret: CryptoKey
): Promise<R> {
return JSON.parse(await decrypt(sharedSecret, encryptedData));
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { decrypt, deriveSharedSecret, encrypt, generateKeyPair } from './KeyManager';
import { decrypt, deriveSharedSecret, encrypt, generateKeyPair } from './Cipher';

describe('SCWCipher', () => {
describe('generateKeyPair', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { hexStringToUint8Array, uint8ArrayToHex } from '../../../core/util';
import { hexStringToUint8Array, uint8ArrayToHex } from '../../../../core/util';

export type EncryptedData = {
iv: Uint8Array;
iv: ArrayBuffer;
cipherText: ArrayBuffer;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ScopedLocalStorage } from '../../../../lib/ScopedLocalStorage';
import { generateKeyPair } from '../KeyManager';
import { generateKeyPair } from './Cipher';
import { KeyStorage } from './KeyStorage';

describe('KeyStorage', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
exportKeyToHexString,
generateKeyPair,
importKeyFromHexString,
} from '../KeyManager';
} from './Cipher';

interface StorageItem {
storageKey: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Action } from './Action';

export type Request = {
action: Action;
export type SCWRequest = {
action: Action; // JSON-RPC call
chainId: number;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ActionResult } from './ActionResult';

export type Response<T> = {
result: ActionResult<T>;
export type SCWResponse<T> = {
result: ActionResult<T>; // JSON-RPC result
data?: unknown;
};
2 changes: 2 additions & 0 deletions packages/wallet-sdk/src/provider/EIP1193Provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import EventEmitter from 'eventemitter3';

import { Connector } from '../connector/ConnectorInterface';
import { KeyStorage } from '../connector/scw/protocol/key/KeyStorage';
import { SCWConnector } from '../connector/scw/SCWConnector';
import { standardErrors } from '../core/error';
import { AddressString } from '../core/type';
Expand Down Expand Up @@ -64,6 +65,7 @@ export class EIP1193Provider extends EventEmitter implements ProviderInterface {
appName: this._appName,
appLogoUrl: this._appLogoUrl,
puc: this._popupCommunicator,
keyStorage: new KeyStorage(this._storage),
});
};

Expand Down
14 changes: 3 additions & 11 deletions packages/wallet-sdk/src/transport/CrossDomainCommunicator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UUID } from 'crypto';

export interface Message {
type: 'config' | 'web3Request' | 'web3Response';
type: 'config' | 'scw';
id: UUID;
}

Expand All @@ -28,15 +28,7 @@ export abstract class CrossDomainCommunicator {
}

protected peerWindow: Window | null = null;
protected postMessage(message: Message | Omit<Message, 'id'>): UUID {
const id = 'id' in message ? message.id : crypto.randomUUID();
this.peerWindow?.postMessage(
{
...message,
id,
},
this.url?.origin ?? '*'
);
return id;
protected postMessage(message: Message) {
this.peerWindow?.postMessage(message, this.url?.origin ?? '*');
}
}
17 changes: 4 additions & 13 deletions packages/wallet-sdk/src/transport/PopUpCommunicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
isConfigMessage,
} from './ConfigMessage';
import { CrossDomainCommunicator, Message } from './CrossDomainCommunicator';
import { SCWRequestMessage, SCWResponseMessage } from './SCWMessage';

// TODO: how to set/change configurations?
const POPUP_WIDTH = 688;
Expand Down Expand Up @@ -98,24 +97,16 @@ export class PopUpCommunicator extends CrossDomainCommunicator {
this.postMessage(configMessage);
}

request<T>(request: SCWRequestMessage['content']): Promise<SCWResponseMessage<T>> {
// Send message that expect to receive response
request(message: Message): Promise<Message> {
return new Promise((resolve, reject) => {
if (!this.peerWindow) {
reject(new Error('No pop up window found. Make sure to run .connect() before .send()'));
}

const requestMessage: SCWRequestMessage = {
type: 'web3Request',
id: crypto.randomUUID(),
content: request,
timestamp: new Date(),
};
this.postMessage(message);

const requestId = this.postMessage(requestMessage);

this.requestResolutions.set(requestId, (resEnv) => {
resolve(resEnv as SCWResponseMessage<T>);
});
this.requestResolutions.set(message.id, resolve);
});
}

Expand Down
22 changes: 0 additions & 22 deletions packages/wallet-sdk/src/transport/SCWMessage.ts

This file was deleted.

9 changes: 0 additions & 9 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,5 @@
"noUnusedParameters": true,
"target": "es2016",
"types": ["node", "jest", "@testing-library/jest-dom"],
// Paths
"paths": {
":wallet-sdk/*": [
"packages/wallet-sdk/*"
],
":wallet-sdk-testapp/*": [
"apps/testapp/*"
],
}
}
}

0 comments on commit 3c353fc

Please sign in to comment.