@prosopo/account 2.8.102 → 2.8.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$cjs.log +2 -5
- package/.turbo/turbo-build$colon$tsc.log +5 -17
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +37 -0
- package/LICENSE +201 -0
- package/package.json +75 -76
- package/src/custom.d.ts +0 -44
- package/src/extension/Extension.ts +0 -29
- package/src/extension/ExtensionWeb2.ts +0 -129
- package/src/extension/ExtensionWeb3.ts +0 -60
- package/src/index.ts +0 -16
- package/src/tests/ExtensionWeb2.unit.test.ts +0 -172
- package/src/tests/cryptoWorker.unit.test.ts +0 -146
- package/src/workers/CryptoWorkerManager.ts +0 -192
- package/src/workers/cryptoWorker.ts +0 -98
- package/src/workers/index.ts +0 -18
- package/tsconfig.cjs.json +0 -37
- package/tsconfig.json +0 -40
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.types.json +0 -9
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import Signer from "@polkadot/extension-base/page/Signer";
|
|
16
|
-
import type { InjectedAccount } from "@polkadot/extension-inject/types";
|
|
17
|
-
import type { InjectedExtension } from "@polkadot/extension-inject/types";
|
|
18
|
-
import { stringToU8a } from "@polkadot/util";
|
|
19
|
-
import { getFingerprint, prefetchFingerprint } from "@prosopo/fingerprint";
|
|
20
|
-
import { Keyring } from "@prosopo/keyring";
|
|
21
|
-
import type { KeyringPair } from "@prosopo/types";
|
|
22
|
-
import type { Account, ProcaptchaClientConfigOutput } from "@prosopo/types";
|
|
23
|
-
import { u8aToHex, version } from "@prosopo/util";
|
|
24
|
-
import { hexHash } from "@prosopo/util-crypto";
|
|
25
|
-
import type { KeypairType } from "@prosopo/util-crypto";
|
|
26
|
-
import { getCryptoWorkerManager } from "../workers/CryptoWorkerManager.js";
|
|
27
|
-
import { Extension } from "./Extension.js";
|
|
28
|
-
|
|
29
|
-
prefetchFingerprint();
|
|
30
|
-
getCryptoWorkerManager().prewarm();
|
|
31
|
-
|
|
32
|
-
const EntropyToMnemonicLoader = async () =>
|
|
33
|
-
(await import("@prosopo/util-crypto")).entropyToMnemonic;
|
|
34
|
-
|
|
35
|
-
type AccountWithKeyPair = InjectedAccount & { keypair: KeyringPair };
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Class for interfacing with web3 accounts.
|
|
39
|
-
*/
|
|
40
|
-
export class ExtensionWeb2 extends Extension {
|
|
41
|
-
public async getAccount(
|
|
42
|
-
config: ProcaptchaClientConfigOutput,
|
|
43
|
-
): Promise<Account> {
|
|
44
|
-
const account = await this.createAccount(config);
|
|
45
|
-
const extension: InjectedExtension = this.createExtension(account);
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
account,
|
|
49
|
-
extension,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private createExtension(account: AccountWithKeyPair): InjectedExtension {
|
|
54
|
-
const signer = new Signer(async () => {
|
|
55
|
-
return;
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// signing carried out by the keypair. Signs the data with the private key, creating a signature. Other people can verify this signature given the message and the public key, proving that the message was indeed signed by account and proving ownership of the account.
|
|
59
|
-
signer.signRaw = async (payload) => {
|
|
60
|
-
const signature = account.keypair.sign(payload.data);
|
|
61
|
-
return {
|
|
62
|
-
id: 1, // the id of the request to sign. This should be incremented each time and adjust the signature, but we're hacking around this. Hence the signature will always be the same given the same payload.
|
|
63
|
-
signature: u8aToHex(signature) as `0x${string}`,
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
accounts: {
|
|
69
|
-
get: async () => {
|
|
70
|
-
// there is only ever 1 account
|
|
71
|
-
return [account];
|
|
72
|
-
},
|
|
73
|
-
subscribe: () => {
|
|
74
|
-
// do nothing, there will never be account changes
|
|
75
|
-
return () => {
|
|
76
|
-
return;
|
|
77
|
-
};
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
name: "procaptcha-web2",
|
|
81
|
-
version,
|
|
82
|
-
signer,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private async createAccount(
|
|
87
|
-
config: ProcaptchaClientConfigOutput,
|
|
88
|
-
): Promise<AccountWithKeyPair> {
|
|
89
|
-
const browserEntropy = await getFingerprint();
|
|
90
|
-
|
|
91
|
-
// hash the entropy and remove 0x
|
|
92
|
-
const entropy = hexHash(browserEntropy, 128).slice(2);
|
|
93
|
-
|
|
94
|
-
const u8Entropy = stringToU8a(entropy);
|
|
95
|
-
|
|
96
|
-
const type: KeypairType = "sr25519";
|
|
97
|
-
const keyring = new Keyring({ type });
|
|
98
|
-
const workerManager = getCryptoWorkerManager();
|
|
99
|
-
|
|
100
|
-
// Two paths, in order of preference:
|
|
101
|
-
//
|
|
102
|
-
// 1. Worker path: entropy → mnemonic → sr25519 keypair in ONE fused
|
|
103
|
-
// worker round-trip. sr25519 derivation is a ~500ms scalar-mul
|
|
104
|
-
// on mid-tier hardware and was the biggest main-thread cost in
|
|
105
|
-
// the frictionless critical path pre-worker. Doing it in the
|
|
106
|
-
// worker lets it overlap with BotScoreWorker and main-thread
|
|
107
|
-
// detectors. We wrap the raw keypair bytes on main via
|
|
108
|
-
// `addFromPair` (cheap packaging — no ECC work).
|
|
109
|
-
//
|
|
110
|
-
// 2. Fallback path (worker refused to boot / CSP): synchronous
|
|
111
|
-
// derivation on main thread. Preserves behaviour for browsers
|
|
112
|
-
// that block workers.
|
|
113
|
-
try {
|
|
114
|
-
const { publicKey, secretKey } =
|
|
115
|
-
await workerManager.entropyToKeypair(u8Entropy);
|
|
116
|
-
const keypair = keyring.addFromPair({ publicKey, secretKey }, {}, type);
|
|
117
|
-
const address = keypair.address;
|
|
118
|
-
return { address, name: address, keypair };
|
|
119
|
-
} catch (workerError) {
|
|
120
|
-
const entropyToMnemonic = await EntropyToMnemonicLoader();
|
|
121
|
-
const mnemonic = entropyToMnemonic(u8Entropy);
|
|
122
|
-
const keypair = keyring.addFromMnemonic(mnemonic);
|
|
123
|
-
const address = keypair.address;
|
|
124
|
-
return { address, name: address, keypair };
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export default ExtensionWeb2;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import { web3Enable } from "@polkadot/extension-dapp";
|
|
16
|
-
import type { InjectedExtension } from "@polkadot/extension-inject/types";
|
|
17
|
-
import { cryptoWaitReady } from "@polkadot/util-crypto";
|
|
18
|
-
import { ProsopoError } from "@prosopo/common";
|
|
19
|
-
import type { Account, ProcaptchaClientConfigOutput } from "@prosopo/types";
|
|
20
|
-
import { Extension } from "./Extension.js";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Class for interfacing with web3 accounts.
|
|
24
|
-
*/
|
|
25
|
-
export class ExtensionWeb3 extends Extension {
|
|
26
|
-
public async getAccount(
|
|
27
|
-
config: ProcaptchaClientConfigOutput,
|
|
28
|
-
): Promise<Account> {
|
|
29
|
-
const { dappName, userAccountAddress: address } = config;
|
|
30
|
-
|
|
31
|
-
if (!address) {
|
|
32
|
-
throw new ProsopoError("WIDGET.NO_ACCOUNTS_FOUND", {
|
|
33
|
-
context: { error: "No account address provided" },
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
await cryptoWaitReady();
|
|
38
|
-
|
|
39
|
-
// enable access to all extensions
|
|
40
|
-
const extensions: InjectedExtension[] = await web3Enable(dappName);
|
|
41
|
-
if (extensions.length === 0) {
|
|
42
|
-
throw new ProsopoError("WIDGET.NO_EXTENSION_FOUND");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// search through all extensions for the one that has the account
|
|
46
|
-
for (const extension of extensions) {
|
|
47
|
-
const accounts = await extension.accounts.get();
|
|
48
|
-
const account = accounts.find((account) => account.address === address);
|
|
49
|
-
if (account) {
|
|
50
|
-
return { account, extension };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
throw new ProsopoError("WIDGET.ACCOUNT_NOT_FOUND", {
|
|
55
|
-
context: { error: `No account found matching ${address}` },
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export default ExtensionWeb3;
|
package/src/index.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
export * from "./extension/Extension.js";
|
|
15
|
-
export * from "./extension/ExtensionWeb2.js";
|
|
16
|
-
export * from "./extension/ExtensionWeb3.js";
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
// Tests for ExtensionWeb2.createAccount. The interesting behaviour to lock
|
|
16
|
-
// down is the two-branch account-derivation path:
|
|
17
|
-
//
|
|
18
|
-
// 1. Worker branch (preferred): `workerManager.entropyToKeypair(...)` →
|
|
19
|
-
// `keyring.addFromPair({publicKey, secretKey})` — the sr25519 scalar-
|
|
20
|
-
// mul runs off the main thread, main thread just packages the bytes.
|
|
21
|
-
// 2. Fallback branch: worker throws (CSP block, task timeout, etc.) →
|
|
22
|
-
// dynamic-import `entropyToMnemonic` → `keyring.addFromMnemonic(...)`
|
|
23
|
-
// on the main thread.
|
|
24
|
-
//
|
|
25
|
-
// Both branches must produce the same account address for a given fingerprint
|
|
26
|
-
// or session identity breaks between the two paths — a repeat visitor whose
|
|
27
|
-
// worker fails would look like a different user to the provider.
|
|
28
|
-
|
|
29
|
-
import { stringToU8a } from "@polkadot/util";
|
|
30
|
-
import { getFingerprint } from "@prosopo/fingerprint";
|
|
31
|
-
import { Keyring } from "@prosopo/keyring";
|
|
32
|
-
import { entropyToMnemonic, hexHash } from "@prosopo/util-crypto";
|
|
33
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
34
|
-
|
|
35
|
-
// vi.hoisted() makes these accessible from within vi.mock factories, which
|
|
36
|
-
// run before the module's own imports at collection time.
|
|
37
|
-
const mocks = vi.hoisted(() => {
|
|
38
|
-
const entropyToKeypair = vi.fn();
|
|
39
|
-
return {
|
|
40
|
-
entropyToKeypair,
|
|
41
|
-
getCryptoWorkerManager: vi.fn(() => ({
|
|
42
|
-
prewarm: vi.fn(),
|
|
43
|
-
entropyToKeypair,
|
|
44
|
-
entropyToMnemonic: vi.fn(),
|
|
45
|
-
})),
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
vi.mock("@prosopo/fingerprint", async (importOriginal) => {
|
|
50
|
-
const actual =
|
|
51
|
-
(await importOriginal()) as typeof import("@prosopo/fingerprint");
|
|
52
|
-
return {
|
|
53
|
-
...actual,
|
|
54
|
-
getFingerprint: vi.fn(),
|
|
55
|
-
prefetchFingerprint: vi.fn(),
|
|
56
|
-
};
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
vi.mock("../workers/CryptoWorkerManager.js", () => ({
|
|
60
|
-
getCryptoWorkerManager: mocks.getCryptoWorkerManager,
|
|
61
|
-
// Types re-exported at runtime aren't needed for the tests but keep the
|
|
62
|
-
// mock shape aligned with the module's actual named exports.
|
|
63
|
-
CryptoWorkerManager: class {},
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
|
-
// Imported after mocks so the module picks up the mocked getFingerprint and
|
|
67
|
-
// worker manager during its top-level side effects (prefetchFingerprint,
|
|
68
|
-
// getCryptoWorkerManager().prewarm()).
|
|
69
|
-
const { default: ExtensionWeb2 } = await import(
|
|
70
|
-
"../extension/ExtensionWeb2.js"
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
// Fixed browser-fingerprint hex the mocked getFingerprint always returns.
|
|
74
|
-
// The real code hashes this to 128 bits, hex-encodes it, and passes the ASCII
|
|
75
|
-
// hex bytes as entropy to the mnemonic derivation — so both branches under
|
|
76
|
-
// test see the same input regardless of the underlying fingerprint.
|
|
77
|
-
const FIXED_FINGERPRINT_HEX = "0x1234567890abcdef1234567890abcdef";
|
|
78
|
-
|
|
79
|
-
// Config placeholder — ExtensionWeb2.createAccount ignores fields other than
|
|
80
|
-
// what the fingerprint hash + keyring type need.
|
|
81
|
-
const config = {
|
|
82
|
-
account: { address: "" },
|
|
83
|
-
web2: true,
|
|
84
|
-
defaultEnvironment: "test",
|
|
85
|
-
} as unknown as Parameters<InstanceType<typeof ExtensionWeb2>["getAccount"]>[0];
|
|
86
|
-
|
|
87
|
-
describe("ExtensionWeb2.createAccount", () => {
|
|
88
|
-
beforeEach(() => {
|
|
89
|
-
vi.mocked(getFingerprint).mockReset();
|
|
90
|
-
mocks.entropyToKeypair.mockReset();
|
|
91
|
-
vi.mocked(getFingerprint).mockResolvedValue(FIXED_FINGERPRINT_HEX);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("worker branch: derives account from worker-returned keypair bytes", async () => {
|
|
95
|
-
// Compute the expected keypair the same way ExtensionWeb2 would — we
|
|
96
|
-
// need to feed the mock the exact bytes the worker would produce for
|
|
97
|
-
// this fingerprint, otherwise the reconstructed address won't match.
|
|
98
|
-
const entropyHex = hexHash(FIXED_FINGERPRINT_HEX, 128).slice(2);
|
|
99
|
-
const u8Entropy = stringToU8a(entropyHex);
|
|
100
|
-
const mnemonic = entropyToMnemonic(u8Entropy);
|
|
101
|
-
const keyring = new Keyring({ type: "sr25519" });
|
|
102
|
-
const expectedPair = keyring.addFromMnemonic(mnemonic);
|
|
103
|
-
|
|
104
|
-
mocks.entropyToKeypair.mockResolvedValue({
|
|
105
|
-
publicKey: expectedPair.publicKey,
|
|
106
|
-
secretKey:
|
|
107
|
-
// Fish the secret out of the reference pair — this is what the
|
|
108
|
-
// real CryptoWorker returns from sr25519FromSeed(seed).
|
|
109
|
-
(expectedPair as unknown as { secretKey: Uint8Array }).secretKey ??
|
|
110
|
-
new Uint8Array(64),
|
|
111
|
-
mnemonic,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const ext = new ExtensionWeb2();
|
|
115
|
-
const { account } = await ext.getAccount(config);
|
|
116
|
-
|
|
117
|
-
expect(mocks.entropyToKeypair).toHaveBeenCalledTimes(1);
|
|
118
|
-
// The worker got called with the entropy ExtensionWeb2 hashed from the
|
|
119
|
-
// fingerprint — verifies the argument-shaping code isn't broken.
|
|
120
|
-
const [entropyArg] = mocks.entropyToKeypair.mock.calls[0] ?? [];
|
|
121
|
-
expect(entropyArg).toBeInstanceOf(Uint8Array);
|
|
122
|
-
expect(entropyArg).toEqual(u8Entropy);
|
|
123
|
-
// Account address matches the addFromMnemonic-derived one — proves the
|
|
124
|
-
// addFromPair wrapping preserves account identity.
|
|
125
|
-
expect(account.address).toBe(expectedPair.address);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("fallback branch: derives account on main thread when worker throws", async () => {
|
|
129
|
-
// Simulate a worker failure — CSP block, worker script parse failure,
|
|
130
|
-
// task timeout — anything that surfaces as a rejection from
|
|
131
|
-
// entropyToKeypair. Fallback path must reach the same account address.
|
|
132
|
-
mocks.entropyToKeypair.mockRejectedValue(new Error("Worker unavailable"));
|
|
133
|
-
|
|
134
|
-
const entropyHex = hexHash(FIXED_FINGERPRINT_HEX, 128).slice(2);
|
|
135
|
-
const u8Entropy = stringToU8a(entropyHex);
|
|
136
|
-
const mnemonic = entropyToMnemonic(u8Entropy);
|
|
137
|
-
const keyring = new Keyring({ type: "sr25519" });
|
|
138
|
-
const expectedPair = keyring.addFromMnemonic(mnemonic);
|
|
139
|
-
|
|
140
|
-
const ext = new ExtensionWeb2();
|
|
141
|
-
const { account } = await ext.getAccount(config);
|
|
142
|
-
|
|
143
|
-
expect(mocks.entropyToKeypair).toHaveBeenCalledTimes(1);
|
|
144
|
-
// Fallback still gives us a working keypair with the same address —
|
|
145
|
-
// this is the invariant that lets us safely swap in the worker path.
|
|
146
|
-
expect(account.address).toBe(expectedPair.address);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("worker and fallback branches derive the same account address", async () => {
|
|
150
|
-
// Direct A/B: one call goes through the worker mock, the next through
|
|
151
|
-
// the fallback. Assert the addresses match.
|
|
152
|
-
const entropyHex = hexHash(FIXED_FINGERPRINT_HEX, 128).slice(2);
|
|
153
|
-
const u8Entropy = stringToU8a(entropyHex);
|
|
154
|
-
const mnemonic = entropyToMnemonic(u8Entropy);
|
|
155
|
-
const refKeyring = new Keyring({ type: "sr25519" });
|
|
156
|
-
const refPair = refKeyring.addFromMnemonic(mnemonic);
|
|
157
|
-
|
|
158
|
-
mocks.entropyToKeypair.mockResolvedValueOnce({
|
|
159
|
-
publicKey: refPair.publicKey,
|
|
160
|
-
secretKey:
|
|
161
|
-
(refPair as unknown as { secretKey: Uint8Array }).secretKey ??
|
|
162
|
-
new Uint8Array(64),
|
|
163
|
-
mnemonic,
|
|
164
|
-
});
|
|
165
|
-
const workerAccount = await new ExtensionWeb2().getAccount(config);
|
|
166
|
-
|
|
167
|
-
mocks.entropyToKeypair.mockRejectedValueOnce(new Error("worker down"));
|
|
168
|
-
const fallbackAccount = await new ExtensionWeb2().getAccount(config);
|
|
169
|
-
|
|
170
|
-
expect(workerAccount.account.address).toBe(fallbackAccount.account.address);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
// Unit tests for the CryptoWorker task implementations. The worker itself
|
|
16
|
-
// bootstraps under `?worker&inline` in the browser build and can't easily be
|
|
17
|
-
// spun up in a Node vitest env, so these tests exercise the same primitives
|
|
18
|
-
// the worker's message handler dispatches to (entropyToMnemonic,
|
|
19
|
-
// mnemonicToMiniSecret, sr25519FromSeed) and prove they produce a keypair
|
|
20
|
-
// equivalent to `keyring.addFromMnemonic` — the shipped-blob invariant that
|
|
21
|
-
// `ExtensionWeb2.createAccount`'s worker path relies on.
|
|
22
|
-
|
|
23
|
-
import { stringToU8a } from "@polkadot/util";
|
|
24
|
-
import { Keyring } from "@prosopo/keyring";
|
|
25
|
-
import {
|
|
26
|
-
entropyToMnemonic,
|
|
27
|
-
mnemonicToMiniSecret,
|
|
28
|
-
sr25519FromSeed,
|
|
29
|
-
} from "@prosopo/util-crypto";
|
|
30
|
-
import { describe, expect, it } from "vitest";
|
|
31
|
-
|
|
32
|
-
// Mirrors ExtensionWeb2.createAccount: browserEntropy → hexHash(., 128) →
|
|
33
|
-
// slice(2) → 32 ASCII-hex characters → stringToU8a → 32 bytes. Fixed value
|
|
34
|
-
// so tests are deterministic (real code path uses the browser fingerprint).
|
|
35
|
-
const FIXED_ENTROPY_HEX = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6";
|
|
36
|
-
|
|
37
|
-
const entropyBytes = (hex: string): Uint8Array => stringToU8a(hex);
|
|
38
|
-
|
|
39
|
-
describe("CryptoWorker task primitives", () => {
|
|
40
|
-
describe("entropyToKeypair pipeline", () => {
|
|
41
|
-
it("derives an sr25519 keypair from entropy deterministically", () => {
|
|
42
|
-
const entropy = entropyBytes(FIXED_ENTROPY_HEX);
|
|
43
|
-
|
|
44
|
-
const mnemonic = entropyToMnemonic(entropy);
|
|
45
|
-
const seed = mnemonicToMiniSecret(mnemonic);
|
|
46
|
-
const { publicKey, secretKey } = sr25519FromSeed(seed);
|
|
47
|
-
|
|
48
|
-
expect(publicKey).toBeInstanceOf(Uint8Array);
|
|
49
|
-
expect(secretKey).toBeInstanceOf(Uint8Array);
|
|
50
|
-
expect(publicKey.length).toBe(32);
|
|
51
|
-
expect(secretKey.length).toBe(64);
|
|
52
|
-
|
|
53
|
-
// Re-running with the same entropy must yield the same keys —
|
|
54
|
-
// otherwise the CryptoWorker cache would be poisoned by a
|
|
55
|
-
// non-deterministic derivation.
|
|
56
|
-
const again = sr25519FromSeed(
|
|
57
|
-
mnemonicToMiniSecret(entropyToMnemonic(entropy)),
|
|
58
|
-
);
|
|
59
|
-
expect(again.publicKey).toEqual(publicKey);
|
|
60
|
-
expect(again.secretKey).toEqual(secretKey);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("produces the same address as keyring.addFromMnemonic (worker/main-thread parity)", () => {
|
|
64
|
-
const entropy = entropyBytes(FIXED_ENTROPY_HEX);
|
|
65
|
-
const mnemonic = entropyToMnemonic(entropy);
|
|
66
|
-
|
|
67
|
-
// Worker path: entropy → mnemonic → miniSecret → sr25519 → addFromPair
|
|
68
|
-
const seed = mnemonicToMiniSecret(mnemonic);
|
|
69
|
-
const { publicKey, secretKey } = sr25519FromSeed(seed);
|
|
70
|
-
const workerKeyring = new Keyring({ type: "sr25519" });
|
|
71
|
-
const workerPair = workerKeyring.addFromPair(
|
|
72
|
-
{ publicKey, secretKey },
|
|
73
|
-
{},
|
|
74
|
-
"sr25519",
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Legacy main-thread path: keyring.addFromMnemonic
|
|
78
|
-
const mainKeyring = new Keyring({ type: "sr25519" });
|
|
79
|
-
const mainPair = mainKeyring.addFromMnemonic(mnemonic);
|
|
80
|
-
|
|
81
|
-
// Addresses must match exactly — this is the invariant that guarantees
|
|
82
|
-
// swapping addFromMnemonic → addFromPair(entropyToKeypair(...)) is a
|
|
83
|
-
// behavioural no-op for account identity.
|
|
84
|
-
expect(workerPair.address).toBe(mainPair.address);
|
|
85
|
-
expect(workerPair.publicKey).toEqual(mainPair.publicKey);
|
|
86
|
-
expect(workerPair.type).toBe(mainPair.type);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("signatures produced by addFromPair verify against the addFromMnemonic public key", () => {
|
|
90
|
-
const entropy = entropyBytes(FIXED_ENTROPY_HEX);
|
|
91
|
-
const mnemonic = entropyToMnemonic(entropy);
|
|
92
|
-
const seed = mnemonicToMiniSecret(mnemonic);
|
|
93
|
-
const { publicKey, secretKey } = sr25519FromSeed(seed);
|
|
94
|
-
|
|
95
|
-
const workerKeyring = new Keyring({ type: "sr25519" });
|
|
96
|
-
const workerPair = workerKeyring.addFromPair(
|
|
97
|
-
{ publicKey, secretKey },
|
|
98
|
-
{},
|
|
99
|
-
"sr25519",
|
|
100
|
-
);
|
|
101
|
-
const mainKeyring = new Keyring({ type: "sr25519" });
|
|
102
|
-
const mainPair = mainKeyring.addFromMnemonic(mnemonic);
|
|
103
|
-
|
|
104
|
-
const message = stringToU8a("prosopo-test-message");
|
|
105
|
-
const workerSig = workerPair.sign(message);
|
|
106
|
-
const mainSig = mainPair.sign(message);
|
|
107
|
-
|
|
108
|
-
expect(workerSig.length).toBe(64);
|
|
109
|
-
expect(mainSig.length).toBe(64);
|
|
110
|
-
// sr25519 signatures are non-deterministic, so we can't byte-compare;
|
|
111
|
-
// but each signature must verify against BOTH pairs' public keys.
|
|
112
|
-
expect(mainPair.verify(message, workerSig, mainPair.publicKey)).toBe(
|
|
113
|
-
true,
|
|
114
|
-
);
|
|
115
|
-
expect(workerPair.verify(message, mainSig, workerPair.publicKey)).toBe(
|
|
116
|
-
true,
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe("entropyToMnemonic (existing worker task, unchanged)", () => {
|
|
122
|
-
it("produces a 24-word BIP39 mnemonic from 32 bytes of entropy", () => {
|
|
123
|
-
const entropy = entropyBytes(FIXED_ENTROPY_HEX);
|
|
124
|
-
const mnemonic = entropyToMnemonic(entropy);
|
|
125
|
-
const words = mnemonic.split(" ");
|
|
126
|
-
// 32 bytes of entropy = 256 bits → 24 BIP39 words.
|
|
127
|
-
expect(words.length).toBe(24);
|
|
128
|
-
for (const w of words) {
|
|
129
|
-
expect(w).toMatch(/^[a-z]+$/);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("is deterministic for a given entropy", () => {
|
|
134
|
-
const entropy = entropyBytes(FIXED_ENTROPY_HEX);
|
|
135
|
-
expect(entropyToMnemonic(entropy)).toBe(entropyToMnemonic(entropy));
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("rejects entropy that isn't 16/20/24/28/32 bytes", () => {
|
|
139
|
-
// bip39 accepts only these sizes — CryptoWorker relies on the caller
|
|
140
|
-
// passing valid entropy (already-hashed to 32 bytes in ExtensionWeb2).
|
|
141
|
-
expect(() => entropyToMnemonic(new Uint8Array(15))).toThrow();
|
|
142
|
-
expect(() => entropyToMnemonic(new Uint8Array(17))).toThrow();
|
|
143
|
-
expect(() => entropyToMnemonic(new Uint8Array(33))).toThrow();
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
});
|