mixdog 1.3.3 → 1.3.5
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/LICENSES/oh-my-pi-MIT.txt +23 -0
- package/NOTICE.md +15 -0
- package/README.md +11 -0
- package/package.json +3 -2
- package/scripts/embed-google-oauth-client.mjs +49 -0
- package/scripts/lib/test-spawn-ledger.mjs +46 -1
- package/scripts/test.mjs +5 -0
- package/src/defaults/shipped-history.json +8 -0
- package/src/defaults/skills/browser-use/SKILL.md +21 -9
- package/src/defaults/skills/setup/SKILL.md +7 -2
- package/src/defaults/skills/setup/references/actions.md +82 -27
- package/src/defaults/skills/setup/references/surfaces.md +67 -34
- package/src/defaults/skills/setup-guide/SKILL.md +3 -3
- package/src/lib/keychain-cjs.cjs +359 -29
- package/src/rules/agent-mode/AGENT_MODE.md +10 -2
- package/src/rules/agent-mode/MIXY.md +12 -0
- package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
- package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
- package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
- package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
- package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
- package/src/runtime/channels/lib/scheduler.mjs +109 -14
- package/src/runtime/channels/lib/schedules-db.mjs +49 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
- package/src/runtime/channels/lib/worker-main.mjs +9 -0
- package/src/runtime/office/com/com-adapter.mjs +17 -1
- package/src/runtime/office/com/office-com-host.ps1 +51 -1
- package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
- package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
- package/src/runtime/office/pdf/document-preview.mjs +326 -8
- package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
- package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
- package/src/runtime/office/portable/portable-soffice.mjs +45 -5
- package/src/runtime/shared/agent-room-host.mjs +1 -1
- package/src/runtime/shared/bot-hops.mjs +62 -2
- package/src/runtime/shared/config.mjs +49 -22
- package/src/runtime/shared/connector-catalog.d.mts +4 -2
- package/src/runtime/shared/connector-catalog.mjs +16 -8
- package/src/runtime/shared/developer-options.mjs +32 -1
- package/src/runtime/shared/google-oauth-client.generated.json +4 -0
- package/src/runtime/shared/google-oauth-client.mjs +34 -9
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
- package/src/runtime/shared/llm/well-formed.mjs +66 -0
- package/src/runtime/shared/provider-accounts.mjs +224 -58
- package/src/runtime/shared/provider-api-key.mjs +19 -8
- package/src/runtime/shared/routine-trigger.mjs +42 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -0
- package/src/runtime/shared/transcript-metadata.mjs +38 -0
- package/src/runtime/shared/user-data-guard.mjs +30 -2
- package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
- package/src/session-runtime/agent-home-delivery.mjs +57 -13
- package/src/session-runtime/agent-room-delivery.mjs +28 -1
- package/src/session-runtime/agent-task-sessions.mjs +292 -79
- package/src/session-runtime/agent-tool-routing.mjs +49 -4
- package/src/session-runtime/boot/begin.mjs +3 -2
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/bot-chain.mjs +32 -0
- package/src/session-runtime/bot-status.mjs +7 -4
- package/src/session-runtime/channel-config-api.mjs +63 -17
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
- package/src/session-runtime/internal-tool-executor.mjs +4 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
- package/src/session-runtime/mixy-heartbeat.mjs +807 -0
- package/src/session-runtime/model-route/route-tuning.mjs +10 -6
- package/src/session-runtime/owned-automation-run.mjs +33 -12
- package/src/session-runtime/provider-auth/login.mjs +7 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
- package/src/session-runtime/routine-home-turn.mjs +33 -6
- package/src/session-runtime/schedule-session-run.mjs +7 -3
- package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
- package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
- package/src/session-runtime/services/agent-tool/render.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
- package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
- package/src/session-runtime/services/channel-admin.mjs +66 -18
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +128 -25
- package/src/session-runtime/services/schedule-access.mjs +14 -0
- package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
- package/src/session-runtime/session-create/session-options.mjs +7 -4
- package/src/session-runtime/settings-system-api.mjs +16 -1
- package/src/session-runtime/setup-tool/executor.mjs +49 -14
- package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
- package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
- package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
- package/src/session-runtime/task-session-route.mjs +99 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
- package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
- package/src/session-runtime/tool-surface.mjs +4 -0
- package/src/session-runtime/usage-stats-api.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
- package/src/session-runtime/workspace/transitions.mjs +10 -2
- package/src/standalone/daemon-session-control.mjs +88 -20
- package/src/standalone/daemon.mjs +8 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
- package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
- package/src/standalone/session-service.mjs +13 -0
- package/src/standalone/session-tool-output.mjs +8 -5
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/dist/index.mjs +335 -172
- package/src/tui/session/notice-surface.mjs +17 -1
- package/src/tui/session/session-api/integrations.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +8 -0
- package/src/tui/session/session-api/settings.mjs +14 -2
- package/src/tui/session/turn-stream.mjs +17 -1
- package/src/tui/session/turn.mjs +22 -2
- package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
- package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
- package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// Opening an encrypted Office Open XML package with its password, exactly as [MS-OFFCRYPTO] 2.3.4 specifies it.
|
|
2
|
+
//
|
|
3
|
+
// The \EncryptionInfo stream says how the package was encrypted and carries a password verifier: agile encryption
|
|
4
|
+
// (2.3.4.10) with an XML descriptor, or standard encryption (2.3.4.5) with a binary header. The verifier decides
|
|
5
|
+
// whether a password is right before anything else happens (2.3.4.9, 2.3.4.13); for agile encryption the
|
|
6
|
+
// descriptor's HMAC then has to prove \EncryptedPackage intact (2.3.4.14), and only then does the key decrypt it
|
|
7
|
+
// back into the plain package (2.3.4.15). Anything outside those two schemes (extensible encryption, a cipher or
|
|
8
|
+
// chaining mode the spec allows but Office does not write) is "unsupported", never a guess. The password hashing
|
|
9
|
+
// (up to 10,000,000 rounds) runs on a worker thread that a cancelled request ends.
|
|
10
|
+
import { createDecipheriv, createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
11
|
+
import { Worker } from 'node:worker_threads';
|
|
12
|
+
import { SaxesParser } from 'saxes';
|
|
13
|
+
import { workerExecArgv } from '../../shared/worker-exec-argv.mjs';
|
|
14
|
+
|
|
15
|
+
// The agile descriptor is XML, read with saxes (the namespace-aware parser jsdom also parses XML with). saxes
|
|
16
|
+
// resolves no external entity and knows only the predefined ones; a DTD is refused outright (descriptorFields).
|
|
17
|
+
|
|
18
|
+
const ENCRYPTION_NAMESPACE = 'http://schemas.microsoft.com/office/2006/encryption';
|
|
19
|
+
const PASSWORD_KEY_ENCRYPTOR = 'http://schemas.microsoft.com/office/2006/keyEncryptor/password';
|
|
20
|
+
// 2.3.4.13: the block keys of the verifier input, the verifier hash and the encrypted key.
|
|
21
|
+
const VERIFIER_INPUT_BLOCK = Buffer.from([0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, 0x79]);
|
|
22
|
+
const VERIFIER_HASH_BLOCK = Buffer.from([0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, 0x4e]);
|
|
23
|
+
const ENCRYPTED_KEY_BLOCK = Buffer.from([0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6]);
|
|
24
|
+
// 2.3.4.14: the block keys of the HMAC key and the HMAC value.
|
|
25
|
+
const INTEGRITY_KEY_BLOCK = Buffer.from([0x5f, 0xb2, 0xad, 0x01, 0x0c, 0xb9, 0xe1, 0xf6]);
|
|
26
|
+
const INTEGRITY_VALUE_BLOCK = Buffer.from([0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, 0x33]);
|
|
27
|
+
// 2.3.4.10: spinCount MUST NOT exceed 10,000,000.
|
|
28
|
+
const MAX_SPIN_COUNT = 10_000_000;
|
|
29
|
+
// 2.3.4.7: standard encryption hashes the password 50,000 times.
|
|
30
|
+
const STANDARD_SPIN_COUNT = 50_000;
|
|
31
|
+
const SEGMENT_LENGTH = 4096;
|
|
32
|
+
// 2.3.4.10 HashAlgorithm: the values the table defines, by their OpenSSL names. "SHA1" is not in the table; it is
|
|
33
|
+
// the name Windows resolves SHA-1 by (as it resolves SHA256, SHA384 and SHA512), which footnote <21> allows: "any
|
|
34
|
+
// algorithm that can be resolved by name by the underlying operating system can be used for hashing". A defined
|
|
35
|
+
// algorithm this runtime cannot compute (MD2, RIPEMD-128, ...) leaves the document unsupported.
|
|
36
|
+
const HASH_ALGORITHMS = new Map([
|
|
37
|
+
['SHA-1', 'sha1'],
|
|
38
|
+
['SHA1', 'sha1'],
|
|
39
|
+
['SHA256', 'sha256'],
|
|
40
|
+
['SHA384', 'sha384'],
|
|
41
|
+
['SHA512', 'sha512'],
|
|
42
|
+
['MD5', 'md5'],
|
|
43
|
+
['MD4', 'md4'],
|
|
44
|
+
['MD2', 'md2'],
|
|
45
|
+
['RIPEMD-128', 'ripemd128'],
|
|
46
|
+
['RIPEMD-160', 'ripemd160'],
|
|
47
|
+
['WHIRLPOOL', 'whirlpool'],
|
|
48
|
+
]);
|
|
49
|
+
const AES_ALGORITHMS = new Map([
|
|
50
|
+
[0x660e, 128],
|
|
51
|
+
[0x660f, 192],
|
|
52
|
+
[0x6610, 256],
|
|
53
|
+
]);
|
|
54
|
+
const STANDARD_FLAG_CRYPTOAPI = 0x04;
|
|
55
|
+
const STANDARD_FLAG_AES = 0x20;
|
|
56
|
+
|
|
57
|
+
const hash = (algorithm, ...parts) => {
|
|
58
|
+
const digest = createHash(algorithm);
|
|
59
|
+
for (const part of parts) digest.update(part);
|
|
60
|
+
return digest.digest();
|
|
61
|
+
};
|
|
62
|
+
const uint32 = (value) => {
|
|
63
|
+
const bytes = Buffer.alloc(4);
|
|
64
|
+
bytes.writeUInt32LE(value >>> 0, 0);
|
|
65
|
+
return bytes;
|
|
66
|
+
};
|
|
67
|
+
// Truncated to, or padded with 0x36 up to, `length` bytes (2.3.4.11, 2.3.4.12).
|
|
68
|
+
const fitted = (bytes, length) =>
|
|
69
|
+
bytes.length >= length
|
|
70
|
+
? bytes.subarray(0, length)
|
|
71
|
+
: Buffer.concat([bytes, Buffer.alloc(length - bytes.length, 0x36)]);
|
|
72
|
+
|
|
73
|
+
function decrypt(mode, keyBits, key, iv, data) {
|
|
74
|
+
const decipher = createDecipheriv(`aes-${keyBits}-${mode}`, key, iv);
|
|
75
|
+
decipher.setAutoPadding(false);
|
|
76
|
+
return Buffer.concat([decipher.update(data), decipher.final()]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The digest length of an OpenSSL hash, or null when this runtime cannot compute it.
|
|
80
|
+
const digestLengths = new Map();
|
|
81
|
+
function digestLength(algorithm) {
|
|
82
|
+
if (!digestLengths.has(algorithm)) {
|
|
83
|
+
let length = null;
|
|
84
|
+
try {
|
|
85
|
+
length = createHash(algorithm).digest().length;
|
|
86
|
+
} catch {}
|
|
87
|
+
digestLengths.set(algorithm, length);
|
|
88
|
+
}
|
|
89
|
+
return digestLengths.get(algorithm);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// [W3C-XSD] lexical forms of the attribute types the descriptor uses.
|
|
93
|
+
const XML_SPACE = /[\t\n\r ]+/g;
|
|
94
|
+
// xs:unsignedInt (whitespace collapsed): decimal digits with an optional "+", at most 4,294,967,295.
|
|
95
|
+
function unsignedInt(value) {
|
|
96
|
+
const text = typeof value === 'string' ? value.replace(XML_SPACE, ' ').trim() : '';
|
|
97
|
+
if (!/^\+?[0-9]+$/.test(text)) return null;
|
|
98
|
+
const number = Number(text);
|
|
99
|
+
return number <= 0xffffffff ? number : null;
|
|
100
|
+
}
|
|
101
|
+
// xs:base64Binary: whole quads of the base64 alphabet (whitespace between them allowed), the last one padded with
|
|
102
|
+
// "=" over the unused bits, which must be zero.
|
|
103
|
+
function base64Binary(value) {
|
|
104
|
+
const text = typeof value === 'string' ? value.replace(XML_SPACE, '') : '';
|
|
105
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=)?$/.test(text)) return null;
|
|
106
|
+
return Buffer.from(text, 'base64');
|
|
107
|
+
}
|
|
108
|
+
// xs:token: whitespace collapsed.
|
|
109
|
+
const token = (value) => (typeof value === 'string' ? value.replace(XML_SPACE, ' ').trim() : null);
|
|
110
|
+
|
|
111
|
+
// The unqualified attributes of an element (the descriptor's schemas set attributeFormDefault="unqualified").
|
|
112
|
+
function unqualified(tag) {
|
|
113
|
+
const values = Object.create(null);
|
|
114
|
+
for (const attribute of Object.values(tag.attributes)) {
|
|
115
|
+
if (attribute.uri === '') values[attribute.local] = attribute.value;
|
|
116
|
+
}
|
|
117
|
+
return values;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// CT_Encryption is this sequence: keyData, an optional dataIntegrity, keyEncryptors.
|
|
121
|
+
const ENCRYPTION_SEQUENCE = ['keyData', 'dataIntegrity', 'keyEncryptors'];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The attributes of keyData, dataIntegrity (null when absent) and the password encryptedKey of an
|
|
125
|
+
* XmlEncryptionDescriptor (2.3.4.10), read by namespace: the root is encryption, its children follow the
|
|
126
|
+
* CT_Encryption sequence, keyEncryptors holds at least one keyEncryptor of exactly one element each, and exactly one
|
|
127
|
+
* of them is the password key encryptor (uri "…/keyEncryptor/password") holding the encryptedKey of that namespace.
|
|
128
|
+
* Throws for a DTD, malformed XML, an encoding other than UTF-8, or any other structure where these fields live.
|
|
129
|
+
*/
|
|
130
|
+
function descriptorFields(xml) {
|
|
131
|
+
const parser = new SaxesParser({ xmlns: true, position: false });
|
|
132
|
+
const fields = { keyData: null, dataIntegrity: null, encryptedKey: null };
|
|
133
|
+
const open = [];
|
|
134
|
+
let position = -1;
|
|
135
|
+
let keyEncryptors = 0;
|
|
136
|
+
let passwordKeyEncryptors = 0;
|
|
137
|
+
parser.on('xmldecl', ({ encoding }) => {
|
|
138
|
+
if (encoding !== undefined && encoding.toLowerCase() !== 'utf-8') throw new Error('descriptor is not UTF-8');
|
|
139
|
+
});
|
|
140
|
+
parser.on('doctype', () => {
|
|
141
|
+
throw new Error('a DTD is not allowed in the descriptor');
|
|
142
|
+
});
|
|
143
|
+
parser.on('opentag', (tag) => {
|
|
144
|
+
const parent = open.at(-1);
|
|
145
|
+
const encryption = tag.uri === ENCRYPTION_NAMESPACE;
|
|
146
|
+
let kind;
|
|
147
|
+
if (!parent) {
|
|
148
|
+
if (!encryption || tag.local !== 'encryption') throw new Error('not an encryption descriptor');
|
|
149
|
+
kind = 'encryption';
|
|
150
|
+
} else if (parent.kind === 'encryption') {
|
|
151
|
+
const at = ENCRYPTION_SEQUENCE.indexOf(tag.local);
|
|
152
|
+
if (!encryption || at <= position) throw new Error('unexpected element in encryption');
|
|
153
|
+
position = at;
|
|
154
|
+
kind = tag.local;
|
|
155
|
+
if (kind !== 'keyEncryptors') fields[kind] = unqualified(tag);
|
|
156
|
+
} else if (parent.kind === 'keyEncryptors') {
|
|
157
|
+
if (!encryption || tag.local !== 'keyEncryptor') throw new Error('unexpected element in keyEncryptors');
|
|
158
|
+
keyEncryptors++;
|
|
159
|
+
const password = token(unqualified(tag).uri) === PASSWORD_KEY_ENCRYPTOR;
|
|
160
|
+
if (password) passwordKeyEncryptors++;
|
|
161
|
+
kind = password ? 'passwordKeyEncryptor' : 'keyEncryptor';
|
|
162
|
+
} else if (parent.kind === 'passwordKeyEncryptor' || parent.kind === 'keyEncryptor') {
|
|
163
|
+
if (++parent.children > 1) throw new Error('a keyEncryptor holds one element');
|
|
164
|
+
if (parent.kind === 'passwordKeyEncryptor') {
|
|
165
|
+
if (tag.uri !== PASSWORD_KEY_ENCRYPTOR || tag.local !== 'encryptedKey') throw new Error('not a password key');
|
|
166
|
+
fields.encryptedKey = unqualified(tag);
|
|
167
|
+
kind = 'encryptedKey';
|
|
168
|
+
} else {
|
|
169
|
+
kind = 'other';
|
|
170
|
+
}
|
|
171
|
+
} else if (parent.kind === 'other') {
|
|
172
|
+
kind = 'other';
|
|
173
|
+
} else {
|
|
174
|
+
// keyData, dataIntegrity and encryptedKey are attributes only.
|
|
175
|
+
throw new Error(`${parent.kind} has no child elements`);
|
|
176
|
+
}
|
|
177
|
+
open.push({ kind, children: 0 });
|
|
178
|
+
});
|
|
179
|
+
parser.on('closetag', () => {
|
|
180
|
+
const closed = open.pop();
|
|
181
|
+
if ((closed.kind === 'keyEncryptor' || closed.kind === 'passwordKeyEncryptor') && closed.children !== 1) {
|
|
182
|
+
throw new Error('a keyEncryptor holds one element');
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
parser.write(xml).close();
|
|
186
|
+
if (!fields.keyData || position !== 2 || keyEncryptors < 1 || passwordKeyEncryptors !== 1) {
|
|
187
|
+
throw new Error('incomplete encryption descriptor');
|
|
188
|
+
}
|
|
189
|
+
return fields;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// One cipher description (keyData, or the password encryptedKey), or null when it is not AES-CBC with a hash this
|
|
193
|
+
// runtime computes, or its values do not match their schema types.
|
|
194
|
+
function agileCipher(values) {
|
|
195
|
+
const keyBits = unsignedInt(values.keyBits);
|
|
196
|
+
const blockSize = unsignedInt(values.blockSize);
|
|
197
|
+
const saltSize = unsignedInt(values.saltSize);
|
|
198
|
+
const hashSize = unsignedInt(values.hashSize);
|
|
199
|
+
const algorithm = HASH_ALGORITHMS.get(values.hashAlgorithm);
|
|
200
|
+
const salt = base64Binary(values.saltValue);
|
|
201
|
+
if (values.cipherAlgorithm !== 'AES' || values.cipherChaining !== 'ChainingModeCBC') return null;
|
|
202
|
+
if (![128, 192, 256].includes(keyBits) || blockSize !== 16 || !algorithm) return null;
|
|
203
|
+
if (digestLength(algorithm) !== hashSize || !salt || saltSize < 1 || salt.length !== saltSize) return null;
|
|
204
|
+
return { keyBits, blockSize, saltSize, hashSize, algorithm, salt };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// base64Binary ciphertext of whole cipher blocks holding at least `least` bytes, or null.
|
|
208
|
+
function ciphertext(value, blockSize, least) {
|
|
209
|
+
const bytes = base64Binary(value);
|
|
210
|
+
return bytes && bytes.length >= least && bytes.length % blockSize === 0 ? bytes : null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const wholeBlocks = (length, blockSize) => Math.ceil(length / blockSize) * blockSize;
|
|
214
|
+
|
|
215
|
+
// 2.3.4.14: the encrypted salt (the HMAC key) and the encrypted HMAC (hashSize bytes), each padded with 0x00 to
|
|
216
|
+
// whole blocks. Step 2 makes the salt KeyData.saltSize bytes long. Excel writes hashSize bytes instead (a file it
|
|
217
|
+
// saves holds saltSize 16, hashSize 64 and a 64-byte encryptedHmacKey whose bytes past the 16th are not padding),
|
|
218
|
+
// as Apache POI and msoffcrypto-tool do. The padded length says which of the two a file holds; where both pad to
|
|
219
|
+
// the same size the longer is taken, because the shorter one's further bytes are its 0x00 padding and [RFC2104]
|
|
220
|
+
// keys the HMAC identically with them (that size is at most 64 bytes, within one hash block). A present element
|
|
221
|
+
// that is anything else is malformed: the package is then never rendered.
|
|
222
|
+
function dataIntegrity(values, data) {
|
|
223
|
+
if (!values) return null;
|
|
224
|
+
const hmacKey = base64Binary(values.encryptedHmacKey);
|
|
225
|
+
const hmacValue = base64Binary(values.encryptedHmacValue);
|
|
226
|
+
const saltLengths = [data.saltSize, data.hashSize].filter(
|
|
227
|
+
(length) => hmacKey?.length === wholeBlocks(length, data.blockSize)
|
|
228
|
+
);
|
|
229
|
+
if (!saltLengths.length || hmacValue?.length !== wholeBlocks(data.hashSize, data.blockSize)) {
|
|
230
|
+
return { malformed: true };
|
|
231
|
+
}
|
|
232
|
+
return { hmacKey, hmacValue, saltLength: Math.max(...saltLengths) };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function agileDescriptor(info) {
|
|
236
|
+
let fields;
|
|
237
|
+
try {
|
|
238
|
+
fields = descriptorFields(new TextDecoder('utf-8', { fatal: true }).decode(info.subarray(8)));
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const values = fields.encryptedKey;
|
|
243
|
+
const data = agileCipher(fields.keyData);
|
|
244
|
+
const key = agileCipher(values);
|
|
245
|
+
const spinCount = unsignedInt(values.spinCount);
|
|
246
|
+
if (!data || !key || spinCount === null || spinCount > MAX_SPIN_COUNT) return null;
|
|
247
|
+
// The password key encryptor MUST use the keyData's cipher (both AES here) and hash algorithm.
|
|
248
|
+
if (key.algorithm !== data.algorithm) return null;
|
|
249
|
+
// 2.3.4.13: the verifier input (saltSize bytes), its hash and the intermediate key, each padded to whole blocks.
|
|
250
|
+
const verifierInput = ciphertext(values.encryptedVerifierHashInput, key.blockSize, key.saltSize);
|
|
251
|
+
const verifierHash = ciphertext(values.encryptedVerifierHashValue, key.blockSize, key.hashSize);
|
|
252
|
+
const encryptedKey = ciphertext(values.encryptedKeyValue, key.blockSize, data.keyBits / 8);
|
|
253
|
+
if (!verifierInput || !verifierHash || !encryptedKey) return null;
|
|
254
|
+
return {
|
|
255
|
+
type: 'agile',
|
|
256
|
+
data,
|
|
257
|
+
key,
|
|
258
|
+
spinCount,
|
|
259
|
+
verifierInput,
|
|
260
|
+
verifierHash,
|
|
261
|
+
encryptedKey,
|
|
262
|
+
integrity: dataIntegrity(fields.dataIntegrity, data),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function standardDescriptor(info) {
|
|
267
|
+
if (info.length < 12) return null;
|
|
268
|
+
const headerSize = info.readUInt32LE(8);
|
|
269
|
+
if (headerSize < 32 || 12 + headerSize + 4 > info.length) return null;
|
|
270
|
+
// 2.3.2 EncryptionHeader.Flags: standard encryption sets fCryptoAPI and fAES.
|
|
271
|
+
const flags = info.readUInt32LE(12);
|
|
272
|
+
const keyBits = AES_ALGORITHMS.get(info.readUInt32LE(20));
|
|
273
|
+
const hashAlgorithm = info.readUInt32LE(24);
|
|
274
|
+
if (!(flags & STANDARD_FLAG_CRYPTOAPI) || !(flags & STANDARD_FLAG_AES)) return null;
|
|
275
|
+
if (!keyBits || info.readUInt32LE(28) !== keyBits || ![0, 0x8004].includes(hashAlgorithm)) return null;
|
|
276
|
+
// 2.3.3 EncryptionVerifier: SaltSize (16), Salt, EncryptedVerifier (16), VerifierHashSize (20), and the
|
|
277
|
+
// AES-padded EncryptedVerifierHash (32).
|
|
278
|
+
const at = 12 + headerSize;
|
|
279
|
+
if (info.readUInt32LE(at) !== 16 || at + 4 + 16 + 16 + 4 + 32 > info.length) return null;
|
|
280
|
+
const verifierHashSize = info.readUInt32LE(at + 36);
|
|
281
|
+
if (verifierHashSize !== 20) return null;
|
|
282
|
+
return {
|
|
283
|
+
type: 'standard',
|
|
284
|
+
keyBits,
|
|
285
|
+
salt: info.subarray(at + 4, at + 20),
|
|
286
|
+
verifier: info.subarray(at + 20, at + 36),
|
|
287
|
+
verifierHash: info.subarray(at + 40, at + 72),
|
|
288
|
+
verifierHashSize,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* How an \EncryptionInfo stream encrypts its package, or null when this is not agile or standard encryption with
|
|
294
|
+
* parameters Office writes. The version decides (2.3.4.5, 2.3.4.10): 4.4 with reserved 0x40 is agile, minor 2 with
|
|
295
|
+
* major 2, 3 or 4 is standard; minor 3 (extensible) is unsupported. An agile descriptor's `integrity` is null
|
|
296
|
+
* without a dataIntegrity element and `{ malformed: true }` for one that cannot be checked.
|
|
297
|
+
*/
|
|
298
|
+
export function ooxmlEncryption(info) {
|
|
299
|
+
if (!Buffer.isBuffer(info) || info.length < 8) return null;
|
|
300
|
+
const major = info.readUInt16LE(0);
|
|
301
|
+
const minor = info.readUInt16LE(2);
|
|
302
|
+
if (major === 4 && minor === 4) return info.readUInt32LE(4) === 0x40 ? agileDescriptor(info) : null;
|
|
303
|
+
if ([2, 3, 4].includes(major) && minor === 2) return standardDescriptor(info);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const KEY_WORKER = new URL('./ooxml-key-worker.mjs', import.meta.url);
|
|
308
|
+
// Password derivations that may run at once, each on its own thread; later ones wait for a free one.
|
|
309
|
+
const MAX_KEY_DERIVATIONS = 2;
|
|
310
|
+
let runningDerivations = 0;
|
|
311
|
+
const waitingDerivations = [];
|
|
312
|
+
|
|
313
|
+
/** Password derivations running on their threads and waiting for one (for diagnostics and tests). */
|
|
314
|
+
export const ooxmlKeyDerivations = () => ({ running: runningDerivations, waiting: waitingDerivations.length });
|
|
315
|
+
|
|
316
|
+
function derivationSlot(signal) {
|
|
317
|
+
if (runningDerivations < MAX_KEY_DERIVATIONS) {
|
|
318
|
+
runningDerivations++;
|
|
319
|
+
return Promise.resolve();
|
|
320
|
+
}
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const start = () => {
|
|
323
|
+
signal?.removeEventListener('abort', onAbort);
|
|
324
|
+
runningDerivations++;
|
|
325
|
+
resolve();
|
|
326
|
+
};
|
|
327
|
+
const onAbort = () => {
|
|
328
|
+
waitingDerivations.splice(waitingDerivations.indexOf(start), 1);
|
|
329
|
+
reject(signal.reason);
|
|
330
|
+
};
|
|
331
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
332
|
+
waitingDerivations.push(start);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function releaseDerivationSlot() {
|
|
337
|
+
runningDerivations--;
|
|
338
|
+
waitingDerivations.shift()?.();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 2.3.4.7 / 2.3.4.11: H0 = H(salt + password), Hn = H(iterator + Hn-1), computed on a worker thread
|
|
342
|
+
// (ooxml-key-worker.mjs). An abort ends the thread and rejects with the signal's reason; the thread's slot is free
|
|
343
|
+
// again once it has exited, however it ends.
|
|
344
|
+
async function iteratedHash(algorithm, salt, password, spinCount, signal) {
|
|
345
|
+
signal?.throwIfAborted();
|
|
346
|
+
await derivationSlot(signal);
|
|
347
|
+
let worker;
|
|
348
|
+
try {
|
|
349
|
+
signal?.throwIfAborted();
|
|
350
|
+
worker = new Worker(KEY_WORKER, {
|
|
351
|
+
execArgv: workerExecArgv(),
|
|
352
|
+
workerData: { algorithm, salt, password: String(password), spinCount },
|
|
353
|
+
});
|
|
354
|
+
} catch (error) {
|
|
355
|
+
releaseDerivationSlot();
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
worker.once('exit', releaseDerivationSlot);
|
|
359
|
+
return await new Promise((resolve, reject) => {
|
|
360
|
+
const settle = (callback, value) => {
|
|
361
|
+
signal?.removeEventListener('abort', onAbort);
|
|
362
|
+
callback(value);
|
|
363
|
+
};
|
|
364
|
+
const onAbort = () => {
|
|
365
|
+
void worker.terminate();
|
|
366
|
+
settle(reject, signal.reason);
|
|
367
|
+
};
|
|
368
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
369
|
+
worker.once('message', (value) => settle(resolve, Buffer.from(value.buffer, value.byteOffset, value.byteLength)));
|
|
370
|
+
worker.once('error', (error) => settle(reject, error));
|
|
371
|
+
worker.once('exit', (code) =>
|
|
372
|
+
settle(reject, new Error(`The password derivation ended early (exit code ${code}).`))
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function agileKey(descriptor, password, signal) {
|
|
378
|
+
const { key } = descriptor;
|
|
379
|
+
const spun = await iteratedHash(key.algorithm, key.salt, password, descriptor.spinCount, signal);
|
|
380
|
+
const iv = fitted(key.salt, key.blockSize);
|
|
381
|
+
const open = (block, data) =>
|
|
382
|
+
decrypt('cbc', key.keyBits, fitted(hash(key.algorithm, spun, block), key.keyBits / 8), iv, data);
|
|
383
|
+
// 2.3.4.13: the decrypted verifier input, hashed, must equal the decrypted verifier hash.
|
|
384
|
+
const input = open(VERIFIER_INPUT_BLOCK, descriptor.verifierInput).subarray(0, key.saltSize);
|
|
385
|
+
const expected = hash(key.algorithm, input);
|
|
386
|
+
const actual = open(VERIFIER_HASH_BLOCK, descriptor.verifierHash).subarray(0, key.hashSize);
|
|
387
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) return null;
|
|
388
|
+
// 2.3.4.13: the intermediate key that encrypts the package.
|
|
389
|
+
return open(ENCRYPTED_KEY_BLOCK, descriptor.encryptedKey).subarray(0, descriptor.data.keyBits / 8);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 2.3.4.7: the key derived from the iterated SHA-1 hash, and 2.3.4.9: the verifier check.
|
|
393
|
+
async function standardKey(descriptor, password, signal) {
|
|
394
|
+
const spun = await iteratedHash('sha1', descriptor.salt, password, STANDARD_SPIN_COUNT, signal);
|
|
395
|
+
const final = hash('sha1', spun, uint32(0));
|
|
396
|
+
const derive = (fill) => {
|
|
397
|
+
const buffer = Buffer.alloc(64, fill);
|
|
398
|
+
for (let index = 0; index < final.length; index++) buffer[index] ^= final[index];
|
|
399
|
+
return hash('sha1', buffer);
|
|
400
|
+
};
|
|
401
|
+
const key = Buffer.concat([derive(0x36), derive(0x5c)]).subarray(0, descriptor.keyBits / 8);
|
|
402
|
+
const verifier = decrypt('ecb', descriptor.keyBits, key, null, descriptor.verifier);
|
|
403
|
+
const expected = hash('sha1', verifier);
|
|
404
|
+
const actual = decrypt('ecb', descriptor.keyBits, key, null, descriptor.verifierHash).subarray(
|
|
405
|
+
0,
|
|
406
|
+
descriptor.verifierHashSize
|
|
407
|
+
);
|
|
408
|
+
return timingSafeEqual(actual, expected) ? key : null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The key that decrypts the package when `password` is right, else null. The verifier alone decides; the package
|
|
413
|
+
* is not touched. The password hashing runs off this thread, at most two at once, and an abort of `signal` ends it
|
|
414
|
+
* and rejects with the signal's reason.
|
|
415
|
+
*/
|
|
416
|
+
export async function ooxmlPasswordKey(descriptor, password, { signal = null } = {}) {
|
|
417
|
+
return descriptor.type === 'agile'
|
|
418
|
+
? await agileKey(descriptor, password, signal)
|
|
419
|
+
: await standardKey(descriptor, password, signal);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 2.3.4.14: whether \EncryptedPackage is the stream the descriptor's HMAC was computed over. The HMAC key (the
|
|
424
|
+
* salt) and value are decrypted with the package key and IVs H(keyData salt + block key) (2.3.4.12), and the HMAC
|
|
425
|
+
* covers the whole stream, its StreamSize field included. Standard encryption and an agile descriptor without
|
|
426
|
+
* dataIntegrity carry nothing to check (the schema makes the element optional); a malformed one never passes.
|
|
427
|
+
*/
|
|
428
|
+
export function ooxmlPackageIntact(descriptor, key, encryptedPackage) {
|
|
429
|
+
const { integrity } = descriptor;
|
|
430
|
+
if (descriptor.type !== 'agile' || !integrity) return true;
|
|
431
|
+
if (integrity.malformed) return false;
|
|
432
|
+
const { data } = descriptor;
|
|
433
|
+
const open = (block, value) =>
|
|
434
|
+
decrypt('cbc', data.keyBits, key, fitted(hash(data.algorithm, data.salt, block), data.blockSize), value);
|
|
435
|
+
// The HMAC key is the salt without its block padding (its length read in dataIntegrity), the HMAC the first
|
|
436
|
+
// hashSize bytes.
|
|
437
|
+
const hmacKey = open(INTEGRITY_KEY_BLOCK, integrity.hmacKey).subarray(0, integrity.saltLength);
|
|
438
|
+
const expected = open(INTEGRITY_VALUE_BLOCK, integrity.hmacValue).subarray(0, data.hashSize);
|
|
439
|
+
const actual = createHmac(data.algorithm, hmacKey).update(encryptedPackage).digest();
|
|
440
|
+
return timingSafeEqual(actual, expected);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* 2.3.4.15 (agile) / 2.3.4.4 (standard): the plain package. \EncryptedPackage starts with the 8-byte StreamSize of
|
|
445
|
+
* the plain package; agile ciphertext is 4096-byte segments, each with IV H(keyData salt + segment index), standard
|
|
446
|
+
* ciphertext is one AES-ECB run.
|
|
447
|
+
*/
|
|
448
|
+
export function decryptOoxmlPackage(descriptor, key, encryptedPackage) {
|
|
449
|
+
if (encryptedPackage.length < 8) throw new Error('The encrypted package is truncated.');
|
|
450
|
+
const size = Number(encryptedPackage.readBigUInt64LE(0));
|
|
451
|
+
const body = encryptedPackage.subarray(8);
|
|
452
|
+
if (size > body.length) throw new Error('The encrypted package is truncated.');
|
|
453
|
+
let plain;
|
|
454
|
+
if (descriptor.type === 'agile') {
|
|
455
|
+
const { data } = descriptor;
|
|
456
|
+
const parts = [];
|
|
457
|
+
for (let offset = 0, index = 0; offset < body.length; offset += SEGMENT_LENGTH, index++) {
|
|
458
|
+
const segment = body.subarray(offset, offset + SEGMENT_LENGTH);
|
|
459
|
+
const whole = segment.subarray(0, segment.length - (segment.length % data.blockSize));
|
|
460
|
+
const iv = fitted(hash(data.algorithm, data.salt, uint32(index)), data.blockSize);
|
|
461
|
+
parts.push(decrypt('cbc', data.keyBits, key, iv, whole));
|
|
462
|
+
}
|
|
463
|
+
plain = Buffer.concat(parts);
|
|
464
|
+
} else {
|
|
465
|
+
plain = decrypt('ecb', descriptor.keyBits, key, null, body.subarray(0, body.length - (body.length % 16)));
|
|
466
|
+
}
|
|
467
|
+
if (plain.length < size) throw new Error('The encrypted package is truncated.');
|
|
468
|
+
return plain.subarray(0, size);
|
|
469
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The password hash iteration of [MS-OFFCRYPTO] 2.3.4.7 (standard) and 2.3.4.11 (agile), on its own thread so up to
|
|
2
|
+
// 10,000,000 hashes never hold the host's event loop; ending the thread cancels it (ooxml-crypto.mjs).
|
|
3
|
+
// H0 = H(salt + password as UTF-16LE), Hn = H(iterator + Hn-1) for iterator 0 .. spinCount - 1.
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
6
|
+
|
|
7
|
+
const { algorithm, salt, password, spinCount } = workerData;
|
|
8
|
+
let value = createHash(algorithm).update(salt).update(Buffer.from(password, 'utf16le')).digest();
|
|
9
|
+
const iterator = Buffer.alloc(4);
|
|
10
|
+
for (let index = 0; index < spinCount; index++) {
|
|
11
|
+
iterator.writeUInt32LE(index, 0);
|
|
12
|
+
value = createHash(algorithm).update(iterator).update(value).digest();
|
|
13
|
+
}
|
|
14
|
+
parentPort.postMessage(value);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, dirname, extname, join } from 'node:path';
|
|
2
2
|
import { homedir, tmpdir } from 'node:os';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
3
|
+
import { execFile, spawn } from 'node:child_process';
|
|
4
4
|
import { redactDirectories } from '../../shared/redact-directories.mjs';
|
|
5
5
|
import { removeDirectoryWithRetry } from '../../shared/remove-directory.mjs';
|
|
6
6
|
import { constants as fsConstants, rmSync } from 'node:fs';
|
|
@@ -146,8 +146,16 @@ function libreOfficeCandidates() {
|
|
|
146
146
|
// exits once the conversion finishes. soffice.exe returns early or never,
|
|
147
147
|
// so a caller awaiting its exit cannot tell when output is ready.
|
|
148
148
|
'soffice.com',
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
...new Set([
|
|
150
|
+
join(process.env.ProgramFiles || 'C:\\Program Files', 'LibreOffice', 'program', 'soffice.com'),
|
|
151
|
+
join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'LibreOffice', 'program', 'soffice.com'),
|
|
152
|
+
'C:\\Program Files\\LibreOffice\\program\\soffice.com',
|
|
153
|
+
'C:\\Program Files (x86)\\LibreOffice\\program\\soffice.com',
|
|
154
|
+
// A per-user install (the MSI's "just for me" choice).
|
|
155
|
+
...(process.env.LOCALAPPDATA
|
|
156
|
+
? [join(process.env.LOCALAPPDATA, 'Programs', 'LibreOffice', 'program', 'soffice.com')]
|
|
157
|
+
: []),
|
|
158
|
+
]),
|
|
151
159
|
];
|
|
152
160
|
}
|
|
153
161
|
if (process.platform === 'darwin') {
|
|
@@ -169,11 +177,43 @@ function libreOfficeCandidates() {
|
|
|
169
177
|
];
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
// The LibreOffice installer records its `program` folder as the default value
|
|
181
|
+
// of SOFTWARE\LibreOffice\UNO\InstallPath, in HKLM (all users, 64- or 32-bit
|
|
182
|
+
// view) or HKCU (per-user). This covers installs on another drive or in a
|
|
183
|
+
// custom folder. The value's display name is localized, so only the REG_SZ
|
|
184
|
+
// data is read.
|
|
185
|
+
const LIBREOFFICE_REGISTRY_QUERIES = [
|
|
186
|
+
['HKLM', '/reg:64'],
|
|
187
|
+
['HKLM', '/reg:32'],
|
|
188
|
+
['HKCU', '/reg:64'],
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
/** The `soffice.com` paths named by `reg query ... /ve` output. */
|
|
192
|
+
export function registrySofficePrograms(output) {
|
|
193
|
+
return String(output || '')
|
|
194
|
+
.split(/\r?\n/)
|
|
195
|
+
.map((line) => /REG_(?:EXPAND_)?SZ\s+(.+?)\s*$/.exec(line)?.[1])
|
|
196
|
+
.filter(Boolean)
|
|
197
|
+
.map((directory) => join(directory, 'soffice.com'));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function queryRegistry(args) {
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
execFile('reg.exe', args, { windowsHide: true, timeout: 5000 }, (error, stdout) => resolve(error ? '' : stdout));
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
172
206
|
async function findLibreOfficeProgram() {
|
|
173
|
-
const
|
|
174
|
-
for (const candidate of candidates) {
|
|
207
|
+
for (const candidate of libreOfficeCandidates()) {
|
|
175
208
|
if (await commandExists(candidate)) return candidate;
|
|
176
209
|
}
|
|
210
|
+
if (process.platform !== 'win32') return '';
|
|
211
|
+
for (const [hive, view] of LIBREOFFICE_REGISTRY_QUERIES) {
|
|
212
|
+
const output = await queryRegistry(['query', `${hive}\\SOFTWARE\\LibreOffice\\UNO\\InstallPath`, '/ve', view]);
|
|
213
|
+
for (const candidate of registrySofficePrograms(output)) {
|
|
214
|
+
if (await commandExists(candidate)) return candidate;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
177
217
|
return '';
|
|
178
218
|
}
|
|
179
219
|
|
|
@@ -30,7 +30,7 @@ function hosts() {
|
|
|
30
30
|
* resolves the turn's end status ('done' when it finished).
|
|
31
31
|
* - `stop(roomSessionId)` → boolean: the room's stop button: the speaking
|
|
32
32
|
* member's turn is cancelled, queued calls settle cancelled, and no one else
|
|
33
|
-
* gets a turn; true when an exchange was
|
|
33
|
+
* gets a turn; true when an exchange or incoming request was cancelled.
|
|
34
34
|
* `hop`: the send's or report's place in a bot-to-bot chain (bot-hops.mjs);
|
|
35
35
|
* the member's turn carries it, so the sends it makes count on from there.
|
|
36
36
|
* `follow(submissionId)` (both): the turn that answers is submitted under
|
|
@@ -20,16 +20,76 @@ export const hopOf = (value) => (Number.isInteger(value) && value > 0 ? value :
|
|
|
20
20
|
* depth: bot-descended work resumes where it was, independent work at 0. */
|
|
21
21
|
export const sendDepth = ({ hop, target } = {}) => Math.max(0, hopOf(hop) - (target === 'session' ? 1 : 0));
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// The bots of a chain (`botChain`): the agent ids that handed the work on,
|
|
24
|
+
// oldest first, the sender last. A bot's send carries the chain of its turn
|
|
25
|
+
// plus itself in the transcript meta of the message it delivers (like `hop`);
|
|
26
|
+
// the turn that message starts or joins is in that chain (`turnBotChain`), and
|
|
27
|
+
// a send or spawn from it may never reach a bot already in it, nor itself
|
|
28
|
+
// (assertBotChainTarget). A turn the user, a routine or a notification
|
|
29
|
+
// started begins a chain of its own.
|
|
30
|
+
export const botChainOf = (value) =>
|
|
31
|
+
Array.isArray(value) ? [...new Set(value.filter((id) => typeof id === 'string' && id))] : [];
|
|
32
|
+
|
|
33
|
+
// The chain of a submission the runtime takes without transcript meta of its
|
|
34
|
+
// own (a group-room member's turn, submitted by the desktop room host under
|
|
35
|
+
// the send's submission id), recorded by the sender before it is submitted.
|
|
36
|
+
const chainsBySubmission = new Map();
|
|
37
|
+
const MAX_SUBMISSION_CHAINS = 1000;
|
|
38
|
+
|
|
39
|
+
/** Record that submission `submissionId` starts or joins a turn of `chain`. */
|
|
40
|
+
export function rememberSubmissionBotChain(submissionId, chain) {
|
|
41
|
+
const id = String(submissionId || '').trim();
|
|
42
|
+
const bots = botChainOf(chain);
|
|
43
|
+
if (!id || !bots.length) return;
|
|
44
|
+
chainsBySubmission.set(id, bots);
|
|
45
|
+
while (chainsBySubmission.size > MAX_SUBMISSION_CHAINS) {
|
|
46
|
+
chainsBySubmission.delete(chainsBySubmission.keys().next().value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The chain a submission's transcript meta carries, else the one recorded for its ids. */
|
|
51
|
+
function submissionBotChain(transcriptMeta) {
|
|
52
|
+
const carried = botChainOf(transcriptMeta?.botChain);
|
|
53
|
+
if (carried.length) return carried;
|
|
54
|
+
const merged = Array.isArray(transcriptMeta?.submissionIds) ? transcriptMeta.submissionIds : [];
|
|
55
|
+
const ids = [transcriptMeta?.checkpointId, ...merged];
|
|
56
|
+
return botChainOf(ids.flatMap((id) => chainsBySubmission.get(String(id || '')) || []));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Stamp the hop and bot chain of the submission that starts the turn (else 0 and none) on `session`. */
|
|
24
60
|
export function stampTurnHop(session, transcriptMeta) {
|
|
25
61
|
session.turnHop = hopOf(transcriptMeta?.hop);
|
|
62
|
+
session.turnBotChain = submissionBotChain(transcriptMeta);
|
|
26
63
|
}
|
|
27
64
|
|
|
28
|
-
/** A bot's message steered into the running turn raises the turn's hop. */
|
|
65
|
+
/** A bot's message steered into the running turn raises the turn's hop and joins its chain. */
|
|
29
66
|
export function raiseTurnHop(session, transcriptMeta) {
|
|
30
67
|
const hop = hopOf(transcriptMeta?.hop);
|
|
31
68
|
if (session && hop > hopOf(session.turnHop)) session.turnHop = hop;
|
|
69
|
+
const chain = submissionBotChain(transcriptMeta);
|
|
70
|
+
if (session && chain.length) session.turnBotChain = botChainOf([...turnBotChainOf(session), ...chain]);
|
|
32
71
|
}
|
|
33
72
|
|
|
34
73
|
/** The hop of the turn `session` is in. */
|
|
35
74
|
export const turnHopOf = (session) => hopOf(session?.turnHop);
|
|
75
|
+
|
|
76
|
+
/** The bot chain of the turn `session` is in. */
|
|
77
|
+
export const turnBotChainOf = (session) => botChainOf(session?.turnBotChain);
|
|
78
|
+
|
|
79
|
+
/** The one check of every bot-to-bot hand-off (session send, a room seat,
|
|
80
|
+
* a contact agent spawn): `target`, the bot the work would reach, is
|
|
81
|
+
* neither `self` (the bot handing it on) nor already in `chain`. */
|
|
82
|
+
export function assertBotChainTarget(target, { chain = [], self = '' } = {}, prefix = 'session') {
|
|
83
|
+
if (!target) return;
|
|
84
|
+
if (target === self) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`${prefix}: the contact agent "${target}" cannot hand work to itself; do it yourself or delegate to a sub-agent`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (chain.includes(target)) {
|
|
90
|
+
const path = [...chain, ...(self && chain.at(-1) !== self ? [self] : []), target];
|
|
91
|
+
throw new Error(
|
|
92
|
+
`${prefix}: the contact agent "${target}" is already in this delegation chain (${path.join(' → ')}); do not hand the work back, answer or delegate to a sub-agent instead`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|