stegdoc 3.0.2 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/commands/decode.js +485 -215
- package/src/commands/encode.js +567 -346
- package/src/commands/info.js +118 -113
- package/src/commands/verify.js +207 -169
- package/src/index.js +89 -87
- package/src/lib/compression.js +177 -97
- package/src/lib/crypto.js +172 -118
- package/src/lib/decoy-generator.js +306 -306
- package/src/lib/docx-handler.js +587 -161
- package/src/lib/docx-templates.js +355 -0
- package/src/lib/file-handler.js +113 -113
- package/src/lib/file-utils.js +160 -150
- package/src/lib/interactive.js +190 -190
- package/src/lib/log-generator.js +764 -0
- package/src/lib/metadata.js +151 -111
- package/src/lib/streams.js +197 -0
- package/src/lib/utils.js +227 -227
- package/src/lib/xlsx-handler.js +597 -359
- package/src/lib/xml-utils.js +115 -115
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log-Based Steganography Engine (v5)
|
|
3
|
+
*
|
|
4
|
+
* Encodes binary payload into realistic nginx access log entries.
|
|
5
|
+
* Payload is distributed across URL paths, query params, referer fields,
|
|
6
|
+
* UUIDs (X-Request-ID), and hex trace IDs (X-Trace-ID).
|
|
7
|
+
*
|
|
8
|
+
* No hidden sheets — the data IS the logs.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Payload bytes carried per channel per log line.
|
|
15
|
+
* These are RAW BYTE counts (before base64url/hex encoding).
|
|
16
|
+
*/
|
|
17
|
+
const CHANNEL_SIZES = {
|
|
18
|
+
urlPath: 21, // base64url in URL path segment (28 chars)
|
|
19
|
+
queryToken: 21, // base64url in ?token= param (28 chars)
|
|
20
|
+
queryState: 21, // base64url in &state= param (28 chars)
|
|
21
|
+
referer: 21, // base64url in referer ?ref= param (28 chars)
|
|
22
|
+
requestId: 14, // hex in UUID v4 format (32 hex chars, 6 bits forced)
|
|
23
|
+
traceId: 16, // hex in 32-char trace ID
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const BYTES_PER_DATA_LINE = Object.values(CHANNEL_SIZES).reduce((a, b) => a + b, 0); // 114
|
|
27
|
+
|
|
28
|
+
const HEADER_MARKER_PATH = '/api/v1/health';
|
|
29
|
+
|
|
30
|
+
// ─── IP Pool (Zipfian-ish distribution) ─────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const IP_POOL_FREQUENT = [
|
|
33
|
+
'10.0.14.72', '10.0.14.73', '10.0.14.80', '10.0.15.12',
|
|
34
|
+
'172.16.0.45', '172.16.0.46', '192.168.1.100', '192.168.1.101',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const IP_POOL_RARE = [
|
|
38
|
+
'10.0.14.91', '10.0.14.92', '10.0.14.93', '10.0.15.20',
|
|
39
|
+
'10.0.15.21', '10.0.15.30', '10.0.16.10', '10.0.16.11',
|
|
40
|
+
'172.16.0.50', '172.16.0.51', '172.16.0.55', '172.16.1.10',
|
|
41
|
+
'172.16.1.15', '172.16.1.20', '192.168.1.110', '192.168.1.120',
|
|
42
|
+
'192.168.1.130', '192.168.1.140', '192.168.1.200', '192.168.2.10',
|
|
43
|
+
'192.168.2.20', '192.168.2.30', '192.168.2.40', '192.168.2.50',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// ─── User-Agent Pool ────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const USER_AGENTS = [
|
|
49
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
50
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
|
51
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
52
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0',
|
|
53
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15',
|
|
54
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
55
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/122.0.0.0 Safari/537.36',
|
|
56
|
+
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0',
|
|
57
|
+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Mobile/15E148 Safari/604.1',
|
|
58
|
+
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36',
|
|
59
|
+
// Bots (occasional)
|
|
60
|
+
'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
|
|
61
|
+
'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Weights for UA selection (frequent browsers appear more)
|
|
65
|
+
const UA_WEIGHTS = [20, 15, 12, 10, 8, 7, 6, 5, 4, 3, 1, 1];
|
|
66
|
+
|
|
67
|
+
// ─── URL Path Templates (for data lines) ───────────────────────────────────
|
|
68
|
+
|
|
69
|
+
// All templates place the token as the LAST path segment for unambiguous extraction.
|
|
70
|
+
const URL_TEMPLATES = [
|
|
71
|
+
{ path: '/api/v2/sessions/validate/{0}', query: 'token={1}&state={2}' },
|
|
72
|
+
{ path: '/api/v2/auth/callback/{0}', query: 'token={1}&state={2}' },
|
|
73
|
+
{ path: '/api/v2/users/profile/{0}', query: 'token={1}&state={2}' },
|
|
74
|
+
{ path: '/api/v2/tokens/refresh/{0}', query: 'token={1}&state={2}' },
|
|
75
|
+
{ path: '/api/v2/oauth/authorize/{0}', query: 'token={1}&state={2}' },
|
|
76
|
+
{ path: '/api/v2/verify/email/{0}', query: 'token={1}&state={2}' },
|
|
77
|
+
{ path: '/api/v2/sso/saml/{0}', query: 'token={1}&state={2}' },
|
|
78
|
+
{ path: '/api/v2/mfa/challenge/{0}', query: 'token={1}&state={2}' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const REFERER_TEMPLATES = [
|
|
82
|
+
'https://dashboard.internal.local/overview?ref={0}',
|
|
83
|
+
'https://dashboard.internal.local/settings?ref={0}',
|
|
84
|
+
'https://sso.internal.local/auth/login?ref={0}',
|
|
85
|
+
'https://dashboard.internal.local/account?ref={0}',
|
|
86
|
+
'https://admin.internal.local/users?ref={0}',
|
|
87
|
+
'https://dashboard.internal.local/reports?ref={0}',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// ─── Status Code Distribution ───────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const STATUS_CODES = [
|
|
93
|
+
{ code: 200, weight: 75 },
|
|
94
|
+
{ code: 201, weight: 3 },
|
|
95
|
+
{ code: 204, weight: 2 },
|
|
96
|
+
{ code: 301, weight: 3 },
|
|
97
|
+
{ code: 302, weight: 5 },
|
|
98
|
+
{ code: 304, weight: 4 },
|
|
99
|
+
{ code: 400, weight: 2 },
|
|
100
|
+
{ code: 401, weight: 1 },
|
|
101
|
+
{ code: 403, weight: 1 },
|
|
102
|
+
{ code: 404, weight: 3 },
|
|
103
|
+
{ code: 500, weight: 1 },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// ─── HTTP Methods ───────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
const HTTP_METHODS = [
|
|
109
|
+
{ method: 'GET', weight: 70 },
|
|
110
|
+
{ method: 'POST', weight: 20 },
|
|
111
|
+
{ method: 'PUT', weight: 5 },
|
|
112
|
+
{ method: 'DELETE', weight: 3 },
|
|
113
|
+
{ method: 'PATCH', weight: 2 },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
// ─── Timestamp Management ───────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
let sharedTimeState = null;
|
|
119
|
+
|
|
120
|
+
function initTimeState() {
|
|
121
|
+
if (!sharedTimeState) {
|
|
122
|
+
const now = new Date();
|
|
123
|
+
sharedTimeState = {
|
|
124
|
+
current: new Date(now.getTime() - 4 * 60 * 60 * 1000), // Start 4 hours ago
|
|
125
|
+
end: now,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return sharedTimeState;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resetTimeState() {
|
|
132
|
+
sharedTimeState = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function nextTimestamp() {
|
|
136
|
+
const state = initTimeState();
|
|
137
|
+
// Advance by 0.1-3 seconds (realistic inter-request interval)
|
|
138
|
+
const advanceMs = 100 + Math.floor(Math.random() * 2900);
|
|
139
|
+
state.current = new Date(state.current.getTime() + advanceMs);
|
|
140
|
+
|
|
141
|
+
const d = state.current;
|
|
142
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
143
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
144
|
+
const mon = months[d.getMonth()];
|
|
145
|
+
const year = d.getFullYear();
|
|
146
|
+
const h = String(d.getHours()).padStart(2, '0');
|
|
147
|
+
const m = String(d.getMinutes()).padStart(2, '0');
|
|
148
|
+
const s = String(d.getSeconds()).padStart(2, '0');
|
|
149
|
+
|
|
150
|
+
return `[${day}/${mon}/${year}:${h}:${m}:${s} +0000]`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Base64url Helpers ──────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
function toBase64Url(buffer) {
|
|
156
|
+
return buffer.toString('base64')
|
|
157
|
+
.replace(/\+/g, '-')
|
|
158
|
+
.replace(/\//g, '_')
|
|
159
|
+
.replace(/=+$/, '');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function fromBase64Url(str) {
|
|
163
|
+
// Restore padding
|
|
164
|
+
let s = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
165
|
+
const pad = (4 - (s.length % 4)) % 4;
|
|
166
|
+
s += '='.repeat(pad);
|
|
167
|
+
return Buffer.from(s, 'base64');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── UUID Payload Encoding ──────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Encode exactly 14 bytes into a valid UUID v4 string.
|
|
174
|
+
* Uses 28 hex chars from payload, inserts version nibble (4) and variant bits (10xx).
|
|
175
|
+
*/
|
|
176
|
+
function encodeUuidPayload(buf14) {
|
|
177
|
+
if (buf14.length !== 14) throw new Error(`UUID payload must be 14 bytes, got ${buf14.length}`);
|
|
178
|
+
|
|
179
|
+
const hex = buf14.toString('hex'); // 28 chars
|
|
180
|
+
|
|
181
|
+
// UUID hex layout (32 chars, no dashes):
|
|
182
|
+
// Positions: 0-7 | 8-11 | 12(ver)13-15 | 16(var)17-19 | 20-31
|
|
183
|
+
// We inject version nibble '4' at pos 12, variant nibble at pos 16
|
|
184
|
+
// Variant: top 2 bits = 10, bottom 2 bits from payload
|
|
185
|
+
const variantNibble = (8 | (parseInt(hex[12], 16) & 0x3)).toString(16);
|
|
186
|
+
|
|
187
|
+
const uuidHex =
|
|
188
|
+
hex.slice(0, 12) + // positions 0-11 (payload bytes 0-5)
|
|
189
|
+
'4' + // position 12 (version)
|
|
190
|
+
hex.slice(12, 15) + // positions 13-15 (payload byte 6 low nibble + byte 7 high nibble)
|
|
191
|
+
variantNibble + // position 16 (variant, carries 2 bits from payload)
|
|
192
|
+
hex.slice(15, 28); // positions 17-31 ... wait, 28-15 = 13 chars at positions 17-29, need 15 chars (17-31)
|
|
193
|
+
|
|
194
|
+
// Let me recalculate. We have 28 payload hex chars to place in 30 usable positions
|
|
195
|
+
// (32 total - version nibble - variant loses 2 bits = 30 full nibbles + 2 bits)
|
|
196
|
+
// Actually: 32 positions - 1 (version fully replaced) = 31 positions
|
|
197
|
+
// Position 16 carries 2 payload bits (low 2 of the nibble), high 2 are forced to 10
|
|
198
|
+
// So: 30 full nibbles + 2 bits = 122 bits = 15.25 bytes
|
|
199
|
+
// We use 14 bytes = 112 bits = 28 nibbles, well within 30+2
|
|
200
|
+
|
|
201
|
+
// Simpler approach: place 28 hex chars sequentially, skip position 12 and 16
|
|
202
|
+
// Position 0-11: hex[0-11]
|
|
203
|
+
// Position 12: '4' (version)
|
|
204
|
+
// Position 13-15: hex[12-14]
|
|
205
|
+
// Position 16: variant nibble (8 | (hex[15] & 0x3))... but this destroys 2 bits of hex[15]
|
|
206
|
+
|
|
207
|
+
// Even simpler: just use first 12 + last 16 hex chars = 28, around the forced nibbles
|
|
208
|
+
// No — let's do it properly with a clean algorithm:
|
|
209
|
+
|
|
210
|
+
// Actually the above code has an issue. Let me redo this cleanly.
|
|
211
|
+
// We have 28 hex chars (14 bytes payload).
|
|
212
|
+
// We need to place them into 32 UUID hex positions, skipping pos 12 (version=4)
|
|
213
|
+
// and at pos 16 modifying the high 2 bits (variant=10).
|
|
214
|
+
|
|
215
|
+
// Available positions for full payload nibbles: 0-11, 13-15, 17-31 = 30 positions
|
|
216
|
+
// Position 16 carries 2 payload bits (low 2) + 2 forced bits (high 2)
|
|
217
|
+
// We only need 28 positions, so we have room.
|
|
218
|
+
|
|
219
|
+
// Strategy: fill positions 0-11 with hex[0-11], skip 12 (='4'),
|
|
220
|
+
// fill positions 13-15 with hex[12-14],
|
|
221
|
+
// position 16 = (0x8 | (parseInt(hex[15], 16) & 0x3)).toString(16) -- carries 2 bits
|
|
222
|
+
// fill positions 17-31 with hex[16-27]... that's only 12 chars but need 15.
|
|
223
|
+
|
|
224
|
+
// hex chars used: 0-11(12) + 12-14(3) + [15 partial 2 bits] + 16-27(12) = 27 full + 2 bits
|
|
225
|
+
// That's only 27.25 nibbles = 13.625 bytes. Not 14. We're short.
|
|
226
|
+
|
|
227
|
+
// The issue: position 16 only carries 2 bits from hex[15], wasting 2 bits.
|
|
228
|
+
// So we actually carry: 12 + 3 + 0.5 + 12 = 27.5 nibbles = 110 bits = 13.75 bytes
|
|
229
|
+
// We can reliably carry 13 bytes this way, not 14.
|
|
230
|
+
|
|
231
|
+
// To carry 14 bytes (112 bits = 28 nibbles), we need 28 full nibble positions.
|
|
232
|
+
// Available full positions: 0-11 (12) + 13-15 (3) + 17-31 (15) = 30 full positions.
|
|
233
|
+
// Plus position 16 carries 2 partial bits.
|
|
234
|
+
// So 30 full positions > 28 needed. We have room!
|
|
235
|
+
|
|
236
|
+
// Correct strategy:
|
|
237
|
+
// Positions 0-11: hex[0-11] (12 nibbles)
|
|
238
|
+
// Position 12: '4' (version)
|
|
239
|
+
// Positions 13-15: hex[12-14] (3 nibbles)
|
|
240
|
+
// Position 16: variant nibble (forced, carries 0 payload bits)
|
|
241
|
+
// Positions 17-31: hex[15-27] (13 nibbles) -- wait 27-15+1 = 13, total = 12+3+13 = 28 ✓
|
|
242
|
+
|
|
243
|
+
// So position 16 is FULLY forced (no payload bits), and we use 30 available
|
|
244
|
+
// full positions minus 2 unused = 28 payload nibbles.
|
|
245
|
+
|
|
246
|
+
// Build it:
|
|
247
|
+
const pos0_11 = hex.slice(0, 12);
|
|
248
|
+
const pos13_15 = hex.slice(12, 15);
|
|
249
|
+
const pos16 = ['8', '9', 'a', 'b'][Math.floor(Math.random() * 4)]; // random valid variant
|
|
250
|
+
const pos17_31 = hex.slice(15, 28);
|
|
251
|
+
|
|
252
|
+
const full = pos0_11 + '4' + pos13_15 + pos16 + pos17_31;
|
|
253
|
+
// full length: 12 + 1 + 3 + 1 + 13 = 30... need 32
|
|
254
|
+
|
|
255
|
+
// I'm off by 2. Let me count UUID hex chars: 8-4-4-4-12 = 32 chars without dashes.
|
|
256
|
+
// Positions 0-31.
|
|
257
|
+
// 0-11 = 12, pos12 = 1, 13-15 = 3, pos16 = 1, 17-31 = 15. Total = 12+1+3+1+15 = 32. ✓
|
|
258
|
+
// So positions 17-31 = 15 chars. hex[15-27] = 13 chars. Need 15.
|
|
259
|
+
// 13 < 15. We're 2 chars short.
|
|
260
|
+
|
|
261
|
+
// We have 28 payload nibbles to place in 30 available slots (0-11, 13-15, 17-31).
|
|
262
|
+
// That's 12 + 3 + 15 = 30 slots. 28 payload chars leaves 2 unused.
|
|
263
|
+
// Fill remaining 2 with random hex to complete the UUID.
|
|
264
|
+
|
|
265
|
+
const padding = randomHex(2);
|
|
266
|
+
const fullHex = pos0_11 + '4' + pos13_15 + pos16 + hex.slice(15, 28) + padding;
|
|
267
|
+
|
|
268
|
+
return `${fullHex.slice(0, 8)}-${fullHex.slice(8, 12)}-${fullHex.slice(12, 16)}-${fullHex.slice(16, 20)}-${fullHex.slice(20, 32)}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function decodeUuidPayload(uuidStr) {
|
|
272
|
+
const hex = uuidStr.replace(/-/g, '');
|
|
273
|
+
if (hex.length !== 32) throw new Error('Invalid UUID format');
|
|
274
|
+
|
|
275
|
+
// Extract payload nibbles from known positions:
|
|
276
|
+
// Positions 0-11: payload[0-11]
|
|
277
|
+
// Position 12: version (skip)
|
|
278
|
+
// Positions 13-15: payload[12-14]
|
|
279
|
+
// Position 16: variant (skip)
|
|
280
|
+
// Positions 17-29: payload[15-27]
|
|
281
|
+
// Positions 30-31: padding (skip)
|
|
282
|
+
|
|
283
|
+
const payloadHex =
|
|
284
|
+
hex.slice(0, 12) + // 12 nibbles
|
|
285
|
+
hex.slice(13, 16) + // 3 nibbles
|
|
286
|
+
hex.slice(17, 30); // 13 nibbles = 28 total
|
|
287
|
+
|
|
288
|
+
return Buffer.from(payloadHex, 'hex'); // 14 bytes
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Trace ID Encoding ──────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function encodeTraceId(buf16) {
|
|
294
|
+
if (buf16.length !== 16) throw new Error(`Trace ID payload must be 16 bytes, got ${buf16.length}`);
|
|
295
|
+
return buf16.toString('hex');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function decodeTraceId(traceStr) {
|
|
299
|
+
return Buffer.from(traceStr, 'hex');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─── Random Helpers ─────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
const crypto = require('crypto');
|
|
305
|
+
|
|
306
|
+
function randomHex(chars) {
|
|
307
|
+
return crypto.randomBytes(Math.ceil(chars / 2)).toString('hex').slice(0, chars);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function weightedRandom(items) {
|
|
311
|
+
const total = items.reduce((sum, item) => sum + item.weight, 0);
|
|
312
|
+
let r = Math.random() * total;
|
|
313
|
+
for (const item of items) {
|
|
314
|
+
r -= item.weight;
|
|
315
|
+
if (r <= 0) return item;
|
|
316
|
+
}
|
|
317
|
+
return items[0];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function randomIp() {
|
|
321
|
+
// 80% frequent IPs, 20% rare
|
|
322
|
+
if (Math.random() < 0.8) {
|
|
323
|
+
return IP_POOL_FREQUENT[Math.floor(Math.random() * IP_POOL_FREQUENT.length)];
|
|
324
|
+
}
|
|
325
|
+
return IP_POOL_RARE[Math.floor(Math.random() * IP_POOL_RARE.length)];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function randomUserAgent() {
|
|
329
|
+
const totalWeight = UA_WEIGHTS.reduce((a, b) => a + b, 0);
|
|
330
|
+
let r = Math.random() * totalWeight;
|
|
331
|
+
for (let i = 0; i < USER_AGENTS.length; i++) {
|
|
332
|
+
r -= UA_WEIGHTS[i];
|
|
333
|
+
if (r <= 0) return USER_AGENTS[i];
|
|
334
|
+
}
|
|
335
|
+
return USER_AGENTS[0];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function randomResponseBytes(status) {
|
|
339
|
+
if (status === 204 || status === 304) return 0;
|
|
340
|
+
if (status >= 300 && status < 400) return 0;
|
|
341
|
+
if (status === 404) return 200 + Math.floor(Math.random() * 300);
|
|
342
|
+
if (status >= 500) return 300 + Math.floor(Math.random() * 500);
|
|
343
|
+
// 200-range: typical API response sizes
|
|
344
|
+
return 100 + Math.floor(Math.random() * 15000);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function randomFillerUuid() {
|
|
348
|
+
const bytes = crypto.randomBytes(16);
|
|
349
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
350
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
351
|
+
const hex = bytes.toString('hex');
|
|
352
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function randomFillerTraceId() {
|
|
356
|
+
return crypto.randomBytes(16).toString('hex');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── Core Encoding ──────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Generate column headers for the Access Logs sheet.
|
|
363
|
+
*/
|
|
364
|
+
function generateLogHeaders() {
|
|
365
|
+
return [
|
|
366
|
+
'Remote Address',
|
|
367
|
+
'Timestamp',
|
|
368
|
+
'Method',
|
|
369
|
+
'Request',
|
|
370
|
+
'Status',
|
|
371
|
+
'Bytes',
|
|
372
|
+
'Referer',
|
|
373
|
+
'User-Agent',
|
|
374
|
+
'X-Request-ID',
|
|
375
|
+
'X-Trace-ID',
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Distribute a payload chunk (Buffer) across log line channels.
|
|
381
|
+
* Returns { urlPath, queryToken, queryState, referer, requestId, traceId } as encoded strings.
|
|
382
|
+
*/
|
|
383
|
+
function distributePayload(payloadBuf) {
|
|
384
|
+
let offset = 0;
|
|
385
|
+
|
|
386
|
+
function take(n) {
|
|
387
|
+
const end = Math.min(offset + n, payloadBuf.length);
|
|
388
|
+
const slice = payloadBuf.slice(offset, end);
|
|
389
|
+
offset = end;
|
|
390
|
+
return slice;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function takePadded(n) {
|
|
394
|
+
const slice = take(n);
|
|
395
|
+
if (slice.length < n) {
|
|
396
|
+
// Pad with random bytes for the last partial line
|
|
397
|
+
return Buffer.concat([slice, crypto.randomBytes(n - slice.length)]);
|
|
398
|
+
}
|
|
399
|
+
return slice;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
urlPath: toBase64Url(takePadded(CHANNEL_SIZES.urlPath)),
|
|
404
|
+
queryToken: toBase64Url(takePadded(CHANNEL_SIZES.queryToken)),
|
|
405
|
+
queryState: toBase64Url(takePadded(CHANNEL_SIZES.queryState)),
|
|
406
|
+
referer: toBase64Url(takePadded(CHANNEL_SIZES.referer)),
|
|
407
|
+
requestId: encodeUuidPayload(takePadded(CHANNEL_SIZES.requestId)),
|
|
408
|
+
traceId: encodeTraceId(takePadded(CHANNEL_SIZES.traceId)),
|
|
409
|
+
bytesConsumed: Math.min(offset, payloadBuf.length),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Generate a data log line carrying payload bytes.
|
|
415
|
+
* @param {Buffer} payloadChunk - Chunk of payload for this line (up to BYTES_PER_DATA_LINE)
|
|
416
|
+
* @returns {Array} Row array of 10 cell values
|
|
417
|
+
*/
|
|
418
|
+
function generateDataLogLine(payloadChunk) {
|
|
419
|
+
const channels = distributePayload(payloadChunk);
|
|
420
|
+
const template = URL_TEMPLATES[Math.floor(Math.random() * URL_TEMPLATES.length)];
|
|
421
|
+
const refTemplate = REFERER_TEMPLATES[Math.floor(Math.random() * REFERER_TEMPLATES.length)];
|
|
422
|
+
const method = weightedRandom(HTTP_METHODS).method;
|
|
423
|
+
const statusObj = weightedRandom(STATUS_CODES);
|
|
424
|
+
const status = statusObj.code;
|
|
425
|
+
|
|
426
|
+
const requestPath = template.path.replace('{0}', channels.urlPath) +
|
|
427
|
+
'?' + template.query.replace('{1}', channels.queryToken).replace('{2}', channels.queryState);
|
|
428
|
+
|
|
429
|
+
const refererUrl = refTemplate.replace('{0}', channels.referer);
|
|
430
|
+
|
|
431
|
+
return [
|
|
432
|
+
randomIp(),
|
|
433
|
+
nextTimestamp(),
|
|
434
|
+
method,
|
|
435
|
+
`${method} ${requestPath} HTTP/1.1`,
|
|
436
|
+
status,
|
|
437
|
+
randomResponseBytes(status),
|
|
438
|
+
refererUrl,
|
|
439
|
+
randomUserAgent(),
|
|
440
|
+
channels.requestId,
|
|
441
|
+
channels.traceId,
|
|
442
|
+
];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Generate a header log line carrying metadata/encryption bytes.
|
|
447
|
+
* Uses the /api/v1/health marker so the decoder can identify these lines.
|
|
448
|
+
* @param {Buffer} payloadChunk - Metadata chunk for this line
|
|
449
|
+
* @returns {Array} Row array of 10 cell values
|
|
450
|
+
*/
|
|
451
|
+
function generateHeaderLogLine(payloadChunk) {
|
|
452
|
+
const channels = distributePayload(payloadChunk);
|
|
453
|
+
const status = 200;
|
|
454
|
+
|
|
455
|
+
// Health check always uses GET and the marker path
|
|
456
|
+
const requestPath = `${HEADER_MARKER_PATH}/${channels.urlPath}` +
|
|
457
|
+
`?token=${channels.queryToken}&state=${channels.queryState}`;
|
|
458
|
+
|
|
459
|
+
const refererUrl = `https://monitor.internal.local/health?ref=${channels.referer}`;
|
|
460
|
+
|
|
461
|
+
return [
|
|
462
|
+
'10.0.0.1', // Monitoring system IP (consistent)
|
|
463
|
+
nextTimestamp(),
|
|
464
|
+
'GET',
|
|
465
|
+
`GET ${requestPath} HTTP/1.1`,
|
|
466
|
+
status,
|
|
467
|
+
randomResponseBytes(status),
|
|
468
|
+
refererUrl,
|
|
469
|
+
'HealthCheck/1.0 (monitoring)',
|
|
470
|
+
channels.requestId,
|
|
471
|
+
channels.traceId,
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Generate a pure filler log line (no payload, all random data).
|
|
477
|
+
* @returns {Array} Row array of 10 cell values
|
|
478
|
+
*/
|
|
479
|
+
function generateFillerLogLine() {
|
|
480
|
+
const template = URL_TEMPLATES[Math.floor(Math.random() * URL_TEMPLATES.length)];
|
|
481
|
+
const refTemplate = REFERER_TEMPLATES[Math.floor(Math.random() * REFERER_TEMPLATES.length)];
|
|
482
|
+
const method = weightedRandom(HTTP_METHODS).method;
|
|
483
|
+
const status = weightedRandom(STATUS_CODES).code;
|
|
484
|
+
|
|
485
|
+
// Random tokens for realistic look
|
|
486
|
+
const randPath = toBase64Url(crypto.randomBytes(CHANNEL_SIZES.urlPath));
|
|
487
|
+
const randToken = toBase64Url(crypto.randomBytes(CHANNEL_SIZES.queryToken));
|
|
488
|
+
const randState = toBase64Url(crypto.randomBytes(CHANNEL_SIZES.queryState));
|
|
489
|
+
const randRef = toBase64Url(crypto.randomBytes(CHANNEL_SIZES.referer));
|
|
490
|
+
|
|
491
|
+
const requestPath = template.path.replace('{0}', randPath) +
|
|
492
|
+
'?' + template.query.replace('{1}', randToken).replace('{2}', randState);
|
|
493
|
+
const refererUrl = refTemplate.replace('{0}', randRef);
|
|
494
|
+
|
|
495
|
+
return [
|
|
496
|
+
randomIp(),
|
|
497
|
+
nextTimestamp(),
|
|
498
|
+
method,
|
|
499
|
+
`${method} ${requestPath} HTTP/1.1`,
|
|
500
|
+
status,
|
|
501
|
+
randomResponseBytes(status),
|
|
502
|
+
refererUrl,
|
|
503
|
+
randomUserAgent(),
|
|
504
|
+
randomFillerUuid(),
|
|
505
|
+
randomFillerTraceId(),
|
|
506
|
+
];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ─── Core Decoding ──────────────────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Check if a row is a header line (carries metadata).
|
|
513
|
+
* @param {Array} row - Row array from spreadsheet
|
|
514
|
+
* @returns {boolean}
|
|
515
|
+
*/
|
|
516
|
+
function isHeaderLine(row) {
|
|
517
|
+
const request = String(row[3] || '');
|
|
518
|
+
return request.includes(HEADER_MARKER_PATH);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Extract payload bytes from a data or header log line.
|
|
523
|
+
* @param {Array} row - Row array from spreadsheet
|
|
524
|
+
* @returns {Buffer} Extracted payload bytes (up to BYTES_PER_DATA_LINE)
|
|
525
|
+
*/
|
|
526
|
+
function extractPayloadFromLine(row) {
|
|
527
|
+
const request = String(row[3] || '');
|
|
528
|
+
const referer = String(row[6] || '');
|
|
529
|
+
const requestId = String(row[8] || '');
|
|
530
|
+
const traceId = String(row[9] || '');
|
|
531
|
+
|
|
532
|
+
const buffers = [];
|
|
533
|
+
|
|
534
|
+
// Strip HTTP version from request: "GET /path?q=v HTTP/1.1" → "/path?q=v"
|
|
535
|
+
const urlOnly = request.replace(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/, '').replace(/\s+HTTP\/[\d.]+$/, '');
|
|
536
|
+
|
|
537
|
+
// 1. URL path segment — extract the LAST base64url segment from the path
|
|
538
|
+
let pathPayload;
|
|
539
|
+
const pathOnly = urlOnly.split('?')[0]; // Remove query string
|
|
540
|
+
if (isHeaderLine(row)) {
|
|
541
|
+
// Header: /api/v1/health/{token}
|
|
542
|
+
const pathMatch = pathOnly.match(/\/api\/v1\/health\/([A-Za-z0-9_-]+)$/);
|
|
543
|
+
pathPayload = pathMatch ? pathMatch[1] : null;
|
|
544
|
+
} else {
|
|
545
|
+
// Data: token is always the last path segment after /api/v2/.../<word>/
|
|
546
|
+
const segments = pathOnly.split('/').filter(Boolean);
|
|
547
|
+
// Last segment is the token (all templates place {0} last)
|
|
548
|
+
pathPayload = segments.length > 0 ? segments[segments.length - 1] : null;
|
|
549
|
+
// Verify it looks like base64url (at least 20 chars of [A-Za-z0-9_-])
|
|
550
|
+
if (pathPayload && !/^[A-Za-z0-9_-]{20,}$/.test(pathPayload)) {
|
|
551
|
+
pathPayload = null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
buffers.push(pathPayload ? fromBase64Url(pathPayload).slice(0, CHANNEL_SIZES.urlPath) : Buffer.alloc(CHANNEL_SIZES.urlPath));
|
|
555
|
+
|
|
556
|
+
// 2. Query token param
|
|
557
|
+
const tokenMatch = urlOnly.match(/[?&]token=([A-Za-z0-9_-]+)/);
|
|
558
|
+
buffers.push(tokenMatch ? fromBase64Url(tokenMatch[1]).slice(0, CHANNEL_SIZES.queryToken) : Buffer.alloc(CHANNEL_SIZES.queryToken));
|
|
559
|
+
|
|
560
|
+
// 3. Query state param
|
|
561
|
+
const stateMatch = urlOnly.match(/[?&]state=([A-Za-z0-9_-]+)/);
|
|
562
|
+
buffers.push(stateMatch ? fromBase64Url(stateMatch[1]).slice(0, CHANNEL_SIZES.queryState) : Buffer.alloc(CHANNEL_SIZES.queryState));
|
|
563
|
+
|
|
564
|
+
// 4. Referer ref param
|
|
565
|
+
const refMatch = referer.match(/[?&]ref=([A-Za-z0-9_-]+)/);
|
|
566
|
+
buffers.push(refMatch ? fromBase64Url(refMatch[1]).slice(0, CHANNEL_SIZES.referer) : Buffer.alloc(CHANNEL_SIZES.referer));
|
|
567
|
+
|
|
568
|
+
// 5. X-Request-ID (UUID)
|
|
569
|
+
if (requestId && requestId.includes('-')) {
|
|
570
|
+
buffers.push(decodeUuidPayload(requestId));
|
|
571
|
+
} else {
|
|
572
|
+
buffers.push(Buffer.alloc(CHANNEL_SIZES.requestId));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 6. X-Trace-ID (hex)
|
|
576
|
+
if (traceId && traceId.length === 32 && /^[a-f0-9]+$/i.test(traceId)) {
|
|
577
|
+
buffers.push(decodeTraceId(traceId));
|
|
578
|
+
} else {
|
|
579
|
+
buffers.push(Buffer.alloc(CHANNEL_SIZES.traceId));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return Buffer.concat(buffers);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ─── High-Level API ─────────────────────────────────────────────────────────
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Encode a complete payload buffer into log line rows.
|
|
589
|
+
* Returns { headerRows, dataRows, fillerRows } — arrays of row arrays.
|
|
590
|
+
*
|
|
591
|
+
* @param {Buffer} payloadBuffer - The encrypted payload bytes
|
|
592
|
+
* @param {string} metadataJson - Serialized metadata JSON
|
|
593
|
+
* @param {string} encryptionMeta - Packed encryption meta (iv:salt:authTag) or ''
|
|
594
|
+
* @returns {object} { headerRows, dataRows, fillerRows }
|
|
595
|
+
*/
|
|
596
|
+
function encodePayloadToLogLines(payloadBuffer, metadataJson, encryptionMeta) {
|
|
597
|
+
// Build the header payload with length-prefixed format to avoid parsing ambiguity.
|
|
598
|
+
// Format: "STGD05|<metaLen>|<encLen>|{metadataJson}{encryptionMeta}"
|
|
599
|
+
// This way we know exactly where metadata ends and encryption meta begins.
|
|
600
|
+
const metaBytes = Buffer.from(metadataJson, 'utf8');
|
|
601
|
+
const encBytes = Buffer.from(encryptionMeta || '', 'utf8');
|
|
602
|
+
const prefix = `STGD05|${metaBytes.length}|${encBytes.length}|`;
|
|
603
|
+
const headerPayload = Buffer.concat([Buffer.from(prefix), metaBytes, encBytes]);
|
|
604
|
+
|
|
605
|
+
// Generate header lines
|
|
606
|
+
const headerLineCount = Math.ceil(headerPayload.length / BYTES_PER_DATA_LINE);
|
|
607
|
+
const headerRows = [];
|
|
608
|
+
for (let i = 0; i < headerLineCount; i++) {
|
|
609
|
+
const start = i * BYTES_PER_DATA_LINE;
|
|
610
|
+
const chunk = headerPayload.slice(start, start + BYTES_PER_DATA_LINE);
|
|
611
|
+
headerRows.push(generateHeaderLogLine(chunk));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Generate data lines
|
|
615
|
+
const dataLineCount = Math.ceil(payloadBuffer.length / BYTES_PER_DATA_LINE);
|
|
616
|
+
const dataRows = [];
|
|
617
|
+
for (let i = 0; i < dataLineCount; i++) {
|
|
618
|
+
const start = i * BYTES_PER_DATA_LINE;
|
|
619
|
+
const chunk = payloadBuffer.slice(start, start + BYTES_PER_DATA_LINE);
|
|
620
|
+
dataRows.push(generateDataLogLine(chunk));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Generate filler lines for realism
|
|
624
|
+
const fillerCount = Math.max(30, Math.floor(dataLineCount * 0.2));
|
|
625
|
+
const fillerRows = [];
|
|
626
|
+
for (let i = 0; i < fillerCount; i++) {
|
|
627
|
+
fillerRows.push(generateFillerLogLine());
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return { headerRows, dataRows, fillerRows };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Decode log line rows back into metadata and payload.
|
|
635
|
+
* @param {Array<Array>} allRows - All rows from the spreadsheet (excluding header row)
|
|
636
|
+
* @returns {object} { metadataJson, encryptionMeta, payloadBuffer }
|
|
637
|
+
*/
|
|
638
|
+
function decodeLogLines(allRows) {
|
|
639
|
+
const headerPayloadBuffers = [];
|
|
640
|
+
const dataPayloadBuffers = [];
|
|
641
|
+
|
|
642
|
+
// Separate header lines from data/filler lines
|
|
643
|
+
let headersDone = false;
|
|
644
|
+
let dataLinesRead = 0;
|
|
645
|
+
|
|
646
|
+
for (const row of allRows) {
|
|
647
|
+
if (!headersDone && isHeaderLine(row)) {
|
|
648
|
+
headerPayloadBuffers.push(extractPayloadFromLine(row));
|
|
649
|
+
} else {
|
|
650
|
+
headersDone = true;
|
|
651
|
+
// Could be data or filler — collect all, we'll truncate later
|
|
652
|
+
dataPayloadBuffers.push(extractPayloadFromLine(row));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Reassemble header payload
|
|
657
|
+
const headerPayload = Buffer.concat(headerPayloadBuffers);
|
|
658
|
+
const headerStr = headerPayload.toString('utf8');
|
|
659
|
+
|
|
660
|
+
// Parse header: "STGD05|<metaLen>|<encLen>|{metadataJson}{encryptionMeta}"
|
|
661
|
+
const markerIdx = headerStr.indexOf('STGD05|');
|
|
662
|
+
if (markerIdx === -1) {
|
|
663
|
+
throw new Error('Invalid v5 format: magic marker not found. This may not be a stegdoc v5 file.');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const afterMarker = headerStr.slice(markerIdx + 7);
|
|
667
|
+
|
|
668
|
+
// Parse length fields
|
|
669
|
+
const firstPipe = afterMarker.indexOf('|');
|
|
670
|
+
if (firstPipe === -1) {
|
|
671
|
+
throw new Error('Invalid v5 format: malformed header payload.');
|
|
672
|
+
}
|
|
673
|
+
const secondPipe = afterMarker.indexOf('|', firstPipe + 1);
|
|
674
|
+
if (secondPipe === -1) {
|
|
675
|
+
throw new Error('Invalid v5 format: malformed header payload (missing encLen).');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const metaLen = parseInt(afterMarker.slice(0, firstPipe), 10);
|
|
679
|
+
const encLen = parseInt(afterMarker.slice(firstPipe + 1, secondPipe), 10);
|
|
680
|
+
|
|
681
|
+
if (isNaN(metaLen) || isNaN(encLen)) {
|
|
682
|
+
throw new Error('Invalid v5 format: non-numeric length fields.');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Extract exact bytes using the known lengths
|
|
686
|
+
const dataStart = markerIdx + 7 + secondPipe + 1;
|
|
687
|
+
const metadataBytes = headerPayload.slice(dataStart, dataStart + metaLen);
|
|
688
|
+
const encryptionBytes = headerPayload.slice(dataStart + metaLen, dataStart + metaLen + encLen);
|
|
689
|
+
|
|
690
|
+
const metadataJson = metadataBytes.toString('utf8');
|
|
691
|
+
const encryptionMeta = encryptionBytes.toString('utf8');
|
|
692
|
+
|
|
693
|
+
// Parse metadata to get payloadSize for truncation
|
|
694
|
+
let metadata;
|
|
695
|
+
try {
|
|
696
|
+
metadata = JSON.parse(metadataJson);
|
|
697
|
+
} catch (e) {
|
|
698
|
+
throw new Error(`Failed to parse v5 metadata: ${e.message}`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const payloadSize = metadata.payloadSize;
|
|
702
|
+
if (typeof payloadSize !== 'number' || payloadSize < 0) {
|
|
703
|
+
throw new Error('Invalid v5 metadata: missing or invalid payloadSize');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Use dataLineCount from metadata to only read actual data lines (skip filler)
|
|
707
|
+
const dataLineCount = metadata.dataLineCount;
|
|
708
|
+
if (typeof dataLineCount !== 'number' || dataLineCount < 0) {
|
|
709
|
+
throw new Error('Invalid v5 metadata: missing or invalid dataLineCount');
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Re-extract only the correct number of data lines
|
|
713
|
+
const actualDataBuffers = dataPayloadBuffers.slice(0, dataLineCount);
|
|
714
|
+
const fullPayload = Buffer.concat(actualDataBuffers);
|
|
715
|
+
|
|
716
|
+
// Truncate to exact payload size
|
|
717
|
+
const payloadBuffer = fullPayload.slice(0, payloadSize);
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
metadataJson,
|
|
721
|
+
encryptionMeta: encryptionMeta || '',
|
|
722
|
+
payloadBuffer,
|
|
723
|
+
metadata,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Calculate the number of data lines needed for a payload.
|
|
731
|
+
* @param {number} payloadSizeBytes - Payload size in bytes
|
|
732
|
+
* @returns {number} Number of data lines
|
|
733
|
+
*/
|
|
734
|
+
function calculateDataLineCount(payloadSizeBytes) {
|
|
735
|
+
return Math.ceil(payloadSizeBytes / BYTES_PER_DATA_LINE);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
module.exports = {
|
|
739
|
+
// Constants
|
|
740
|
+
BYTES_PER_DATA_LINE,
|
|
741
|
+
CHANNEL_SIZES,
|
|
742
|
+
|
|
743
|
+
// Encoding
|
|
744
|
+
generateLogHeaders,
|
|
745
|
+
generateDataLogLine,
|
|
746
|
+
generateHeaderLogLine,
|
|
747
|
+
generateFillerLogLine,
|
|
748
|
+
encodePayloadToLogLines,
|
|
749
|
+
|
|
750
|
+
// Decoding
|
|
751
|
+
isHeaderLine,
|
|
752
|
+
extractPayloadFromLine,
|
|
753
|
+
decodeLogLines,
|
|
754
|
+
|
|
755
|
+
// Utilities
|
|
756
|
+
calculateDataLineCount,
|
|
757
|
+
toBase64Url,
|
|
758
|
+
fromBase64Url,
|
|
759
|
+
encodeUuidPayload,
|
|
760
|
+
decodeUuidPayload,
|
|
761
|
+
encodeTraceId,
|
|
762
|
+
decodeTraceId,
|
|
763
|
+
resetTimeState,
|
|
764
|
+
};
|