@zerc20/storage-sdk 0.0.0-stage → 0.1.0
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/LICENSE +24 -0
- package/README.md +14 -2
- package/dist/candid.d.ts +73 -0
- package/dist/candid.js +51 -0
- package/dist/candid.js.map +1 -0
- package/dist/canister.d.ts +33 -0
- package/dist/canister.js +76 -0
- package/dist/canister.js.map +1 -0
- package/dist/crypto.d.ts +27 -0
- package/dist/crypto.js +99 -0
- package/dist/crypto.js.map +1 -0
- package/dist/formats.d.ts +202 -0
- package/dist/formats.js +238 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/powWorker.d.ts +1 -0
- package/dist/powWorker.js +4 -0
- package/dist/powWorker.js.map +1 -0
- package/dist/send.d.ts +51 -0
- package/dist/send.js +162 -0
- package/dist/send.js.map +1 -0
- package/dist/sync.d.ts +82 -0
- package/dist/sync.js +263 -0
- package/dist/sync.js.map +1 -0
- package/dist/viewKey.d.ts +45 -0
- package/dist/viewKey.js +104 -0
- package/dist/viewKey.js.map +1 -0
- package/dist/wire.d.ts +33 -0
- package/dist/wire.js +112 -0
- package/dist/wire.js.map +1 -0
- package/package.json +51 -4
- package/src/candid.ts +116 -0
- package/src/canister.ts +85 -0
- package/src/crypto.ts +113 -0
- package/src/formats.ts +399 -0
- package/src/index.ts +86 -0
- package/src/powWorker.ts +6 -0
- package/src/send.ts +224 -0
- package/src/sync.ts +376 -0
- package/src/viewKey.ts +152 -0
- package/src/wire.ts +136 -0
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerc20/storage-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for zerc20-storage: private note delivery for zERC20 on the Internet Computer.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Unlicense",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/kbizikav/zerc20-v2-new.git",
|
|
10
|
+
"directory": "storage/sdk"
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"prepack": "npm run build",
|
|
27
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
28
|
+
"test": "vitest run --config vitest.config.ts",
|
|
29
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
30
|
+
"demo": "node demo/serve.ts"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@dfinity/vetkeys": "~0.4.0",
|
|
34
|
+
"@icp-sdk/core": "^6.1.1",
|
|
35
|
+
"@noble/ciphers": "^2.3.0",
|
|
36
|
+
"@noble/curves": "^2.3.0",
|
|
37
|
+
"@noble/hashes": "^2.3.0",
|
|
38
|
+
"@scure/base": "^2.4.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@dfinity/pic": "0.25.0",
|
|
42
|
+
"@types/node": "24.19.2",
|
|
43
|
+
"typescript": "6.0.3",
|
|
44
|
+
"vite": "8.3.4",
|
|
45
|
+
"vitest": "4.1.11"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.12"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/candid.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The zerc20_storage Candid interface (canister/zerc20_storage.did), by hand: generated bindings
|
|
2
|
+
// target the older @dfinity/* packages.
|
|
3
|
+
|
|
4
|
+
import type { ActorMethod } from '@icp-sdk/core/agent';
|
|
5
|
+
import type { IDL } from '@icp-sdk/core/candid';
|
|
6
|
+
|
|
7
|
+
export type KindName = 'Outgoing' | 'Lane';
|
|
8
|
+
export type Kind = { [K in KindName]: { [P in K]: null } }[KindName];
|
|
9
|
+
|
|
10
|
+
export type Action =
|
|
11
|
+
| {
|
|
12
|
+
OpenChannel: {
|
|
13
|
+
inbox: Uint8Array;
|
|
14
|
+
enc_announcement: Uint8Array;
|
|
15
|
+
outgoing_id: Uint8Array;
|
|
16
|
+
enc_outgoing: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
| { Put: { kind: Kind; id: Uint8Array; data: Uint8Array } };
|
|
20
|
+
|
|
21
|
+
export interface AuditRequest {
|
|
22
|
+
target: Uint8Array;
|
|
23
|
+
notify_key: Uint8Array;
|
|
24
|
+
transport_public_key: Uint8Array;
|
|
25
|
+
expiry_ns: bigint;
|
|
26
|
+
signature: Uint8Array;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Registration {
|
|
30
|
+
address: Uint8Array;
|
|
31
|
+
public_key: Uint8Array;
|
|
32
|
+
notify_key: Uint8Array;
|
|
33
|
+
escrow: Uint8Array;
|
|
34
|
+
expiry_ns: bigint;
|
|
35
|
+
signature: Uint8Array;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Slot {
|
|
39
|
+
data: Uint8Array;
|
|
40
|
+
at_ns: bigint;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AuditRecord {
|
|
44
|
+
auditor: Uint8Array;
|
|
45
|
+
target: Uint8Array;
|
|
46
|
+
notify_key: Uint8Array;
|
|
47
|
+
transport_public_key: Uint8Array;
|
|
48
|
+
expiry_ns: bigint;
|
|
49
|
+
signature: Uint8Array;
|
|
50
|
+
at_ns: bigint;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type CanisterError = { Occupied: Uint8Array } | { Rejected: string };
|
|
54
|
+
export type Result<T> = { Ok: T } | { Err: CanisterError };
|
|
55
|
+
|
|
56
|
+
export interface Service {
|
|
57
|
+
escrow_public_key: ActorMethod<[], Result<Uint8Array>>;
|
|
58
|
+
register: ActorMethod<[Registration, bigint], Result<null>>;
|
|
59
|
+
registration: ActorMethod<[Uint8Array, [] | [Uint8Array]], [] | [Registration]>;
|
|
60
|
+
audit_escrow_key: ActorMethod<[AuditRequest], Result<Uint8Array>>;
|
|
61
|
+
get_audits: ActorMethod<[bigint, number], AuditRecord[]>;
|
|
62
|
+
submit: ActorMethod<[Action[], bigint], Result<null>>;
|
|
63
|
+
get_announcements: ActorMethod<[Uint8Array, number, number], Uint8Array[]>;
|
|
64
|
+
get_slots: ActorMethod<[Kind, Uint8Array[]], ([] | [Slot])[]>;
|
|
65
|
+
get_log: ActorMethod<[bigint, number], Uint8Array[]>;
|
|
66
|
+
log_length: ActorMethod<[], bigint>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const idlFactory: IDL.InterfaceFactory = ({ IDL }) => {
|
|
70
|
+
const Blob = IDL.Vec(IDL.Nat8);
|
|
71
|
+
const Error = IDL.Variant({ Occupied: Blob, Rejected: IDL.Text });
|
|
72
|
+
const BlobResult = IDL.Variant({ Ok: Blob, Err: Error });
|
|
73
|
+
const UnitResult = IDL.Variant({ Ok: IDL.Null, Err: Error });
|
|
74
|
+
const Kind = IDL.Variant({ Outgoing: IDL.Null, Lane: IDL.Null });
|
|
75
|
+
const Action = IDL.Variant({
|
|
76
|
+
OpenChannel: IDL.Record({ inbox: Blob, enc_announcement: Blob, outgoing_id: Blob, enc_outgoing: Blob }),
|
|
77
|
+
Put: IDL.Record({ kind: Kind, id: Blob, data: Blob }),
|
|
78
|
+
});
|
|
79
|
+
const AuditRequest = IDL.Record({
|
|
80
|
+
target: Blob,
|
|
81
|
+
notify_key: Blob,
|
|
82
|
+
transport_public_key: Blob,
|
|
83
|
+
expiry_ns: IDL.Nat64,
|
|
84
|
+
signature: Blob,
|
|
85
|
+
});
|
|
86
|
+
const Registration = IDL.Record({
|
|
87
|
+
address: Blob,
|
|
88
|
+
public_key: Blob,
|
|
89
|
+
notify_key: Blob,
|
|
90
|
+
escrow: Blob,
|
|
91
|
+
expiry_ns: IDL.Nat64,
|
|
92
|
+
signature: Blob,
|
|
93
|
+
});
|
|
94
|
+
const Slot = IDL.Record({ data: Blob, at_ns: IDL.Nat64 });
|
|
95
|
+
const AuditRecord = IDL.Record({
|
|
96
|
+
auditor: Blob,
|
|
97
|
+
target: Blob,
|
|
98
|
+
notify_key: Blob,
|
|
99
|
+
transport_public_key: Blob,
|
|
100
|
+
expiry_ns: IDL.Nat64,
|
|
101
|
+
signature: Blob,
|
|
102
|
+
at_ns: IDL.Nat64,
|
|
103
|
+
});
|
|
104
|
+
return IDL.Service({
|
|
105
|
+
escrow_public_key: IDL.Func([], [BlobResult], []),
|
|
106
|
+
register: IDL.Func([Registration, IDL.Nat64], [UnitResult], []),
|
|
107
|
+
registration: IDL.Func([Blob, IDL.Opt(Blob)], [IDL.Opt(Registration)], ['query']),
|
|
108
|
+
audit_escrow_key: IDL.Func([AuditRequest], [BlobResult], []),
|
|
109
|
+
get_audits: IDL.Func([IDL.Nat64, IDL.Nat32], [IDL.Vec(AuditRecord)], ['query']),
|
|
110
|
+
submit: IDL.Func([IDL.Vec(Action), IDL.Nat64], [UnitResult], []),
|
|
111
|
+
get_announcements: IDL.Func([Blob, IDL.Nat32, IDL.Nat32], [IDL.Vec(Blob)], ['query']),
|
|
112
|
+
get_slots: IDL.Func([Kind, IDL.Vec(Blob)], [IDL.Vec(IDL.Opt(Slot))], ['query']),
|
|
113
|
+
get_log: IDL.Func([IDL.Nat64, IDL.Nat32], [IDL.Vec(Blob)], ['query']),
|
|
114
|
+
log_length: IDL.Func([], [IDL.Nat64], ['query']),
|
|
115
|
+
});
|
|
116
|
+
};
|
package/src/canister.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// A zerc20_storage canister, called anonymously.
|
|
2
|
+
|
|
3
|
+
import { Actor, type ActorSubclass, type Agent, HttpAgent } from '@icp-sdk/core/agent';
|
|
4
|
+
import { Principal } from '@icp-sdk/core/principal';
|
|
5
|
+
import { DerivedPublicKey } from '@dfinity/vetkeys';
|
|
6
|
+
import { idlFactory, type Action, type CanisterError, type Registration, type Result, type Service } from './candid.js';
|
|
7
|
+
import { hex } from './crypto.js';
|
|
8
|
+
import { REGISTER_POW_BITS, actionsDigest, registerDigest, requiredPowBits, solvePow } from './wire.js';
|
|
9
|
+
|
|
10
|
+
/** A definite answer from the canister: `Occupied` (the slot holds other data) or `Rejected`. */
|
|
11
|
+
export class StorageError extends Error {
|
|
12
|
+
constructor(readonly error: CanisterError) {
|
|
13
|
+
super('Occupied' in error ? `occupied: 0x${hex(error.Occupied)}` : `rejected: ${error.Rejected}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get occupied(): boolean {
|
|
17
|
+
return 'Occupied' in this.error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Each request gives up after this, so that one that hangs does not hold its caller forever. A call cut off may still
|
|
22
|
+
* have landed, as one whose connection drops may. */
|
|
23
|
+
const TIMEOUT_MS = 30_000;
|
|
24
|
+
|
|
25
|
+
const fetchWithTimeout: typeof fetch = (input, init) => {
|
|
26
|
+
const timeout = AbortSignal.timeout(TIMEOUT_MS);
|
|
27
|
+
return fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function unwrap<T>(result: Result<T>): T {
|
|
31
|
+
if ('Err' in result) throw new StorageError(result.Err);
|
|
32
|
+
return result.Ok;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class CanisterClient {
|
|
36
|
+
readonly canisterId: Principal;
|
|
37
|
+
/** Queries answered by a single replica: cheap, but only good for spotting news. */
|
|
38
|
+
readonly actor: ActorSubclass<Service>;
|
|
39
|
+
/** Every method as an update call, so the subnet agrees on the answer. */
|
|
40
|
+
readonly replicated: ActorSubclass<Service>;
|
|
41
|
+
#escrowPublicKey?: Promise<DerivedPublicKey>;
|
|
42
|
+
|
|
43
|
+
/** A client over HTTP. A local replica (an `http://` host on this machine) has its root key fetched; the IC's is
|
|
44
|
+
* built in. A root key fetched from anywhere else would let whoever is on the path forge every certified answer. */
|
|
45
|
+
static async connect(host: string, canisterId: Principal | string): Promise<CanisterClient> {
|
|
46
|
+
const url = new URL(host);
|
|
47
|
+
const local = url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
48
|
+
const agent = await HttpAgent.create({ host, shouldFetchRootKey: local, fetch: fetchWithTimeout });
|
|
49
|
+
return new CanisterClient(agent, canisterId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
constructor(agent: Agent, canisterId: Principal | string) {
|
|
53
|
+
this.canisterId = Principal.from(canisterId);
|
|
54
|
+
const config = { agent, canisterId: this.canisterId };
|
|
55
|
+
this.actor = Actor.createActor<Service>(idlFactory, config);
|
|
56
|
+
this.replicated = Actor.createActor<Service>(idlFactory, { ...config, queryStrategy: 'update' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The key owners IBE-encrypt their escrows with. Fixed for the canister's lifetime. */
|
|
60
|
+
escrowPublicKey(): Promise<DerivedPublicKey> {
|
|
61
|
+
this.#escrowPublicKey ??= this.actor.escrow_public_key().then((r) => DerivedPublicKey.deserialize(unwrap(r)));
|
|
62
|
+
this.#escrowPublicKey.catch(() => (this.#escrowPublicKey = undefined));
|
|
63
|
+
return this.#escrowPublicKey;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `address`'s registration with `notifyKey`, by default its current one: a query, which one replica answers.
|
|
67
|
+
* Unverified: check it with `verifyRegistration`. Call `replicated.registration` where a stale answer matters. */
|
|
68
|
+
async registration(address: Uint8Array, notifyKey?: Uint8Array): Promise<Registration | null> {
|
|
69
|
+
const r = await this.actor.registration(address, notifyKey ? [notifyKey] : []);
|
|
70
|
+
return r[0] ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Solves the proof of work and registers. An identical registration that is already stored is a
|
|
74
|
+
* no-op, so a call whose outcome is unknown can be repeated. */
|
|
75
|
+
async register(registration: Registration): Promise<void> {
|
|
76
|
+
const nonce = await solvePow(registerDigest(registration), REGISTER_POW_BITS);
|
|
77
|
+
unwrap(await this.actor.register(registration, nonce));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Solves the proof of work and submits the batch. Throws `StorageError` on a definite failure. */
|
|
81
|
+
async submit(actions: Action[]): Promise<void> {
|
|
82
|
+
const nonce = await solvePow(actionsDigest(actions), requiredPowBits(actions));
|
|
83
|
+
unwrap(await this.actor.submit(actions, nonce));
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Primitives: HKDF-SHA256, ChaCha20-Poly1305 with random nonces, X25519 ECIES, and byte helpers.
|
|
2
|
+
|
|
3
|
+
import { chacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
4
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
5
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
6
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
7
|
+
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
|
|
8
|
+
|
|
9
|
+
export { bytesToHex as hex, hexToBytes as unhex, randomBytes };
|
|
10
|
+
|
|
11
|
+
export const utf8 = (text: string): Uint8Array => new TextEncoder().encode(text);
|
|
12
|
+
|
|
13
|
+
export function concat(...parts: Uint8Array[]): Uint8Array {
|
|
14
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
15
|
+
let at = 0;
|
|
16
|
+
for (const part of parts) {
|
|
17
|
+
out.set(part, at);
|
|
18
|
+
at += part.length;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function u32be(n: number): Uint8Array {
|
|
24
|
+
const out = new Uint8Array(4);
|
|
25
|
+
new DataView(out.buffer).setUint32(0, n, false);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function u64be(n: bigint): Uint8Array {
|
|
30
|
+
const out = new Uint8Array(8);
|
|
31
|
+
new DataView(out.buffer).setBigUint64(0, n, false);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const readU32 = (bytes: Uint8Array, at: number): number =>
|
|
36
|
+
new DataView(bytes.buffer, bytes.byteOffset).getUint32(at, false);
|
|
37
|
+
|
|
38
|
+
export const readU64 = (bytes: Uint8Array, at: number): bigint =>
|
|
39
|
+
new DataView(bytes.buffer, bytes.byteOffset).getBigUint64(at, false);
|
|
40
|
+
|
|
41
|
+
export function equal(a: Uint8Array, b: Uint8Array): boolean {
|
|
42
|
+
return a.length === b.length && a.every((byte, i) => byte === b[i]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Lexicographic (= big-endian numeric, for equal lengths) comparison. */
|
|
46
|
+
export function compare(a: Uint8Array, b: Uint8Array): number {
|
|
47
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
48
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
49
|
+
}
|
|
50
|
+
return a.length - b.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `u32_be(len) ‖ part` for every part, so fields can never run into each other. */
|
|
54
|
+
export function lengthPrefixed(parts: Uint8Array[]): Uint8Array {
|
|
55
|
+
return concat(...parts.flatMap((part) => [u32be(part.length), part]));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Unix time in ns from the local clock. */
|
|
59
|
+
export const nowNs = (): bigint => BigInt(Date.now()) * 1_000_000n;
|
|
60
|
+
|
|
61
|
+
const KDF_SALT = utf8('zerc20/storage/kdf/v1');
|
|
62
|
+
|
|
63
|
+
/** HKDF-SHA256 of `key`, domain-separated by `label` and the length-prefixed `parts`. */
|
|
64
|
+
export function kdf(key: Uint8Array, label: string, parts: Uint8Array[] = []): Uint8Array {
|
|
65
|
+
return hkdf(sha256, key, KDF_SALT, lengthPrefixed([utf8(label), ...parts]), 32);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** SHA-256 of `tag` and `parts`, each length-prefixed: what anyone can compute, where `kdf` needs a key. */
|
|
69
|
+
export function taggedHash(tag: string, parts: Uint8Array[]): Uint8Array {
|
|
70
|
+
return sha256(lengthPrefixed([utf8(tag), ...parts]));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `nonce(12) ‖ ChaCha20-Poly1305`. The nonce is random so that a retry after a failed but
|
|
74
|
+
* already-visible call never reuses a (key, nonce) pair. */
|
|
75
|
+
export function seal(key: Uint8Array, aad: Uint8Array, msg: Uint8Array): Uint8Array {
|
|
76
|
+
const nonce = randomBytes(12);
|
|
77
|
+
return concat(nonce, chacha20poly1305(key, nonce, aad).encrypt(msg));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function open(key: Uint8Array, aad: Uint8Array, blob: Uint8Array): Uint8Array | null {
|
|
81
|
+
if (blob.length < 12) return null;
|
|
82
|
+
try {
|
|
83
|
+
return chacha20poly1305(key, blob.subarray(0, 12), aad).decrypt(blob.subarray(12));
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** ECIES to an X25519 public key: `ephemeral_pk(32) ‖ seal(..)`. `null` for a low-order key, whose
|
|
90
|
+
* shared secret anyone could compute. */
|
|
91
|
+
export function sealTo(publicKey: Uint8Array, label: string, aad: Uint8Array, msg: Uint8Array): Uint8Array | null {
|
|
92
|
+
const ephemeral = x25519.utils.randomSecretKey();
|
|
93
|
+
const ephemeralPk = x25519.getPublicKey(ephemeral);
|
|
94
|
+
let shared: Uint8Array;
|
|
95
|
+
try {
|
|
96
|
+
shared = x25519.getSharedSecret(ephemeral, publicKey);
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return concat(ephemeralPk, seal(kdf(shared, label, [ephemeralPk, publicKey]), aad, msg));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function openFrom(secret: Uint8Array, label: string, aad: Uint8Array, blob: Uint8Array): Uint8Array | null {
|
|
104
|
+
if (blob.length < 32) return null;
|
|
105
|
+
const ephemeralPk = blob.subarray(0, 32);
|
|
106
|
+
try {
|
|
107
|
+
const shared = x25519.getSharedSecret(secret, ephemeralPk);
|
|
108
|
+
const key = kdf(shared, label, [ephemeralPk, x25519.getPublicKey(secret)]);
|
|
109
|
+
return open(key, aad, blob.subarray(32));
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|