Skip to content

Commit 6c96c83

Browse files
authored
Merge pull request #1013 from input-output-hk/feat/track-cip30-origin
feat: track cip30 method call origin
2 parents 0da7b19 + 75c8af6 commit 6c96c83

File tree

26 files changed

+361
-171
lines changed

26 files changed

+361
-171
lines changed

packages/dapp-connector/src/AuthenticatorApi/PersistentAuthenticator.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { AuthenticatorApi, Origin, RequestAccess } from './types';
22
import { Logger } from 'ts-log';
33
import { PersistentAuthenticatorStorage } from './PersistentAuthenticatorStorage';
4+
import { Runtime } from 'webextension-polyfill';
5+
import { senderOrigin } from '../util';
46

57
export interface PersistentAuhenticatorOptions {
68
requestAccess: RequestAccess;
@@ -32,13 +34,18 @@ export class PersistentAuthenticator implements AuthenticatorApi {
3234
this.#originsReady = storage.get();
3335
}
3436

35-
async requestAccess(origin: Origin) {
37+
async requestAccess(sender: Runtime.MessageSender) {
38+
const origin = senderOrigin(sender);
39+
if (!origin) {
40+
this.#logger.warn('Invalid sender url', sender);
41+
return false;
42+
}
3643
const origins = await this.#originsReady;
3744
if (origins.includes(origin)) {
3845
return true;
3946
}
4047
try {
41-
const accessGranted = await this.#requestAccess(origin);
48+
const accessGranted = await this.#requestAccess(sender);
4249
if (accessGranted) {
4350
const newOrigins = [...origins, origin];
4451
if (await this.#store(newOrigins)) {
@@ -52,7 +59,12 @@ export class PersistentAuthenticator implements AuthenticatorApi {
5259
return false;
5360
}
5461

55-
async revokeAccess(origin: Origin) {
62+
async revokeAccess(sender: Runtime.MessageSender) {
63+
const origin = senderOrigin(sender);
64+
if (!origin) {
65+
this.#logger.warn('Invalid sender url', sender);
66+
return false;
67+
}
5668
const origins = await this.#originsReady;
5769
const idx = origins.indexOf(origin);
5870
if (idx >= 0) {
@@ -67,7 +79,8 @@ export class PersistentAuthenticator implements AuthenticatorApi {
6779
return false;
6880
}
6981

70-
async haveAccess(origin: Origin) {
82+
async haveAccess(sender: Runtime.MessageSender) {
83+
const origin = senderOrigin(sender);
7184
if (!origin) return false;
7285
const origins = await this.#originsReady;
7386
return origins.includes(origin);

packages/dapp-connector/src/AuthenticatorApi/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { Runtime } from 'webextension-polyfill';
2+
13
export type Origin = string;
24

35
/** Resolve true to authorise access to the WalletAPI, or resolve false to deny. Errors: `ApiError` */
4-
export type RequestAccess = (origin: Origin) => Promise<boolean>;
6+
export type RequestAccess = (sender: Runtime.MessageSender) => Promise<boolean>;
57
export type RevokeAccess = RequestAccess;
68
export type HaveAccess = RequestAccess;
79

packages/dapp-connector/src/WalletApi/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Cardano } from '@cardano-sdk/core';
22
import { Ed25519PublicKeyHex } from '@cardano-sdk/crypto';
33
import { HexBlob } from '@cardano-sdk/util';
4+
import { Runtime } from 'webextension-polyfill';
45

56
/** A hex-encoded string of the corresponding bytes. */
67
export type Bytes = string;
@@ -212,3 +213,10 @@ export interface CipExtensionApis {
212213
}
213214

214215
export type Cip30WalletApiWithPossibleExtensions = Cip30WalletApi & Partial<CipExtensionApis>;
216+
217+
export type SenderContext = { sender: Runtime.MessageSender };
218+
type FnWithSender<T> = T extends (...args: infer Args) => infer R ? (context: SenderContext, ...args: Args) => R : T;
219+
220+
export type WithSenderContext<T> = {
221+
[K in keyof T]: FnWithSender<T[K]>;
222+
};

packages/dapp-connector/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './errors';
22
export * from './WalletApi';
33
export * from './AuthenticatorApi';
44
export * from './injectGlobal';
5+
export * from './util';

packages/dapp-connector/src/util.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Runtime } from 'webextension-polyfill';
2+
3+
export const senderOrigin = (sender?: Runtime.MessageSender): string | null => {
4+
try {
5+
const { origin } = new URL(sender?.url || 'throw');
6+
return origin;
7+
} catch {
8+
return null;
9+
}
10+
};
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Origin, PersistentAuthenticator } from '../../src';
1+
import { Origin, PersistentAuthenticator, senderOrigin } from '../../src';
2+
import { Runtime } from 'webextension-polyfill';
23
import { dummyLogger } from 'ts-log';
34

45
const createStubStorage = () => {
@@ -12,8 +13,8 @@ const createStubStorage = () => {
1213
};
1314

1415
describe('PersistentAuthenticator', () => {
15-
const origin1: Origin = 'origin1';
16-
const origin2: Origin = 'origin2';
16+
const sender1: Runtime.MessageSender = { url: 'https://sender1.com' };
17+
const sender2: Runtime.MessageSender = { url: 'https://sender2.com' };
1718
let requestAccess: jest.Mock;
1819
let storage: ReturnType<typeof createStubStorage>;
1920
let authenticator: PersistentAuthenticator;
@@ -28,87 +29,87 @@ describe('PersistentAuthenticator', () => {
2829
describe('requestAccess', () => {
2930
it('resolves to true if allowed and persists the decision', async () => {
3031
requestAccess.mockResolvedValueOnce(true);
31-
expect(await authenticator.requestAccess(origin1)).toBe(true);
32-
expect(await authenticator.requestAccess(origin1)).toBe(true);
32+
expect(await authenticator.requestAccess(sender1)).toBe(true);
33+
expect(await authenticator.requestAccess(sender1)).toBe(true);
3334
expect(requestAccess).toBeCalledTimes(1);
34-
expect(await storage.get()).toContain(origin1);
35+
expect(await storage.get()).toContain(senderOrigin(sender1));
3536
});
3637

3738
it('resolves to false if denied an error and does not persist the decision', async () => {
3839
requestAccess.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
39-
expect(await authenticator.requestAccess(origin1)).toBe(false);
40-
expect(await storage.get()).not.toContain(origin1);
41-
expect(await authenticator.requestAccess(origin1)).toBe(true);
42-
expect(await storage.get()).toContain(origin1);
40+
expect(await authenticator.requestAccess(sender1)).toBe(false);
41+
expect(await storage.get()).not.toContain(senderOrigin(sender1));
42+
expect(await authenticator.requestAccess(sender1)).toBe(true);
43+
expect(await storage.get()).toContain(senderOrigin(sender1));
4344
expect(requestAccess).toBeCalledTimes(2);
4445
});
4546

4647
it('resolves to false if any error is encountered and does not persist the decision', async () => {
4748
requestAccess.mockResolvedValue(true).mockRejectedValueOnce(new Error('any error'));
48-
expect(await authenticator.requestAccess(origin1)).toBe(false);
49-
expect(await storage.get()).not.toContain(origin1);
49+
expect(await authenticator.requestAccess(sender1)).toBe(false);
50+
expect(await storage.get()).not.toContain(senderOrigin(sender1));
5051

5152
storage.set.mockResolvedValue(void 0).mockRejectedValueOnce(new Error('any error'));
52-
expect(await authenticator.requestAccess(origin1)).toBe(false);
53-
expect(await storage.get()).not.toContain(origin1);
53+
expect(await authenticator.requestAccess(sender1)).toBe(false);
54+
expect(await storage.get()).not.toContain(senderOrigin(sender1));
5455
});
5556

5657
it('caches storage by origin', async () => {
5758
requestAccess.mockResolvedValueOnce(true).mockResolvedValueOnce(false);
58-
expect(await authenticator.requestAccess(origin1)).toBe(true);
59-
expect(await authenticator.requestAccess(origin2)).toBe(false);
59+
expect(await authenticator.requestAccess(sender1)).toBe(true);
60+
expect(await authenticator.requestAccess(sender2)).toBe(false);
6061
expect(requestAccess).toBeCalledTimes(2);
6162
});
6263
});
6364

6465
describe('revokeAccess', () => {
6566
beforeEach(async () => {
6667
requestAccess.mockResolvedValueOnce(true);
67-
await authenticator.requestAccess(origin1);
68+
await authenticator.requestAccess(sender1);
6869
storage.set.mockReset();
6970
});
7071

7172
it('unknown origin => returns false', async () => {
72-
expect(await authenticator.revokeAccess(origin2)).toBe(false);
73+
expect(await authenticator.revokeAccess(sender2)).toBe(false);
7374
});
7475

7576
describe('allowed origin', () => {
7677
it('returns true and removes origin from cache', async () => {
77-
expect(await authenticator.revokeAccess(origin1)).toBe(true);
78-
expect(await authenticator.revokeAccess(origin1)).toBe(false);
78+
expect(await authenticator.revokeAccess(sender1)).toBe(true);
79+
expect(await authenticator.revokeAccess(sender1)).toBe(false);
7980
expect(storage.set).toBeCalledTimes(1);
8081
});
8182

8283
it('returns false if storage throws', async () => {
8384
storage.set.mockRejectedValueOnce(new Error('any error'));
84-
expect(await authenticator.revokeAccess(origin1)).toBe(false);
85+
expect(await authenticator.revokeAccess(sender1)).toBe(false);
8586
});
8687
});
8788
});
8889

8990
describe('haveAccess', () => {
9091
beforeEach(async () => {
9192
requestAccess.mockResolvedValueOnce(true);
92-
await authenticator.requestAccess(origin1);
93+
await authenticator.requestAccess(sender1);
9394
});
9495

9596
it('unknown origin => returns false', async () => {
96-
expect(await authenticator.haveAccess(origin2)).toBe(false);
97+
expect(await authenticator.haveAccess(sender2)).toBe(false);
9798
});
9899

99100
it('allowed origin => returns true', async () => {
100-
expect(await authenticator.haveAccess(origin1)).toBe(true);
101+
expect(await authenticator.haveAccess(sender1)).toBe(true);
101102
});
102103
});
103104

104105
describe('clear', () => {
105106
it('removes all origins', async () => {
106107
requestAccess.mockResolvedValue(true);
107-
await authenticator.requestAccess(origin1);
108-
await authenticator.requestAccess(origin2);
108+
await authenticator.requestAccess(sender1);
109+
await authenticator.requestAccess(sender2);
109110
await authenticator.clear();
110-
expect(await authenticator.haveAccess(origin1)).toBe(false);
111-
expect(await authenticator.haveAccess(origin2)).toBe(false);
111+
expect(await authenticator.haveAccess(sender1)).toBe(false);
112+
expect(await authenticator.haveAccess(sender2)).toBe(false);
112113
});
113114
});
114115
});
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { senderOrigin } from '../src';
2+
3+
describe('util', () => {
4+
describe('senderOrigin', () => {
5+
it('returns null when origin url is not present', () => {
6+
expect(senderOrigin()).toBe(null);
7+
expect(senderOrigin({ id: 'id' })).toBe(null);
8+
});
9+
it('returns origin url it is present', () => {
10+
expect(senderOrigin({ url: 'http://origin' })).toBe('http://origin');
11+
});
12+
});
13+
});

packages/e2e/test/web-extension/extension/background/cip30.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ import { walletName } from '../const';
99

1010
// this should come from remote api
1111
const confirmationCallback: walletCip30.CallbackConfirmation = {
12-
signData: async () => true,
13-
signTx: async () => true,
12+
signData: async ({ sender }) => {
13+
if (!sender) throw new Error('No sender context');
14+
logger.info('signData request from', sender);
15+
return true;
16+
},
17+
signTx: async ({ sender }) => {
18+
if (!sender) throw new Error('No sender context');
19+
logger.info('signTx request', sender);
20+
return true;
21+
},
1422
submitTx: async () => true
1523
};
1624

packages/e2e/test/web-extension/extension/background/requestAccess.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RemoteApiPropertyType, consumeRemoteApi } from '@cardano-sdk/web-extension';
2-
import { RequestAccess } from '@cardano-sdk/dapp-connector';
2+
import { RequestAccess, senderOrigin } from '@cardano-sdk/dapp-connector';
33
import { UserPromptService, logger } from '../util';
44
import { ensureUiIsOpenAndLoaded } from './windowManager';
55
import { runtime } from 'webextension-polyfill';
@@ -15,7 +15,9 @@ const userPromptService = consumeRemoteApi<UserPromptService>(
1515
{ logger, runtime }
1616
);
1717

18-
export const requestAccess: RequestAccess = async (origin) => {
18+
export const requestAccess: RequestAccess = async (sender) => {
19+
const origin = senderOrigin(sender);
20+
if (!origin) throw new Error('Invalid requestAccess request: unknown sender origin');
1921
await ensureUiIsOpenAndLoaded();
2022
return await userPromptService.allowOrigin(origin);
2123
};

packages/key-management/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"devDependencies": {
4141
"@types/lodash": "^4.14.182",
4242
"@types/pbkdf2": "^3.1.0",
43+
"@types/webextension-polyfill": "^0.8.0",
4344
"eslint": "^7.32.0",
4445
"jest": "^28.1.3",
4546
"madge": "^5.0.1",

packages/key-management/src/cip8/cip30signData.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Crypto from '@cardano-sdk/crypto';
2-
import { AccountKeyDerivationPath, GroupedAddress, KeyRole } from '../types';
2+
import { AccountKeyDerivationPath, GroupedAddress, KeyRole, MessageSender } from '../types';
33
import {
44
AlgorithmId,
55
CBORValue,
@@ -24,6 +24,7 @@ export interface Cip30SignDataRequest {
2424
witnesser: Bip32Ed25519Witnesser;
2525
signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID;
2626
payload: HexBlob;
27+
sender?: MessageSender;
2728
}
2829

2930
export enum Cip30DataSignErrorCode {
@@ -87,10 +88,11 @@ const createSigStructureHeaders = (addressBytes: Uint8Array) => {
8788
const signSigStructure = (
8889
witnesser: Bip32Ed25519Witnesser,
8990
derivationPath: AccountKeyDerivationPath,
90-
sigStructure: SigStructure
91+
sigStructure: SigStructure,
92+
sender?: MessageSender
9193
) => {
9294
try {
93-
return witnesser.signBlob(derivationPath, util.bytesToHex(sigStructure.to_bytes()));
95+
return witnesser.signBlob(derivationPath, util.bytesToHex(sigStructure.to_bytes()), sender);
9496
} catch (error) {
9597
throw new Cip30DataSignError(Cip30DataSignErrorCode.UserDeclined, 'Failed to sign', error);
9698
}
@@ -115,7 +117,8 @@ export const cip30signData = async ({
115117
knownAddresses,
116118
witnesser,
117119
signWith,
118-
payload
120+
payload,
121+
sender
119122
}: Cip30SignDataRequest): Promise<Cip30DataSignature> => {
120123
if (Cardano.DRepID.isValid(signWith) && !Cardano.DRepID.canSign(signWith)) {
121124
throw new Cip30DataSignError(Cip30DataSignErrorCode.AddressNotPK, 'Invalid address');
@@ -129,7 +132,7 @@ export const cip30signData = async ({
129132
false
130133
);
131134
const sigStructure = builder.make_data_to_sign();
132-
const { signature, publicKey } = await signSigStructure(witnesser, derivationPath, sigStructure);
135+
const { signature, publicKey } = await signSigStructure(witnesser, derivationPath, sigStructure, sender);
133136
const coseSign1 = builder.build(Buffer.from(signature, 'hex'));
134137

135138
const coseKey = createCoseKey(addressBytes, publicKey);

packages/key-management/src/types.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import * as Crypto from '@cardano-sdk/crypto';
22
import { Cardano } from '@cardano-sdk/core';
33
import { HexBlob, OpaqueString, Shutdown } from '@cardano-sdk/util';
44
import { Logger } from 'ts-log';
5+
import type { Runtime } from 'webextension-polyfill';
6+
7+
export type MessageSender = Runtime.MessageSender;
58

69
export interface SignBlobResult {
710
publicKey: Crypto.Ed25519PublicKeyHex;
@@ -141,6 +144,7 @@ export interface SignTransactionOptions {
141144
export interface SignTransactionContext {
142145
txInKeyPathMap: TxInKeyPathMap;
143146
knownAddresses: GroupedAddress[];
147+
sender?: MessageSender;
144148
}
145149

146150
export interface KeyAgent {
@@ -231,5 +235,5 @@ export interface Witnesser {
231235
/**
232236
* @throws AuthenticationError
233237
*/
234-
signBlob(derivationPath: AccountKeyDerivationPath, blob: HexBlob): Promise<SignBlobResult>;
238+
signBlob(derivationPath: AccountKeyDerivationPath, blob: HexBlob, sender?: MessageSender): Promise<SignBlobResult>;
235239
}

packages/key-management/src/util/createWitnesser.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
AccountKeyDerivationPath,
33
AsyncKeyAgent,
4+
MessageSender,
45
SignBlobResult,
56
SignTransactionContext,
67
WitnessOptions,
@@ -25,7 +26,11 @@ export class Bip32Ed25519Witnesser implements Witnesser {
2526
return { signatures: await this.#keyAgent.signTransaction(txInternals, context, options) };
2627
}
2728

28-
async signBlob(derivationPath: AccountKeyDerivationPath, blob: HexBlob): Promise<SignBlobResult> {
29+
async signBlob(
30+
derivationPath: AccountKeyDerivationPath,
31+
blob: HexBlob,
32+
_sender?: MessageSender
33+
): Promise<SignBlobResult> {
2934
return this.#keyAgent.signBlob(derivationPath, blob);
3035
}
3136
}

0 commit comments

Comments
 (0)