@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.100
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/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +92 -0
- package/README.md +101 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCipheriv,
|
|
3
|
+
createDecipheriv,
|
|
4
|
+
createHash,
|
|
5
|
+
createPrivateKey,
|
|
6
|
+
createPublicKey,
|
|
7
|
+
diffieHellman,
|
|
8
|
+
generateKeyPairSync,
|
|
9
|
+
hkdfSync,
|
|
10
|
+
randomBytes,
|
|
11
|
+
sign,
|
|
12
|
+
timingSafeEqual,
|
|
13
|
+
verify,
|
|
14
|
+
} from "node:crypto";
|
|
15
|
+
|
|
16
|
+
export const DIRECT_TEXT_E2EE_SCHEMA_VERSION = 1;
|
|
17
|
+
export const DIRECT_TEXT_E2EE_PROTOCOL = "hellolynk_direct_text_e2ee_v1";
|
|
18
|
+
export const DIRECT_TEXT_E2EE_V2_SCHEMA_VERSION = 2;
|
|
19
|
+
export const DIRECT_TEXT_E2EE_V2_PROTOCOL = "hellolynk_direct_text_e2ee_v2";
|
|
20
|
+
export const DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION = 2;
|
|
21
|
+
export const DIRECT_TEXT_E2EE_ALGORITHM = "X25519-HKDF-SHA256-AES-256-GCM-Ed25519";
|
|
22
|
+
export const DIRECT_TEXT_E2EE_AUTHORIZATION_SCOPE = "relay.text.send";
|
|
23
|
+
export const DIRECT_TEXT_E2EE_INTENT_SOURCE = "hellolynk_client_explicit_send";
|
|
24
|
+
export const DIRECT_TEXT_E2EE_NATIVE_HUMAN_INTENT_SOURCE = "human_native_dialog";
|
|
25
|
+
export const DIRECT_TEXT_E2EE_PREAUTHORIZED_CONTACT_INTENT_SOURCE = "human_preauthorized_contact";
|
|
26
|
+
export const DIRECT_TEXT_E2EE_ALLOWED_INTENT_SOURCES = Object.freeze([
|
|
27
|
+
DIRECT_TEXT_E2EE_INTENT_SOURCE,
|
|
28
|
+
DIRECT_TEXT_E2EE_NATIVE_HUMAN_INTENT_SOURCE,
|
|
29
|
+
DIRECT_TEXT_E2EE_PREAUTHORIZED_CONTACT_INTENT_SOURCE,
|
|
30
|
+
]);
|
|
31
|
+
export const DIRECT_TEXT_E2EE_MAX_TTL_MS = 5 * 60 * 1000;
|
|
32
|
+
export const DIRECT_TEXT_E2EE_MAX_TEXT_UTF8_BYTES = 64 * 1024;
|
|
33
|
+
|
|
34
|
+
const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
|
35
|
+
const MAX_CIPHERTEXT_BYTES = DIRECT_TEXT_E2EE_MAX_TEXT_UTF8_BYTES;
|
|
36
|
+
const DEVICE_KEY_PURPOSE = "hellolynk_direct_text_device_identity_v2";
|
|
37
|
+
const PRE_KEY_PURPOSE = "hellolynk_direct_text_one_time_prekey_v2";
|
|
38
|
+
const DEVICE_IDENTITY_REGISTRATION_DOMAIN = "hellolynk/direct-text/device-identity-registration/v2\0";
|
|
39
|
+
const PRE_KEY_REGISTRATION_DOMAIN = "hellolynk/direct-text/one-time-prekey-registration/v2\0";
|
|
40
|
+
const ENVELOPE_SIGNATURE_DOMAIN = "hellolynk/direct-text/envelope-signature/v1\0";
|
|
41
|
+
const ENVELOPE_AAD_DOMAIN = "hellolynk/direct-text/envelope-aad/v1\0";
|
|
42
|
+
const HKDF_INFO = Buffer.from("hellolynk/direct-text/e2ee/v1/aes-256-gcm", "utf8");
|
|
43
|
+
const ENVELOPE_V2_SIGNATURE_DOMAIN = "hellolynk/direct-text/envelope-signature/v2\0";
|
|
44
|
+
const ENVELOPE_V2_AAD_DOMAIN = "hellolynk/direct-text/envelope-aad/v2\0";
|
|
45
|
+
const HKDF_V2_INFO = Buffer.from("hellolynk/direct-text/e2ee/v2/aes-256-gcm", "utf8");
|
|
46
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
47
|
+
|
|
48
|
+
const DEVICE_PRIVATE_FIELDS = Object.freeze([
|
|
49
|
+
"schemaVersion", "purpose", "keyId", "algorithm", "publicKey", "privateKey", "createdAt",
|
|
50
|
+
]);
|
|
51
|
+
const DEVICE_PUBLIC_UNSIGNED_FIELDS = Object.freeze([
|
|
52
|
+
"schemaVersion", "keyId", "algorithm", "publicKey", "createdAt",
|
|
53
|
+
]);
|
|
54
|
+
const DEVICE_PUBLIC_FIELDS = Object.freeze([
|
|
55
|
+
...DEVICE_PUBLIC_UNSIGNED_FIELDS, "ownershipSignature",
|
|
56
|
+
]);
|
|
57
|
+
const PRE_KEY_PRIVATE_FIELDS = Object.freeze([
|
|
58
|
+
"schemaVersion", "purpose", "preKeyId", "algorithm", "publicKey", "privateKey", "createdAt",
|
|
59
|
+
]);
|
|
60
|
+
const PRE_KEY_PUBLIC_UNSIGNED_FIELDS = Object.freeze([
|
|
61
|
+
"schemaVersion", "preKeyId", "identityKeyId", "algorithm", "publicKey", "createdAt",
|
|
62
|
+
]);
|
|
63
|
+
const PRE_KEY_PUBLIC_FIELDS = Object.freeze([
|
|
64
|
+
...PRE_KEY_PUBLIC_UNSIGNED_FIELDS, "signature",
|
|
65
|
+
]);
|
|
66
|
+
const PROTECTED_FIELDS = Object.freeze([
|
|
67
|
+
"clientMessageId",
|
|
68
|
+
"senderPrincipalId",
|
|
69
|
+
"senderHelloLynkId",
|
|
70
|
+
"senderAgentId",
|
|
71
|
+
"senderDelegateId",
|
|
72
|
+
"senderDeviceId",
|
|
73
|
+
"senderIdentityKeyId",
|
|
74
|
+
"recipientPrincipalId",
|
|
75
|
+
"recipientHelloLynkId",
|
|
76
|
+
"recipientDeviceId",
|
|
77
|
+
"recipientIdentityKeyId",
|
|
78
|
+
"recipientPreKeyId",
|
|
79
|
+
"authorizationScope",
|
|
80
|
+
"intentSource",
|
|
81
|
+
"messageType",
|
|
82
|
+
"createdAt",
|
|
83
|
+
"expiresAt",
|
|
84
|
+
]);
|
|
85
|
+
const PROTECTED_V2_FIELDS = Object.freeze([
|
|
86
|
+
"clientMessageId",
|
|
87
|
+
"routeKind",
|
|
88
|
+
"senderPrincipalId",
|
|
89
|
+
"senderHelloLynkId",
|
|
90
|
+
"senderAgentId",
|
|
91
|
+
"senderDelegateId",
|
|
92
|
+
"senderDelegateAuthorizationVersion",
|
|
93
|
+
"senderHostId",
|
|
94
|
+
"senderDeviceId",
|
|
95
|
+
"senderIdentityKeyId",
|
|
96
|
+
"recipientPrincipalId",
|
|
97
|
+
"recipientHelloLynkId",
|
|
98
|
+
"recipientDelegateId",
|
|
99
|
+
"recipientDelegateAuthorizationVersion",
|
|
100
|
+
"recipientHostId",
|
|
101
|
+
"recipientRouteVersion",
|
|
102
|
+
"recipientDeviceId",
|
|
103
|
+
"recipientIdentityKeyId",
|
|
104
|
+
"recipientPreKeyId",
|
|
105
|
+
"authorizationScope",
|
|
106
|
+
"intentSource",
|
|
107
|
+
"messageType",
|
|
108
|
+
"createdAt",
|
|
109
|
+
"expiresAt",
|
|
110
|
+
]);
|
|
111
|
+
const ENVELOPE_FIELDS = Object.freeze([
|
|
112
|
+
"schemaVersion",
|
|
113
|
+
"protocol",
|
|
114
|
+
"algorithm",
|
|
115
|
+
"protected",
|
|
116
|
+
"ephemeralPublicKey",
|
|
117
|
+
"salt",
|
|
118
|
+
"iv",
|
|
119
|
+
"ciphertext",
|
|
120
|
+
"authenticationTag",
|
|
121
|
+
"signature",
|
|
122
|
+
]);
|
|
123
|
+
const EXPECTED_RECIPIENT_FIELDS = Object.freeze(["principalId", "hellolynkId", "deviceId"]);
|
|
124
|
+
const EXPECTED_RECIPIENT_V2_FIELDS = Object.freeze([
|
|
125
|
+
"principalId", "hellolynkId", "delegateId", "hostId", "deviceId",
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
export class HelloLynkDirectTextE2eeError extends Error {
|
|
129
|
+
constructor(code) {
|
|
130
|
+
super(code);
|
|
131
|
+
this.name = "HelloLynkDirectTextE2eeError";
|
|
132
|
+
this.code = code;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createDirectTextDeviceKeyBundle(options = {}) {
|
|
137
|
+
const nowMs = normalizeNowOptions(options, "e2ee_key_generation_options_invalid");
|
|
138
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
139
|
+
const publicDer = publicKey.export({ format: "der", type: "spki" });
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
schemaVersion: DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION,
|
|
142
|
+
purpose: DEVICE_KEY_PURPOSE,
|
|
143
|
+
keyId: keyIdForPublicDer("e2ee_id_", publicDer),
|
|
144
|
+
algorithm: "Ed25519",
|
|
145
|
+
publicKey: publicDer.toString("base64url"),
|
|
146
|
+
privateKey: privateKey.export({ format: "der", type: "pkcs8" }).toString("base64url"),
|
|
147
|
+
createdAt: new Date(nowMs).toISOString(),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function publicDirectTextDeviceIdentity(deviceKeyBundle) {
|
|
152
|
+
const bundle = canonicalDevicePrivateBundle(deviceKeyBundle);
|
|
153
|
+
const unsigned = canonicalDeviceIdentityUnsigned({
|
|
154
|
+
schemaVersion: bundle.schemaVersion,
|
|
155
|
+
keyId: bundle.keyId,
|
|
156
|
+
algorithm: bundle.algorithm,
|
|
157
|
+
publicKey: bundle.publicKey,
|
|
158
|
+
createdAt: bundle.createdAt,
|
|
159
|
+
});
|
|
160
|
+
const privateKey = importPrivateDer(bundle.privateKey, "ed25519", "e2ee_device_private_key_invalid");
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
...unsigned,
|
|
163
|
+
ownershipSignature: sign(
|
|
164
|
+
null,
|
|
165
|
+
domainSeparatedBytes(DEVICE_IDENTITY_REGISTRATION_DOMAIN, unsigned),
|
|
166
|
+
privateKey,
|
|
167
|
+
).toString("base64url"),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function verifyDeviceIdentityRegistration(identity) {
|
|
172
|
+
try {
|
|
173
|
+
const accepted = canonicalDeviceIdentity(identity);
|
|
174
|
+
const publicKey = importPublicDer(accepted.publicKey, "ed25519", "e2ee_device_public_key_invalid");
|
|
175
|
+
return verify(
|
|
176
|
+
null,
|
|
177
|
+
domainSeparatedBytes(
|
|
178
|
+
DEVICE_IDENTITY_REGISTRATION_DOMAIN,
|
|
179
|
+
canonicalDeviceIdentityUnsigned(accepted),
|
|
180
|
+
),
|
|
181
|
+
publicKey,
|
|
182
|
+
decodeBase64Url(accepted.ownershipSignature, 64, "e2ee_identity_signature_invalid"),
|
|
183
|
+
);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function createOneTimePreKey(options = {}) {
|
|
190
|
+
const nowMs = normalizeNowOptions(options, "e2ee_prekey_generation_options_invalid");
|
|
191
|
+
const { privateKey, publicKey } = generateKeyPairSync("x25519");
|
|
192
|
+
const publicDer = publicKey.export({ format: "der", type: "spki" });
|
|
193
|
+
return Object.freeze({
|
|
194
|
+
schemaVersion: DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION,
|
|
195
|
+
purpose: PRE_KEY_PURPOSE,
|
|
196
|
+
preKeyId: keyIdForPublicDer("e2ee_pre_", publicDer),
|
|
197
|
+
algorithm: "X25519",
|
|
198
|
+
publicKey: publicDer.toString("base64url"),
|
|
199
|
+
privateKey: privateKey.export({ format: "der", type: "pkcs8" }).toString("base64url"),
|
|
200
|
+
createdAt: new Date(nowMs).toISOString(),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function publicOneTimePreKey(oneTimePreKey, deviceKeyBundle) {
|
|
205
|
+
const preKey = canonicalPrivatePreKey(oneTimePreKey);
|
|
206
|
+
const bundle = canonicalDevicePrivateBundle(deviceKeyBundle);
|
|
207
|
+
const unsigned = canonicalPublicPreKeyUnsigned({
|
|
208
|
+
schemaVersion: preKey.schemaVersion,
|
|
209
|
+
preKeyId: preKey.preKeyId,
|
|
210
|
+
identityKeyId: bundle.keyId,
|
|
211
|
+
algorithm: preKey.algorithm,
|
|
212
|
+
publicKey: preKey.publicKey,
|
|
213
|
+
createdAt: preKey.createdAt,
|
|
214
|
+
});
|
|
215
|
+
const privateKey = importPrivateDer(bundle.privateKey, "ed25519", "e2ee_device_private_key_invalid");
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
...unsigned,
|
|
218
|
+
signature: sign(
|
|
219
|
+
null,
|
|
220
|
+
domainSeparatedBytes(PRE_KEY_REGISTRATION_DOMAIN, unsigned),
|
|
221
|
+
privateKey,
|
|
222
|
+
).toString("base64url"),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function verifyOneTimePreKeyRegistration(input) {
|
|
227
|
+
try {
|
|
228
|
+
assertExactFields(input, ["identity", "oneTimePreKey"], "e2ee_prekey_verification_input_invalid");
|
|
229
|
+
const identity = canonicalDeviceIdentity(input.identity);
|
|
230
|
+
const preKey = canonicalPublicPreKey(input.oneTimePreKey);
|
|
231
|
+
if (!verifyDeviceIdentityRegistration(identity) || preKey.identityKeyId !== identity.keyId) return false;
|
|
232
|
+
const publicKey = importPublicDer(identity.publicKey, "ed25519", "e2ee_device_public_key_invalid");
|
|
233
|
+
return verify(
|
|
234
|
+
null,
|
|
235
|
+
domainSeparatedBytes(
|
|
236
|
+
PRE_KEY_REGISTRATION_DOMAIN,
|
|
237
|
+
canonicalPublicPreKeyUnsigned(preKey),
|
|
238
|
+
),
|
|
239
|
+
publicKey,
|
|
240
|
+
decodeBase64Url(preKey.signature, 64, "e2ee_prekey_signature_invalid"),
|
|
241
|
+
);
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function sealDirectTextEnvelope(input, options = {}) {
|
|
248
|
+
assertExactFields(input, [
|
|
249
|
+
"protectedMetadata",
|
|
250
|
+
"recipientDeviceIdentity",
|
|
251
|
+
"recipientOneTimePreKey",
|
|
252
|
+
"senderDeviceKeyBundle",
|
|
253
|
+
"text",
|
|
254
|
+
], "e2ee_seal_input_invalid");
|
|
255
|
+
const nowMs = normalizeNowOptions(options, "e2ee_seal_options_invalid");
|
|
256
|
+
const protectedMetadata = canonicalProtectedMetadata(input.protectedMetadata);
|
|
257
|
+
assertEnvelopeTime(protectedMetadata, nowMs);
|
|
258
|
+
const senderBundle = canonicalDevicePrivateBundle(input.senderDeviceKeyBundle);
|
|
259
|
+
const recipientIdentity = canonicalDeviceIdentity(input.recipientDeviceIdentity);
|
|
260
|
+
const recipientPreKey = canonicalPublicPreKey(input.recipientOneTimePreKey);
|
|
261
|
+
if (!verifyDeviceIdentityRegistration(recipientIdentity)) {
|
|
262
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_identity_signature_invalid");
|
|
263
|
+
}
|
|
264
|
+
if (!verifyOneTimePreKeyRegistration({ identity: recipientIdentity, oneTimePreKey: recipientPreKey })) {
|
|
265
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_prekey_signature_invalid");
|
|
266
|
+
}
|
|
267
|
+
if (protectedMetadata.senderIdentityKeyId !== senderBundle.keyId) {
|
|
268
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_sender_identity_key_mismatch");
|
|
269
|
+
}
|
|
270
|
+
if (protectedMetadata.recipientIdentityKeyId !== recipientIdentity.keyId) {
|
|
271
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_identity_key_mismatch");
|
|
272
|
+
}
|
|
273
|
+
if (protectedMetadata.recipientPreKeyId !== recipientPreKey.preKeyId) {
|
|
274
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_prekey_mismatch");
|
|
275
|
+
}
|
|
276
|
+
const textValue = assertText(input.text);
|
|
277
|
+
const plaintext = Buffer.from(textValue, "utf8");
|
|
278
|
+
const { privateKey: ephemeralPrivateKey, publicKey: ephemeralPublicKey } = generateKeyPairSync("x25519");
|
|
279
|
+
const ephemeralPublicDer = ephemeralPublicKey.export({ format: "der", type: "spki" });
|
|
280
|
+
const salt = randomBytes(32);
|
|
281
|
+
const iv = randomBytes(12);
|
|
282
|
+
const header = {
|
|
283
|
+
protected: protectedMetadata,
|
|
284
|
+
ephemeralPublicKey: ephemeralPublicDer.toString("base64url"),
|
|
285
|
+
salt: salt.toString("base64url"),
|
|
286
|
+
iv: iv.toString("base64url"),
|
|
287
|
+
};
|
|
288
|
+
const aad = domainSeparatedBytes(ENVELOPE_AAD_DOMAIN, header);
|
|
289
|
+
const recipientPublicKey = importPublicDer(
|
|
290
|
+
recipientPreKey.publicKey,
|
|
291
|
+
"x25519",
|
|
292
|
+
"e2ee_recipient_prekey_public_invalid",
|
|
293
|
+
);
|
|
294
|
+
let sharedSecret;
|
|
295
|
+
let aesKey;
|
|
296
|
+
try {
|
|
297
|
+
sharedSecret = diffieHellman({ privateKey: ephemeralPrivateKey, publicKey: recipientPublicKey });
|
|
298
|
+
aesKey = Buffer.from(hkdfSync("sha256", sharedSecret, salt, HKDF_INFO, 32));
|
|
299
|
+
const cipher = createCipheriv("aes-256-gcm", aesKey, iv, { authTagLength: 16 });
|
|
300
|
+
cipher.setAAD(aad, { plaintextLength: plaintext.length });
|
|
301
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
302
|
+
const unsignedEnvelope = canonicalUnsignedEnvelope({
|
|
303
|
+
schemaVersion: DIRECT_TEXT_E2EE_SCHEMA_VERSION,
|
|
304
|
+
protocol: DIRECT_TEXT_E2EE_PROTOCOL,
|
|
305
|
+
algorithm: DIRECT_TEXT_E2EE_ALGORITHM,
|
|
306
|
+
protected: protectedMetadata,
|
|
307
|
+
ephemeralPublicKey: header.ephemeralPublicKey,
|
|
308
|
+
salt: header.salt,
|
|
309
|
+
iv: header.iv,
|
|
310
|
+
ciphertext: ciphertext.toString("base64url"),
|
|
311
|
+
authenticationTag: cipher.getAuthTag().toString("base64url"),
|
|
312
|
+
});
|
|
313
|
+
const signingPrivateKey = importPrivateDer(
|
|
314
|
+
senderBundle.privateKey,
|
|
315
|
+
"ed25519",
|
|
316
|
+
"e2ee_sender_private_key_invalid",
|
|
317
|
+
);
|
|
318
|
+
return Object.freeze({
|
|
319
|
+
...unsignedEnvelope,
|
|
320
|
+
signature: sign(
|
|
321
|
+
null,
|
|
322
|
+
domainSeparatedBytes(ENVELOPE_SIGNATURE_DOMAIN, unsignedEnvelope),
|
|
323
|
+
signingPrivateKey,
|
|
324
|
+
).toString("base64url"),
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error instanceof HelloLynkDirectTextE2eeError) throw error;
|
|
328
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_encryption_failed");
|
|
329
|
+
} finally {
|
|
330
|
+
plaintext.fill(0);
|
|
331
|
+
sharedSecret?.fill(0);
|
|
332
|
+
aesKey?.fill(0);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function openDirectTextEnvelope(input, options = {}) {
|
|
337
|
+
assertExactFields(input, [
|
|
338
|
+
"envelope",
|
|
339
|
+
"senderDeviceIdentity",
|
|
340
|
+
"recipientDeviceKeyBundle",
|
|
341
|
+
"recipientOneTimePreKey",
|
|
342
|
+
"expectedRecipient",
|
|
343
|
+
], "e2ee_open_input_invalid");
|
|
344
|
+
const nowMs = normalizeNowOptions(options, "e2ee_open_options_invalid");
|
|
345
|
+
const envelope = canonicalEnvelope(input.envelope);
|
|
346
|
+
const senderIdentity = canonicalDeviceIdentity(input.senderDeviceIdentity);
|
|
347
|
+
if (!verifyDeviceIdentityRegistration(senderIdentity)) {
|
|
348
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_sender_identity_signature_invalid");
|
|
349
|
+
}
|
|
350
|
+
const recipientBundle = canonicalDevicePrivateBundle(input.recipientDeviceKeyBundle);
|
|
351
|
+
const recipientPreKey = canonicalPrivatePreKey(input.recipientOneTimePreKey);
|
|
352
|
+
const expectedRecipient = canonicalExpectedRecipient(input.expectedRecipient);
|
|
353
|
+
const protectedMetadata = envelope.protected;
|
|
354
|
+
if (protectedMetadata.senderIdentityKeyId !== senderIdentity.keyId) {
|
|
355
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_sender_identity_key_mismatch");
|
|
356
|
+
}
|
|
357
|
+
if (protectedMetadata.recipientIdentityKeyId !== recipientBundle.keyId) {
|
|
358
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_identity_key_mismatch");
|
|
359
|
+
}
|
|
360
|
+
if (protectedMetadata.recipientPreKeyId !== recipientPreKey.preKeyId) {
|
|
361
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_prekey_mismatch");
|
|
362
|
+
}
|
|
363
|
+
if (
|
|
364
|
+
protectedMetadata.recipientPrincipalId !== expectedRecipient.principalId
|
|
365
|
+
|| protectedMetadata.recipientHelloLynkId !== expectedRecipient.hellolynkId
|
|
366
|
+
|| protectedMetadata.recipientDeviceId !== expectedRecipient.deviceId
|
|
367
|
+
) {
|
|
368
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_route_mismatch");
|
|
369
|
+
}
|
|
370
|
+
const unsignedEnvelope = canonicalUnsignedEnvelope(envelope);
|
|
371
|
+
const senderPublicKey = importPublicDer(
|
|
372
|
+
senderIdentity.publicKey,
|
|
373
|
+
"ed25519",
|
|
374
|
+
"e2ee_sender_public_key_invalid",
|
|
375
|
+
);
|
|
376
|
+
if (!verify(
|
|
377
|
+
null,
|
|
378
|
+
domainSeparatedBytes(ENVELOPE_SIGNATURE_DOMAIN, unsignedEnvelope),
|
|
379
|
+
senderPublicKey,
|
|
380
|
+
decodeBase64Url(envelope.signature, 64, "e2ee_envelope_signature_invalid"),
|
|
381
|
+
)) {
|
|
382
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_envelope_signature_invalid");
|
|
383
|
+
}
|
|
384
|
+
assertEnvelopeTime(protectedMetadata, nowMs);
|
|
385
|
+
const ephemeralPublicKey = importPublicDer(
|
|
386
|
+
envelope.ephemeralPublicKey,
|
|
387
|
+
"x25519",
|
|
388
|
+
"e2ee_ephemeral_public_key_invalid",
|
|
389
|
+
);
|
|
390
|
+
const recipientPrivateKey = importPrivateDer(
|
|
391
|
+
recipientPreKey.privateKey,
|
|
392
|
+
"x25519",
|
|
393
|
+
"e2ee_recipient_prekey_private_invalid",
|
|
394
|
+
);
|
|
395
|
+
const salt = decodeBase64Url(envelope.salt, 32, "e2ee_envelope_salt_invalid");
|
|
396
|
+
const iv = decodeBase64Url(envelope.iv, 12, "e2ee_envelope_iv_invalid");
|
|
397
|
+
const ciphertext = decodeBase64Url(
|
|
398
|
+
envelope.ciphertext,
|
|
399
|
+
null,
|
|
400
|
+
"e2ee_envelope_ciphertext_invalid",
|
|
401
|
+
{ minimumBytes: 1, maximumBytes: MAX_CIPHERTEXT_BYTES },
|
|
402
|
+
);
|
|
403
|
+
const authenticationTag = decodeBase64Url(
|
|
404
|
+
envelope.authenticationTag,
|
|
405
|
+
16,
|
|
406
|
+
"e2ee_envelope_authentication_tag_invalid",
|
|
407
|
+
);
|
|
408
|
+
const header = {
|
|
409
|
+
protected: protectedMetadata,
|
|
410
|
+
ephemeralPublicKey: envelope.ephemeralPublicKey,
|
|
411
|
+
salt: envelope.salt,
|
|
412
|
+
iv: envelope.iv,
|
|
413
|
+
};
|
|
414
|
+
const aad = domainSeparatedBytes(ENVELOPE_AAD_DOMAIN, header);
|
|
415
|
+
let sharedSecret;
|
|
416
|
+
let aesKey;
|
|
417
|
+
let plaintext;
|
|
418
|
+
try {
|
|
419
|
+
sharedSecret = diffieHellman({ privateKey: recipientPrivateKey, publicKey: ephemeralPublicKey });
|
|
420
|
+
aesKey = Buffer.from(hkdfSync("sha256", sharedSecret, salt, HKDF_INFO, 32));
|
|
421
|
+
const decipher = createDecipheriv("aes-256-gcm", aesKey, iv, { authTagLength: 16 });
|
|
422
|
+
decipher.setAAD(aad, { plaintextLength: ciphertext.length });
|
|
423
|
+
decipher.setAuthTag(authenticationTag);
|
|
424
|
+
plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
425
|
+
} catch {
|
|
426
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_decryption_failed");
|
|
427
|
+
} finally {
|
|
428
|
+
sharedSecret?.fill(0);
|
|
429
|
+
aesKey?.fill(0);
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
let text;
|
|
433
|
+
try {
|
|
434
|
+
text = textDecoder.decode(plaintext);
|
|
435
|
+
} catch {
|
|
436
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_plaintext_invalid");
|
|
437
|
+
}
|
|
438
|
+
assertText(text);
|
|
439
|
+
return Object.freeze({
|
|
440
|
+
text,
|
|
441
|
+
protectedMetadata,
|
|
442
|
+
envelopeDigest: digestDirectTextEnvelope(envelope),
|
|
443
|
+
});
|
|
444
|
+
} finally {
|
|
445
|
+
plaintext.fill(0);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function digestDirectTextEnvelope(envelope) {
|
|
450
|
+
const canonical = canonicalEnvelope(envelope);
|
|
451
|
+
return createHash("sha256").update(JSON.stringify(canonical), "utf8").digest("hex");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function sealDirectTextEnvelopeV2(input, options = {}) {
|
|
455
|
+
assertExactFields(input, [
|
|
456
|
+
"protectedMetadata",
|
|
457
|
+
"recipientDeviceIdentity",
|
|
458
|
+
"recipientOneTimePreKey",
|
|
459
|
+
"senderDeviceKeyBundle",
|
|
460
|
+
"text",
|
|
461
|
+
], "e2ee_v2_seal_input_invalid");
|
|
462
|
+
const nowMs = normalizeNowOptions(options, "e2ee_v2_seal_options_invalid");
|
|
463
|
+
const protectedMetadata = canonicalProtectedMetadataV2(input.protectedMetadata);
|
|
464
|
+
assertEnvelopeTime(protectedMetadata, nowMs);
|
|
465
|
+
const senderBundle = canonicalDevicePrivateBundle(input.senderDeviceKeyBundle);
|
|
466
|
+
const recipientIdentity = canonicalDeviceIdentity(input.recipientDeviceIdentity);
|
|
467
|
+
const recipientPreKey = canonicalPublicPreKey(input.recipientOneTimePreKey);
|
|
468
|
+
if (!verifyDeviceIdentityRegistration(recipientIdentity)) {
|
|
469
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_identity_signature_invalid");
|
|
470
|
+
}
|
|
471
|
+
if (!verifyOneTimePreKeyRegistration({ identity: recipientIdentity, oneTimePreKey: recipientPreKey })) {
|
|
472
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_prekey_signature_invalid");
|
|
473
|
+
}
|
|
474
|
+
if (protectedMetadata.senderIdentityKeyId !== senderBundle.keyId) {
|
|
475
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_sender_identity_key_mismatch");
|
|
476
|
+
}
|
|
477
|
+
if (protectedMetadata.recipientIdentityKeyId !== recipientIdentity.keyId) {
|
|
478
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_identity_key_mismatch");
|
|
479
|
+
}
|
|
480
|
+
if (protectedMetadata.recipientPreKeyId !== recipientPreKey.preKeyId) {
|
|
481
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_prekey_mismatch");
|
|
482
|
+
}
|
|
483
|
+
const textValue = assertText(input.text);
|
|
484
|
+
const plaintext = Buffer.from(textValue, "utf8");
|
|
485
|
+
const { privateKey: ephemeralPrivateKey, publicKey: ephemeralPublicKey } = generateKeyPairSync("x25519");
|
|
486
|
+
const ephemeralPublicDer = ephemeralPublicKey.export({ format: "der", type: "spki" });
|
|
487
|
+
const salt = randomBytes(32);
|
|
488
|
+
const iv = randomBytes(12);
|
|
489
|
+
const header = {
|
|
490
|
+
protected: protectedMetadata,
|
|
491
|
+
ephemeralPublicKey: ephemeralPublicDer.toString("base64url"),
|
|
492
|
+
salt: salt.toString("base64url"),
|
|
493
|
+
iv: iv.toString("base64url"),
|
|
494
|
+
};
|
|
495
|
+
const aad = domainSeparatedBytes(ENVELOPE_V2_AAD_DOMAIN, header);
|
|
496
|
+
const recipientPublicKey = importPublicDer(
|
|
497
|
+
recipientPreKey.publicKey,
|
|
498
|
+
"x25519",
|
|
499
|
+
"e2ee_recipient_prekey_public_invalid",
|
|
500
|
+
);
|
|
501
|
+
let sharedSecret;
|
|
502
|
+
let aesKey;
|
|
503
|
+
try {
|
|
504
|
+
sharedSecret = diffieHellman({ privateKey: ephemeralPrivateKey, publicKey: recipientPublicKey });
|
|
505
|
+
aesKey = Buffer.from(hkdfSync("sha256", sharedSecret, salt, HKDF_V2_INFO, 32));
|
|
506
|
+
const cipher = createCipheriv("aes-256-gcm", aesKey, iv, { authTagLength: 16 });
|
|
507
|
+
cipher.setAAD(aad, { plaintextLength: plaintext.length });
|
|
508
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
509
|
+
const unsignedEnvelope = canonicalUnsignedEnvelopeV2({
|
|
510
|
+
schemaVersion: DIRECT_TEXT_E2EE_V2_SCHEMA_VERSION,
|
|
511
|
+
protocol: DIRECT_TEXT_E2EE_V2_PROTOCOL,
|
|
512
|
+
algorithm: DIRECT_TEXT_E2EE_ALGORITHM,
|
|
513
|
+
protected: protectedMetadata,
|
|
514
|
+
ephemeralPublicKey: header.ephemeralPublicKey,
|
|
515
|
+
salt: header.salt,
|
|
516
|
+
iv: header.iv,
|
|
517
|
+
ciphertext: ciphertext.toString("base64url"),
|
|
518
|
+
authenticationTag: cipher.getAuthTag().toString("base64url"),
|
|
519
|
+
});
|
|
520
|
+
const signingPrivateKey = importPrivateDer(
|
|
521
|
+
senderBundle.privateKey,
|
|
522
|
+
"ed25519",
|
|
523
|
+
"e2ee_sender_private_key_invalid",
|
|
524
|
+
);
|
|
525
|
+
return Object.freeze({
|
|
526
|
+
...unsignedEnvelope,
|
|
527
|
+
signature: sign(
|
|
528
|
+
null,
|
|
529
|
+
domainSeparatedBytes(ENVELOPE_V2_SIGNATURE_DOMAIN, unsignedEnvelope),
|
|
530
|
+
signingPrivateKey,
|
|
531
|
+
).toString("base64url"),
|
|
532
|
+
});
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (error instanceof HelloLynkDirectTextE2eeError) throw error;
|
|
535
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_encryption_failed");
|
|
536
|
+
} finally {
|
|
537
|
+
plaintext.fill(0);
|
|
538
|
+
sharedSecret?.fill(0);
|
|
539
|
+
aesKey?.fill(0);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function openDirectTextEnvelopeV2(input, options = {}) {
|
|
544
|
+
assertExactFields(input, [
|
|
545
|
+
"envelope",
|
|
546
|
+
"senderDeviceIdentity",
|
|
547
|
+
"recipientDeviceKeyBundle",
|
|
548
|
+
"recipientOneTimePreKey",
|
|
549
|
+
"expectedRecipient",
|
|
550
|
+
], "e2ee_v2_open_input_invalid");
|
|
551
|
+
const nowMs = normalizeNowOptions(options, "e2ee_v2_open_options_invalid");
|
|
552
|
+
const envelope = canonicalEnvelopeV2(input.envelope);
|
|
553
|
+
const senderIdentity = canonicalDeviceIdentity(input.senderDeviceIdentity);
|
|
554
|
+
if (!verifyDeviceIdentityRegistration(senderIdentity)) {
|
|
555
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_sender_identity_signature_invalid");
|
|
556
|
+
}
|
|
557
|
+
const recipientBundle = canonicalDevicePrivateBundle(input.recipientDeviceKeyBundle);
|
|
558
|
+
const recipientPreKey = canonicalPrivatePreKey(input.recipientOneTimePreKey);
|
|
559
|
+
const expectedRecipient = canonicalExpectedRecipientV2(input.expectedRecipient);
|
|
560
|
+
const protectedMetadata = envelope.protected;
|
|
561
|
+
if (protectedMetadata.senderIdentityKeyId !== senderIdentity.keyId) {
|
|
562
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_sender_identity_key_mismatch");
|
|
563
|
+
}
|
|
564
|
+
if (protectedMetadata.recipientIdentityKeyId !== recipientBundle.keyId) {
|
|
565
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_identity_key_mismatch");
|
|
566
|
+
}
|
|
567
|
+
if (protectedMetadata.recipientPreKeyId !== recipientPreKey.preKeyId) {
|
|
568
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_prekey_mismatch");
|
|
569
|
+
}
|
|
570
|
+
if (
|
|
571
|
+
protectedMetadata.recipientPrincipalId !== expectedRecipient.principalId
|
|
572
|
+
|| protectedMetadata.recipientHelloLynkId !== expectedRecipient.hellolynkId
|
|
573
|
+
|| protectedMetadata.recipientDelegateId !== expectedRecipient.delegateId
|
|
574
|
+
|| protectedMetadata.recipientHostId !== expectedRecipient.hostId
|
|
575
|
+
|| protectedMetadata.recipientDeviceId !== expectedRecipient.deviceId
|
|
576
|
+
) {
|
|
577
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_recipient_route_mismatch");
|
|
578
|
+
}
|
|
579
|
+
const unsignedEnvelope = canonicalUnsignedEnvelopeV2(envelope);
|
|
580
|
+
const senderPublicKey = importPublicDer(
|
|
581
|
+
senderIdentity.publicKey,
|
|
582
|
+
"ed25519",
|
|
583
|
+
"e2ee_sender_public_key_invalid",
|
|
584
|
+
);
|
|
585
|
+
if (!verify(
|
|
586
|
+
null,
|
|
587
|
+
domainSeparatedBytes(ENVELOPE_V2_SIGNATURE_DOMAIN, unsignedEnvelope),
|
|
588
|
+
senderPublicKey,
|
|
589
|
+
decodeBase64Url(envelope.signature, 64, "e2ee_envelope_signature_invalid"),
|
|
590
|
+
)) {
|
|
591
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_envelope_signature_invalid");
|
|
592
|
+
}
|
|
593
|
+
assertEnvelopeTime(protectedMetadata, nowMs);
|
|
594
|
+
const ephemeralPublicKey = importPublicDer(
|
|
595
|
+
envelope.ephemeralPublicKey,
|
|
596
|
+
"x25519",
|
|
597
|
+
"e2ee_ephemeral_public_key_invalid",
|
|
598
|
+
);
|
|
599
|
+
const recipientPrivateKey = importPrivateDer(
|
|
600
|
+
recipientPreKey.privateKey,
|
|
601
|
+
"x25519",
|
|
602
|
+
"e2ee_recipient_prekey_private_invalid",
|
|
603
|
+
);
|
|
604
|
+
const salt = decodeBase64Url(envelope.salt, 32, "e2ee_envelope_salt_invalid");
|
|
605
|
+
const iv = decodeBase64Url(envelope.iv, 12, "e2ee_envelope_iv_invalid");
|
|
606
|
+
const ciphertext = decodeBase64Url(
|
|
607
|
+
envelope.ciphertext,
|
|
608
|
+
null,
|
|
609
|
+
"e2ee_envelope_ciphertext_invalid",
|
|
610
|
+
{ minimumBytes: 1, maximumBytes: MAX_CIPHERTEXT_BYTES },
|
|
611
|
+
);
|
|
612
|
+
const authenticationTag = decodeBase64Url(
|
|
613
|
+
envelope.authenticationTag,
|
|
614
|
+
16,
|
|
615
|
+
"e2ee_envelope_authentication_tag_invalid",
|
|
616
|
+
);
|
|
617
|
+
const header = {
|
|
618
|
+
protected: protectedMetadata,
|
|
619
|
+
ephemeralPublicKey: envelope.ephemeralPublicKey,
|
|
620
|
+
salt: envelope.salt,
|
|
621
|
+
iv: envelope.iv,
|
|
622
|
+
};
|
|
623
|
+
const aad = domainSeparatedBytes(ENVELOPE_V2_AAD_DOMAIN, header);
|
|
624
|
+
let sharedSecret;
|
|
625
|
+
let aesKey;
|
|
626
|
+
let plaintext;
|
|
627
|
+
try {
|
|
628
|
+
sharedSecret = diffieHellman({ privateKey: recipientPrivateKey, publicKey: ephemeralPublicKey });
|
|
629
|
+
aesKey = Buffer.from(hkdfSync("sha256", sharedSecret, salt, HKDF_V2_INFO, 32));
|
|
630
|
+
const decipher = createDecipheriv("aes-256-gcm", aesKey, iv, { authTagLength: 16 });
|
|
631
|
+
decipher.setAAD(aad, { plaintextLength: ciphertext.length });
|
|
632
|
+
decipher.setAuthTag(authenticationTag);
|
|
633
|
+
plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
634
|
+
} catch {
|
|
635
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_decryption_failed");
|
|
636
|
+
} finally {
|
|
637
|
+
sharedSecret?.fill(0);
|
|
638
|
+
aesKey?.fill(0);
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
let text;
|
|
642
|
+
try {
|
|
643
|
+
text = textDecoder.decode(plaintext);
|
|
644
|
+
} catch {
|
|
645
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_plaintext_invalid");
|
|
646
|
+
}
|
|
647
|
+
assertText(text);
|
|
648
|
+
return Object.freeze({
|
|
649
|
+
text,
|
|
650
|
+
protectedMetadata,
|
|
651
|
+
envelopeDigest: digestDirectTextEnvelopeV2(envelope),
|
|
652
|
+
});
|
|
653
|
+
} finally {
|
|
654
|
+
plaintext.fill(0);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function verifyDirectTextEnvelopeSignatureV2(input, options = {}) {
|
|
659
|
+
try {
|
|
660
|
+
assertExactFields(input, [
|
|
661
|
+
"envelope",
|
|
662
|
+
"senderDeviceIdentity",
|
|
663
|
+
], "e2ee_v2_verify_input_invalid");
|
|
664
|
+
const nowMs = normalizeNowOptions(options, "e2ee_v2_verify_options_invalid");
|
|
665
|
+
const envelope = canonicalEnvelopeV2(input.envelope);
|
|
666
|
+
const senderIdentity = canonicalDeviceIdentity(input.senderDeviceIdentity);
|
|
667
|
+
if (!verifyDeviceIdentityRegistration(senderIdentity)) return false;
|
|
668
|
+
if (envelope.protected.senderIdentityKeyId !== senderIdentity.keyId) return false;
|
|
669
|
+
const senderPublicKey = importPublicDer(
|
|
670
|
+
senderIdentity.publicKey,
|
|
671
|
+
"ed25519",
|
|
672
|
+
"e2ee_sender_public_key_invalid",
|
|
673
|
+
);
|
|
674
|
+
if (!verify(
|
|
675
|
+
null,
|
|
676
|
+
domainSeparatedBytes(ENVELOPE_V2_SIGNATURE_DOMAIN, canonicalUnsignedEnvelopeV2(envelope)),
|
|
677
|
+
senderPublicKey,
|
|
678
|
+
decodeBase64Url(envelope.signature, 64, "e2ee_envelope_signature_invalid"),
|
|
679
|
+
)) return false;
|
|
680
|
+
assertEnvelopeTime(envelope.protected, nowMs);
|
|
681
|
+
return true;
|
|
682
|
+
} catch {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function digestDirectTextEnvelopeV2(envelope) {
|
|
688
|
+
const canonical = canonicalEnvelopeV2(envelope);
|
|
689
|
+
return createHash("sha256").update(JSON.stringify(canonical), "utf8").digest("hex");
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function classifyDirectTextEnvelopeReplay(input) {
|
|
693
|
+
assertExactFields(
|
|
694
|
+
input,
|
|
695
|
+
["clientMessageId", "envelopeDigest", "existingEnvelopeDigest"],
|
|
696
|
+
"e2ee_replay_input_invalid",
|
|
697
|
+
);
|
|
698
|
+
const clientMessageId = assertSafeId(input.clientMessageId, "e2ee_client_message_id_invalid");
|
|
699
|
+
const envelopeDigest = assertDigest(input.envelopeDigest);
|
|
700
|
+
const existingEnvelopeDigest = input.existingEnvelopeDigest == null
|
|
701
|
+
? null
|
|
702
|
+
: assertDigest(input.existingEnvelopeDigest);
|
|
703
|
+
if (existingEnvelopeDigest == null) {
|
|
704
|
+
return Object.freeze({ decision: "accept_new", clientMessageId, envelopeDigest });
|
|
705
|
+
}
|
|
706
|
+
const same = timingSafeEqual(Buffer.from(envelopeDigest, "hex"), Buffer.from(existingEnvelopeDigest, "hex"));
|
|
707
|
+
return Object.freeze({
|
|
708
|
+
decision: same ? "idempotent_replay" : "conflicting_replay",
|
|
709
|
+
clientMessageId,
|
|
710
|
+
envelopeDigest,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function canonicalDevicePrivateBundle(input) {
|
|
715
|
+
assertExactFields(input, DEVICE_PRIVATE_FIELDS, "e2ee_device_key_bundle_invalid");
|
|
716
|
+
if (
|
|
717
|
+
input.schemaVersion !== DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION
|
|
718
|
+
|| input.purpose !== DEVICE_KEY_PURPOSE
|
|
719
|
+
|| input.algorithm !== "Ed25519"
|
|
720
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_device_key_bundle_invalid");
|
|
721
|
+
const publicDer = decodeBase64Url(input.publicKey, 44, "e2ee_device_public_key_invalid");
|
|
722
|
+
const privateKey = importPrivateDer(input.privateKey, "ed25519", "e2ee_device_private_key_invalid");
|
|
723
|
+
const derivedPublicDer = createPublicKey(privateKey).export({ format: "der", type: "spki" });
|
|
724
|
+
if (!timingSafeEqual(publicDer, derivedPublicDer)) {
|
|
725
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_device_key_pair_mismatch");
|
|
726
|
+
}
|
|
727
|
+
const keyId = assertSafeId(input.keyId, "e2ee_identity_key_id_invalid");
|
|
728
|
+
if (keyId !== keyIdForPublicDer("e2ee_id_", publicDer)) {
|
|
729
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_identity_key_id_invalid");
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
schemaVersion: input.schemaVersion,
|
|
733
|
+
purpose: input.purpose,
|
|
734
|
+
keyId,
|
|
735
|
+
algorithm: input.algorithm,
|
|
736
|
+
publicKey: input.publicKey,
|
|
737
|
+
privateKey: input.privateKey,
|
|
738
|
+
createdAt: assertIsoTimestamp(input.createdAt, "e2ee_identity_created_at_invalid"),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function canonicalDeviceIdentityUnsigned(input) {
|
|
743
|
+
const allowed = DEVICE_PUBLIC_FIELDS.includes("ownershipSignature") && Object.hasOwn(input, "ownershipSignature")
|
|
744
|
+
? DEVICE_PUBLIC_FIELDS
|
|
745
|
+
: DEVICE_PUBLIC_UNSIGNED_FIELDS;
|
|
746
|
+
assertExactFields(input, allowed, "e2ee_device_identity_invalid");
|
|
747
|
+
if (
|
|
748
|
+
input.schemaVersion !== DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION
|
|
749
|
+
|| input.algorithm !== "Ed25519"
|
|
750
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_device_identity_invalid");
|
|
751
|
+
const publicDer = decodeBase64Url(input.publicKey, 44, "e2ee_device_public_key_invalid");
|
|
752
|
+
const keyId = assertSafeId(input.keyId, "e2ee_identity_key_id_invalid");
|
|
753
|
+
if (keyId !== keyIdForPublicDer("e2ee_id_", publicDer)) {
|
|
754
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_identity_key_id_invalid");
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
schemaVersion: input.schemaVersion,
|
|
758
|
+
keyId,
|
|
759
|
+
algorithm: input.algorithm,
|
|
760
|
+
publicKey: input.publicKey,
|
|
761
|
+
createdAt: assertIsoTimestamp(input.createdAt, "e2ee_identity_created_at_invalid"),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function canonicalDeviceIdentity(input) {
|
|
766
|
+
assertExactFields(input, DEVICE_PUBLIC_FIELDS, "e2ee_device_identity_invalid");
|
|
767
|
+
const unsigned = canonicalDeviceIdentityUnsigned({
|
|
768
|
+
schemaVersion: input.schemaVersion,
|
|
769
|
+
keyId: input.keyId,
|
|
770
|
+
algorithm: input.algorithm,
|
|
771
|
+
publicKey: input.publicKey,
|
|
772
|
+
createdAt: input.createdAt,
|
|
773
|
+
});
|
|
774
|
+
decodeBase64Url(input.ownershipSignature, 64, "e2ee_identity_signature_invalid");
|
|
775
|
+
return { ...unsigned, ownershipSignature: input.ownershipSignature };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function canonicalPrivatePreKey(input) {
|
|
779
|
+
assertExactFields(input, PRE_KEY_PRIVATE_FIELDS, "e2ee_private_prekey_invalid");
|
|
780
|
+
if (
|
|
781
|
+
input.schemaVersion !== DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION
|
|
782
|
+
|| input.purpose !== PRE_KEY_PURPOSE
|
|
783
|
+
|| input.algorithm !== "X25519"
|
|
784
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_private_prekey_invalid");
|
|
785
|
+
const publicDer = decodeBase64Url(input.publicKey, 44, "e2ee_prekey_public_invalid");
|
|
786
|
+
const privateKey = importPrivateDer(input.privateKey, "x25519", "e2ee_prekey_private_invalid");
|
|
787
|
+
const derivedPublicDer = createPublicKey(privateKey).export({ format: "der", type: "spki" });
|
|
788
|
+
if (!timingSafeEqual(publicDer, derivedPublicDer)) {
|
|
789
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_prekey_pair_mismatch");
|
|
790
|
+
}
|
|
791
|
+
const preKeyId = assertSafeId(input.preKeyId, "e2ee_prekey_id_invalid");
|
|
792
|
+
if (preKeyId !== keyIdForPublicDer("e2ee_pre_", publicDer)) {
|
|
793
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_prekey_id_invalid");
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
schemaVersion: input.schemaVersion,
|
|
797
|
+
purpose: input.purpose,
|
|
798
|
+
preKeyId,
|
|
799
|
+
algorithm: input.algorithm,
|
|
800
|
+
publicKey: input.publicKey,
|
|
801
|
+
privateKey: input.privateKey,
|
|
802
|
+
createdAt: assertIsoTimestamp(input.createdAt, "e2ee_prekey_created_at_invalid"),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function canonicalPublicPreKeyUnsigned(input) {
|
|
807
|
+
const allowed = PRE_KEY_PUBLIC_FIELDS.includes("signature") && Object.hasOwn(input, "signature")
|
|
808
|
+
? PRE_KEY_PUBLIC_FIELDS
|
|
809
|
+
: PRE_KEY_PUBLIC_UNSIGNED_FIELDS;
|
|
810
|
+
assertExactFields(input, allowed, "e2ee_public_prekey_invalid");
|
|
811
|
+
if (
|
|
812
|
+
input.schemaVersion !== DIRECT_TEXT_E2EE_KEY_SCHEMA_VERSION
|
|
813
|
+
|| input.algorithm !== "X25519"
|
|
814
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_public_prekey_invalid");
|
|
815
|
+
const publicDer = decodeBase64Url(input.publicKey, 44, "e2ee_prekey_public_invalid");
|
|
816
|
+
const preKeyId = assertSafeId(input.preKeyId, "e2ee_prekey_id_invalid");
|
|
817
|
+
if (preKeyId !== keyIdForPublicDer("e2ee_pre_", publicDer)) {
|
|
818
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_prekey_id_invalid");
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
schemaVersion: input.schemaVersion,
|
|
822
|
+
preKeyId,
|
|
823
|
+
identityKeyId: assertSafeId(input.identityKeyId, "e2ee_identity_key_id_invalid"),
|
|
824
|
+
algorithm: input.algorithm,
|
|
825
|
+
publicKey: input.publicKey,
|
|
826
|
+
createdAt: assertIsoTimestamp(input.createdAt, "e2ee_prekey_created_at_invalid"),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function canonicalPublicPreKey(input) {
|
|
831
|
+
assertExactFields(input, PRE_KEY_PUBLIC_FIELDS, "e2ee_public_prekey_invalid");
|
|
832
|
+
const unsigned = canonicalPublicPreKeyUnsigned({
|
|
833
|
+
schemaVersion: input.schemaVersion,
|
|
834
|
+
preKeyId: input.preKeyId,
|
|
835
|
+
identityKeyId: input.identityKeyId,
|
|
836
|
+
algorithm: input.algorithm,
|
|
837
|
+
publicKey: input.publicKey,
|
|
838
|
+
createdAt: input.createdAt,
|
|
839
|
+
});
|
|
840
|
+
decodeBase64Url(input.signature, 64, "e2ee_prekey_signature_invalid");
|
|
841
|
+
return { ...unsigned, signature: input.signature };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function canonicalProtectedMetadata(input) {
|
|
845
|
+
assertExactFields(input, PROTECTED_FIELDS, "e2ee_protected_metadata_invalid");
|
|
846
|
+
const accepted = {
|
|
847
|
+
clientMessageId: assertSafeId(input.clientMessageId, "e2ee_client_message_id_invalid"),
|
|
848
|
+
senderPrincipalId: assertSafeId(input.senderPrincipalId, "e2ee_sender_principal_id_invalid"),
|
|
849
|
+
senderHelloLynkId: assertSafeId(input.senderHelloLynkId, "e2ee_sender_hellolynk_id_invalid"),
|
|
850
|
+
senderAgentId: assertSafeId(input.senderAgentId, "e2ee_sender_agent_id_invalid"),
|
|
851
|
+
senderDelegateId: assertSafeId(input.senderDelegateId, "e2ee_sender_delegate_id_invalid"),
|
|
852
|
+
senderDeviceId: assertSafeId(input.senderDeviceId, "e2ee_sender_device_id_invalid"),
|
|
853
|
+
senderIdentityKeyId: assertSafeId(input.senderIdentityKeyId, "e2ee_sender_identity_key_id_invalid"),
|
|
854
|
+
recipientPrincipalId: assertSafeId(input.recipientPrincipalId, "e2ee_recipient_principal_id_invalid"),
|
|
855
|
+
recipientHelloLynkId: assertSafeId(input.recipientHelloLynkId, "e2ee_recipient_hellolynk_id_invalid"),
|
|
856
|
+
recipientDeviceId: assertSafeId(input.recipientDeviceId, "e2ee_recipient_device_id_invalid"),
|
|
857
|
+
recipientIdentityKeyId: assertSafeId(input.recipientIdentityKeyId, "e2ee_recipient_identity_key_id_invalid"),
|
|
858
|
+
recipientPreKeyId: assertSafeId(input.recipientPreKeyId, "e2ee_recipient_prekey_id_invalid"),
|
|
859
|
+
authorizationScope: input.authorizationScope,
|
|
860
|
+
intentSource: input.intentSource,
|
|
861
|
+
messageType: input.messageType,
|
|
862
|
+
createdAt: assertIsoTimestamp(input.createdAt, "e2ee_created_at_invalid"),
|
|
863
|
+
expiresAt: assertIsoTimestamp(input.expiresAt, "e2ee_expires_at_invalid"),
|
|
864
|
+
};
|
|
865
|
+
if (
|
|
866
|
+
accepted.authorizationScope !== DIRECT_TEXT_E2EE_AUTHORIZATION_SCOPE
|
|
867
|
+
|| !DIRECT_TEXT_E2EE_ALLOWED_INTENT_SOURCES.includes(accepted.intentSource)
|
|
868
|
+
|| accepted.messageType !== "text"
|
|
869
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_protected_metadata_invalid");
|
|
870
|
+
if (
|
|
871
|
+
accepted.senderPrincipalId === accepted.senderAgentId
|
|
872
|
+
|| accepted.senderPrincipalId === accepted.senderDelegateId
|
|
873
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_sender_identity_collapsed");
|
|
874
|
+
const createdAtMs = Date.parse(accepted.createdAt);
|
|
875
|
+
const expiresAtMs = Date.parse(accepted.expiresAt);
|
|
876
|
+
if (expiresAtMs <= createdAtMs || expiresAtMs - createdAtMs > DIRECT_TEXT_E2EE_MAX_TTL_MS) {
|
|
877
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_expiry_window_invalid");
|
|
878
|
+
}
|
|
879
|
+
return Object.freeze(accepted);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function canonicalProtectedMetadataV2(input) {
|
|
883
|
+
const replyFields = Object.hasOwn(input || {}, "replyToClientMessageId") ? ["replyToClientMessageId"] : [];
|
|
884
|
+
assertExactFields(input, [...PROTECTED_V2_FIELDS, ...replyFields], "e2ee_v2_protected_metadata_invalid");
|
|
885
|
+
const routeKind = String(input.routeKind || "");
|
|
886
|
+
if (!new Set(["principal_inbox", "owned_delegate", "instance_direct"]).has(routeKind)) {
|
|
887
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_route_kind_invalid");
|
|
888
|
+
}
|
|
889
|
+
const senderDelegateAuthorizationVersion = assertPositiveInteger(
|
|
890
|
+
input.senderDelegateAuthorizationVersion,
|
|
891
|
+
"e2ee_sender_delegate_authorization_version_invalid",
|
|
892
|
+
);
|
|
893
|
+
const recipientDelegateAuthorizationVersion = assertPositiveInteger(
|
|
894
|
+
input.recipientDelegateAuthorizationVersion,
|
|
895
|
+
"e2ee_recipient_delegate_authorization_version_invalid",
|
|
896
|
+
);
|
|
897
|
+
const recipientRouteVersion = assertNonNegativeInteger(
|
|
898
|
+
input.recipientRouteVersion,
|
|
899
|
+
"e2ee_recipient_route_version_invalid",
|
|
900
|
+
);
|
|
901
|
+
const accepted = {
|
|
902
|
+
clientMessageId: assertSafeId(input.clientMessageId, "e2ee_client_message_id_invalid"),
|
|
903
|
+
routeKind,
|
|
904
|
+
senderPrincipalId: assertSafeId(input.senderPrincipalId, "e2ee_sender_principal_id_invalid"),
|
|
905
|
+
senderHelloLynkId: assertSafeId(input.senderHelloLynkId, "e2ee_sender_hellolynk_id_invalid"),
|
|
906
|
+
senderAgentId: assertSafeId(input.senderAgentId, "e2ee_sender_agent_id_invalid"),
|
|
907
|
+
senderDelegateId: assertSafeId(input.senderDelegateId, "e2ee_sender_delegate_id_invalid"),
|
|
908
|
+
senderDelegateAuthorizationVersion,
|
|
909
|
+
senderHostId: assertSafeId(input.senderHostId, "e2ee_sender_host_id_invalid"),
|
|
910
|
+
senderDeviceId: assertSafeId(input.senderDeviceId, "e2ee_sender_device_id_invalid"),
|
|
911
|
+
senderIdentityKeyId: assertSafeId(input.senderIdentityKeyId, "e2ee_sender_identity_key_id_invalid"),
|
|
912
|
+
recipientPrincipalId: assertSafeId(input.recipientPrincipalId, "e2ee_recipient_principal_id_invalid"),
|
|
913
|
+
recipientHelloLynkId: assertSafeId(input.recipientHelloLynkId, "e2ee_recipient_hellolynk_id_invalid"),
|
|
914
|
+
recipientDelegateId: assertSafeId(input.recipientDelegateId, "e2ee_recipient_delegate_id_invalid"),
|
|
915
|
+
recipientDelegateAuthorizationVersion,
|
|
916
|
+
recipientHostId: assertSafeId(input.recipientHostId, "e2ee_recipient_host_id_invalid"),
|
|
917
|
+
recipientRouteVersion,
|
|
918
|
+
recipientDeviceId: assertSafeId(input.recipientDeviceId, "e2ee_recipient_device_id_invalid"),
|
|
919
|
+
recipientIdentityKeyId: assertSafeId(input.recipientIdentityKeyId, "e2ee_recipient_identity_key_id_invalid"),
|
|
920
|
+
recipientPreKeyId: assertSafeId(input.recipientPreKeyId, "e2ee_recipient_prekey_id_invalid"),
|
|
921
|
+
authorizationScope: input.authorizationScope,
|
|
922
|
+
intentSource: input.intentSource,
|
|
923
|
+
messageType: input.messageType,
|
|
924
|
+
createdAt: assertIsoTimestamp(input.createdAt, "e2ee_created_at_invalid"),
|
|
925
|
+
expiresAt: assertIsoTimestamp(input.expiresAt, "e2ee_expires_at_invalid"),
|
|
926
|
+
...(replyFields.length ? { replyToClientMessageId: assertSafeId(input.replyToClientMessageId, "e2ee_reply_target_invalid") } : {}),
|
|
927
|
+
};
|
|
928
|
+
if (replyFields.length && (routeKind === "principal_inbox" || accepted.intentSource !== "human_preauthorized_contact"
|
|
929
|
+
|| accepted.replyToClientMessageId === accepted.clientMessageId)) throw new HelloLynkDirectTextE2eeError("e2ee_reply_target_invalid");
|
|
930
|
+
if (
|
|
931
|
+
accepted.authorizationScope !== DIRECT_TEXT_E2EE_AUTHORIZATION_SCOPE
|
|
932
|
+
|| !DIRECT_TEXT_E2EE_ALLOWED_INTENT_SOURCES.includes(accepted.intentSource)
|
|
933
|
+
|| accepted.messageType !== "text"
|
|
934
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_v2_protected_metadata_invalid");
|
|
935
|
+
if (
|
|
936
|
+
accepted.senderPrincipalId === accepted.senderAgentId
|
|
937
|
+
|| accepted.senderPrincipalId === accepted.senderDelegateId
|
|
938
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_sender_identity_collapsed");
|
|
939
|
+
if (
|
|
940
|
+
(accepted.senderPrincipalId === accepted.recipientPrincipalId)
|
|
941
|
+
!== (accepted.senderHelloLynkId === accepted.recipientHelloLynkId)
|
|
942
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_route_semantics_invalid");
|
|
943
|
+
if (
|
|
944
|
+
(routeKind === "principal_inbox" && (
|
|
945
|
+
accepted.senderPrincipalId === accepted.recipientPrincipalId
|
|
946
|
+
|| accepted.recipientRouteVersion < 1
|
|
947
|
+
))
|
|
948
|
+
|| (routeKind === "owned_delegate" && (
|
|
949
|
+
accepted.senderPrincipalId !== accepted.recipientPrincipalId
|
|
950
|
+
|| accepted.senderDelegateId === accepted.recipientDelegateId
|
|
951
|
+
|| accepted.recipientRouteVersion !== 0
|
|
952
|
+
))
|
|
953
|
+
|| (routeKind === "instance_direct" && (
|
|
954
|
+
accepted.senderDelegateId === accepted.recipientDelegateId
|
|
955
|
+
|| accepted.recipientRouteVersion !== 0
|
|
956
|
+
))
|
|
957
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_route_semantics_invalid");
|
|
958
|
+
const createdAtMs = Date.parse(accepted.createdAt);
|
|
959
|
+
const expiresAtMs = Date.parse(accepted.expiresAt);
|
|
960
|
+
if (expiresAtMs <= createdAtMs || expiresAtMs - createdAtMs > DIRECT_TEXT_E2EE_MAX_TTL_MS) {
|
|
961
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_expiry_window_invalid");
|
|
962
|
+
}
|
|
963
|
+
return Object.freeze(accepted);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function canonicalUnsignedEnvelope(input) {
|
|
967
|
+
const unsignedFields = ENVELOPE_FIELDS.slice(0, -1);
|
|
968
|
+
const source = Object.hasOwn(input, "signature")
|
|
969
|
+
? {
|
|
970
|
+
schemaVersion: input.schemaVersion,
|
|
971
|
+
protocol: input.protocol,
|
|
972
|
+
algorithm: input.algorithm,
|
|
973
|
+
protected: input.protected,
|
|
974
|
+
ephemeralPublicKey: input.ephemeralPublicKey,
|
|
975
|
+
salt: input.salt,
|
|
976
|
+
iv: input.iv,
|
|
977
|
+
ciphertext: input.ciphertext,
|
|
978
|
+
authenticationTag: input.authenticationTag,
|
|
979
|
+
}
|
|
980
|
+
: input;
|
|
981
|
+
assertExactFields(source, unsignedFields, "e2ee_envelope_invalid");
|
|
982
|
+
if (
|
|
983
|
+
source.schemaVersion !== DIRECT_TEXT_E2EE_SCHEMA_VERSION
|
|
984
|
+
|| source.protocol !== DIRECT_TEXT_E2EE_PROTOCOL
|
|
985
|
+
|| source.algorithm !== DIRECT_TEXT_E2EE_ALGORITHM
|
|
986
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_envelope_algorithm_invalid");
|
|
987
|
+
decodeBase64Url(source.ephemeralPublicKey, 44, "e2ee_ephemeral_public_key_invalid");
|
|
988
|
+
decodeBase64Url(source.salt, 32, "e2ee_envelope_salt_invalid");
|
|
989
|
+
decodeBase64Url(source.iv, 12, "e2ee_envelope_iv_invalid");
|
|
990
|
+
decodeBase64Url(source.ciphertext, null, "e2ee_envelope_ciphertext_invalid", {
|
|
991
|
+
minimumBytes: 1,
|
|
992
|
+
maximumBytes: MAX_CIPHERTEXT_BYTES,
|
|
993
|
+
});
|
|
994
|
+
decodeBase64Url(source.authenticationTag, 16, "e2ee_envelope_authentication_tag_invalid");
|
|
995
|
+
return {
|
|
996
|
+
schemaVersion: source.schemaVersion,
|
|
997
|
+
protocol: source.protocol,
|
|
998
|
+
algorithm: source.algorithm,
|
|
999
|
+
protected: canonicalProtectedMetadata(source.protected),
|
|
1000
|
+
ephemeralPublicKey: source.ephemeralPublicKey,
|
|
1001
|
+
salt: source.salt,
|
|
1002
|
+
iv: source.iv,
|
|
1003
|
+
ciphertext: source.ciphertext,
|
|
1004
|
+
authenticationTag: source.authenticationTag,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function canonicalUnsignedEnvelopeV2(input) {
|
|
1009
|
+
const unsignedFields = ENVELOPE_FIELDS.slice(0, -1);
|
|
1010
|
+
const source = Object.hasOwn(input, "signature")
|
|
1011
|
+
? {
|
|
1012
|
+
schemaVersion: input.schemaVersion,
|
|
1013
|
+
protocol: input.protocol,
|
|
1014
|
+
algorithm: input.algorithm,
|
|
1015
|
+
protected: input.protected,
|
|
1016
|
+
ephemeralPublicKey: input.ephemeralPublicKey,
|
|
1017
|
+
salt: input.salt,
|
|
1018
|
+
iv: input.iv,
|
|
1019
|
+
ciphertext: input.ciphertext,
|
|
1020
|
+
authenticationTag: input.authenticationTag,
|
|
1021
|
+
}
|
|
1022
|
+
: input;
|
|
1023
|
+
assertExactFields(source, unsignedFields, "e2ee_v2_envelope_invalid");
|
|
1024
|
+
if (
|
|
1025
|
+
source.schemaVersion !== DIRECT_TEXT_E2EE_V2_SCHEMA_VERSION
|
|
1026
|
+
|| source.protocol !== DIRECT_TEXT_E2EE_V2_PROTOCOL
|
|
1027
|
+
|| source.algorithm !== DIRECT_TEXT_E2EE_ALGORITHM
|
|
1028
|
+
) throw new HelloLynkDirectTextE2eeError("e2ee_v2_envelope_algorithm_invalid");
|
|
1029
|
+
decodeBase64Url(source.ephemeralPublicKey, 44, "e2ee_ephemeral_public_key_invalid");
|
|
1030
|
+
decodeBase64Url(source.salt, 32, "e2ee_envelope_salt_invalid");
|
|
1031
|
+
decodeBase64Url(source.iv, 12, "e2ee_envelope_iv_invalid");
|
|
1032
|
+
decodeBase64Url(source.ciphertext, null, "e2ee_envelope_ciphertext_invalid", {
|
|
1033
|
+
minimumBytes: 1,
|
|
1034
|
+
maximumBytes: MAX_CIPHERTEXT_BYTES,
|
|
1035
|
+
});
|
|
1036
|
+
decodeBase64Url(source.authenticationTag, 16, "e2ee_envelope_authentication_tag_invalid");
|
|
1037
|
+
return {
|
|
1038
|
+
schemaVersion: source.schemaVersion,
|
|
1039
|
+
protocol: source.protocol,
|
|
1040
|
+
algorithm: source.algorithm,
|
|
1041
|
+
protected: canonicalProtectedMetadataV2(source.protected),
|
|
1042
|
+
ephemeralPublicKey: source.ephemeralPublicKey,
|
|
1043
|
+
salt: source.salt,
|
|
1044
|
+
iv: source.iv,
|
|
1045
|
+
ciphertext: source.ciphertext,
|
|
1046
|
+
authenticationTag: source.authenticationTag,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function canonicalEnvelope(input) {
|
|
1051
|
+
assertExactFields(input, ENVELOPE_FIELDS, "e2ee_envelope_invalid");
|
|
1052
|
+
const unsigned = canonicalUnsignedEnvelope(input);
|
|
1053
|
+
decodeBase64Url(input.signature, 64, "e2ee_envelope_signature_invalid");
|
|
1054
|
+
return { ...unsigned, signature: input.signature };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function canonicalEnvelopeV2(input) {
|
|
1058
|
+
assertExactFields(input, ENVELOPE_FIELDS, "e2ee_v2_envelope_invalid");
|
|
1059
|
+
const unsigned = canonicalUnsignedEnvelopeV2(input);
|
|
1060
|
+
decodeBase64Url(input.signature, 64, "e2ee_envelope_signature_invalid");
|
|
1061
|
+
return { ...unsigned, signature: input.signature };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function canonicalExpectedRecipient(input) {
|
|
1065
|
+
assertExactFields(input, EXPECTED_RECIPIENT_FIELDS, "e2ee_expected_recipient_invalid");
|
|
1066
|
+
return {
|
|
1067
|
+
principalId: assertSafeId(input.principalId, "e2ee_expected_recipient_invalid"),
|
|
1068
|
+
hellolynkId: assertSafeId(input.hellolynkId, "e2ee_expected_recipient_invalid"),
|
|
1069
|
+
deviceId: assertSafeId(input.deviceId, "e2ee_expected_recipient_invalid"),
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function canonicalExpectedRecipientV2(input) {
|
|
1074
|
+
assertExactFields(input, EXPECTED_RECIPIENT_V2_FIELDS, "e2ee_v2_expected_recipient_invalid");
|
|
1075
|
+
return {
|
|
1076
|
+
principalId: assertSafeId(input.principalId, "e2ee_v2_expected_recipient_invalid"),
|
|
1077
|
+
hellolynkId: assertSafeId(input.hellolynkId, "e2ee_v2_expected_recipient_invalid"),
|
|
1078
|
+
delegateId: assertSafeId(input.delegateId, "e2ee_v2_expected_recipient_invalid"),
|
|
1079
|
+
hostId: assertSafeId(input.hostId, "e2ee_v2_expected_recipient_invalid"),
|
|
1080
|
+
deviceId: assertSafeId(input.deviceId, "e2ee_v2_expected_recipient_invalid"),
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function assertEnvelopeTime(protectedMetadata, nowMs) {
|
|
1085
|
+
const createdAtMs = Date.parse(protectedMetadata.createdAt);
|
|
1086
|
+
const expiresAtMs = Date.parse(protectedMetadata.expiresAt);
|
|
1087
|
+
if (createdAtMs > nowMs + MAX_CLOCK_SKEW_MS) {
|
|
1088
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_created_at_in_future");
|
|
1089
|
+
}
|
|
1090
|
+
if (expiresAtMs <= nowMs) throw new HelloLynkDirectTextE2eeError("e2ee_envelope_expired");
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function assertText(value) {
|
|
1094
|
+
if (typeof value !== "string") throw new HelloLynkDirectTextE2eeError("e2ee_text_invalid");
|
|
1095
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
1096
|
+
if (bytes < 1 || bytes > DIRECT_TEXT_E2EE_MAX_TEXT_UTF8_BYTES) {
|
|
1097
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_text_invalid");
|
|
1098
|
+
}
|
|
1099
|
+
return value;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function normalizeNowOptions(options, code) {
|
|
1103
|
+
assertPlainRecord(options, code);
|
|
1104
|
+
const keys = Object.keys(options);
|
|
1105
|
+
if (keys.some((key) => key !== "nowMs")) throw new HelloLynkDirectTextE2eeError(code);
|
|
1106
|
+
const nowMs = options.nowMs == null ? Date.now() : Number(options.nowMs);
|
|
1107
|
+
if (!Number.isSafeInteger(nowMs) || nowMs <= 0) throw new HelloLynkDirectTextE2eeError(code);
|
|
1108
|
+
return nowMs;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function assertIsoTimestamp(value, code) {
|
|
1112
|
+
if (typeof value !== "string") throw new HelloLynkDirectTextE2eeError(code);
|
|
1113
|
+
const parsed = Date.parse(value);
|
|
1114
|
+
if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
|
|
1115
|
+
throw new HelloLynkDirectTextE2eeError(code);
|
|
1116
|
+
}
|
|
1117
|
+
return value;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function assertSafeId(value, code) {
|
|
1121
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$/.test(value)) {
|
|
1122
|
+
throw new HelloLynkDirectTextE2eeError(code);
|
|
1123
|
+
}
|
|
1124
|
+
return value;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function assertPositiveInteger(value, code) {
|
|
1128
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new HelloLynkDirectTextE2eeError(code);
|
|
1129
|
+
return value;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function assertNonNegativeInteger(value, code) {
|
|
1133
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new HelloLynkDirectTextE2eeError(code);
|
|
1134
|
+
return value;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function assertDigest(value) {
|
|
1138
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
|
|
1139
|
+
throw new HelloLynkDirectTextE2eeError("e2ee_envelope_digest_invalid");
|
|
1140
|
+
}
|
|
1141
|
+
return value;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function assertPlainRecord(value, code) {
|
|
1145
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1146
|
+
throw new HelloLynkDirectTextE2eeError(code);
|
|
1147
|
+
}
|
|
1148
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1149
|
+
if (prototype !== Object.prototype && prototype !== null) throw new HelloLynkDirectTextE2eeError(code);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function assertExactFields(value, expectedFields, code) {
|
|
1153
|
+
assertPlainRecord(value, code);
|
|
1154
|
+
const actualFields = Object.keys(value);
|
|
1155
|
+
if (
|
|
1156
|
+
actualFields.length !== expectedFields.length
|
|
1157
|
+
|| expectedFields.some((field) => !Object.hasOwn(value, field))
|
|
1158
|
+
) throw new HelloLynkDirectTextE2eeError(code);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function decodeBase64Url(value, expectedBytes, code, limits = {}) {
|
|
1162
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
1163
|
+
throw new HelloLynkDirectTextE2eeError(code);
|
|
1164
|
+
}
|
|
1165
|
+
const decoded = Buffer.from(value, "base64url");
|
|
1166
|
+
if (decoded.toString("base64url") !== value) throw new HelloLynkDirectTextE2eeError(code);
|
|
1167
|
+
if (expectedBytes != null && decoded.length !== expectedBytes) throw new HelloLynkDirectTextE2eeError(code);
|
|
1168
|
+
if (limits.minimumBytes != null && decoded.length < limits.minimumBytes) {
|
|
1169
|
+
throw new HelloLynkDirectTextE2eeError(code);
|
|
1170
|
+
}
|
|
1171
|
+
if (limits.maximumBytes != null && decoded.length > limits.maximumBytes) {
|
|
1172
|
+
throw new HelloLynkDirectTextE2eeError(code);
|
|
1173
|
+
}
|
|
1174
|
+
return decoded;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function importPrivateDer(value, asymmetricKeyType, code) {
|
|
1178
|
+
try {
|
|
1179
|
+
const der = decodeBase64Url(value, 48, code);
|
|
1180
|
+
const key = createPrivateKey({ key: der, format: "der", type: "pkcs8" });
|
|
1181
|
+
if (key.asymmetricKeyType !== asymmetricKeyType) throw new Error("type");
|
|
1182
|
+
return key;
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
if (error instanceof HelloLynkDirectTextE2eeError) throw error;
|
|
1185
|
+
throw new HelloLynkDirectTextE2eeError(code);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function importPublicDer(value, asymmetricKeyType, code) {
|
|
1190
|
+
try {
|
|
1191
|
+
const der = decodeBase64Url(value, 44, code);
|
|
1192
|
+
const key = createPublicKey({ key: der, format: "der", type: "spki" });
|
|
1193
|
+
if (key.asymmetricKeyType !== asymmetricKeyType) throw new Error("type");
|
|
1194
|
+
return key;
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
if (error instanceof HelloLynkDirectTextE2eeError) throw error;
|
|
1197
|
+
throw new HelloLynkDirectTextE2eeError(code);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function keyIdForPublicDer(prefix, publicDer) {
|
|
1202
|
+
return `${prefix}${createHash("sha256").update(publicDer).digest("base64url")}`;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function domainSeparatedBytes(domain, value) {
|
|
1206
|
+
return Buffer.from(`${domain}${JSON.stringify(value)}`, "utf8");
|
|
1207
|
+
}
|