tomato-ketchup 0.4.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/README.md ADDED
@@ -0,0 +1,239 @@
1
+ # πŸ… Tomato Ketchup
2
+
3
+ A steganography tool that hides encrypted data inside MP4 video files using `free` atoms β€” like squeezing tomatoes into ketchup bottles.
4
+
5
+ ## 🎯 What it does
6
+
7
+ - **Squeeze**: Hide files/folders inside multiple MP4 videos with dual-layer encryption
8
+ - **Pour**: Extract and decrypt the hidden data from MP4 videos
9
+ - **Stealth**: Uses MP4 `free` atoms (padding) to hide data without breaking video playback
10
+ - **Security**: AES-256-GCM + AES-256-encrypted ZIP for dual-layer protection
11
+
12
+ ## πŸ“¦ Installation
13
+
14
+ ```bash
15
+ npm install -g tomato-ketchup
16
+ ```
17
+
18
+ ### Prerequisites
19
+
20
+ **7-Zip** is required for auto-extraction:
21
+
22
+ - **macOS**: `brew install p7zip`
23
+ - **Linux**: `apt install p7zip-full` or `yum install p7zip`
24
+ - **Windows**: Download from [7-zip.org](https://www.7-zip.org/)
25
+
26
+ Verify installation:
27
+
28
+ ```bash
29
+ 7z --help
30
+ ```
31
+
32
+ ## πŸš€ Quick Start
33
+
34
+ ### 1. Squeeze (Hide data)
35
+
36
+ Hide your files inside MP4 videos:
37
+
38
+ ```bash
39
+ tomato-ketchup squeeze ./my-secret-folder \
40
+ --password my-hiding-secret \
41
+ --zip-password my-zip-secret \
42
+ --ratio 0.1 \
43
+ --output ./videos
44
+ ```
45
+
46
+ **Options:**
47
+
48
+ - `-p, --password <password>` β€” Secret for MP4 embedding (required)
49
+ - `--zip-password <password>` β€” Password for ZIP encryption (required)
50
+ - `-r, --ratio <ratio>` β€” Data per video ratio, 0.01~0.5 (default: 0.1)
51
+ - `-o, --output <dir>` β€” Output directory (default: `<path>.ketchup`)
52
+ - `--video-dir <dir>` β€” Use custom MP4 videos instead of downloading
53
+
54
+ **What happens:**
55
+
56
+ 1. Compresses your data into an AES-256-encrypted ZIP
57
+ 2. Encrypts the ZIP with AES-256-GCM (dual encryption)
58
+ 3. Splits the encrypted data into chunks
59
+ 4. Embeds chunks into MP4 `free` atoms across multiple videos
60
+ 5. Videos remain playable β€” data is hidden in padding
61
+
62
+ ### 2. Pour (Extract data)
63
+
64
+ Extract and decrypt the hidden data:
65
+
66
+ ```bash
67
+ tomato-ketchup pour ./videos \
68
+ --password my-hiding-secret \
69
+ --zip-password my-zip-secret
70
+ ```
71
+
72
+ **Options:**
73
+
74
+ - `-p, --password <password>` β€” Secret for MP4 extraction (required)
75
+ - `--zip-password <password>` β€” Password for auto-extraction (required unless `--no-extract`)
76
+ - `-o, --output <path>` β€” Output path (default: `<dir>/recovered.zip`)
77
+ - `--no-extract` β€” Skip auto-extraction, keep `recovered.zip` only
78
+
79
+ **What happens:**
80
+
81
+ 1. Scans MP4 files for hidden chunks
82
+ 2. Reassembles and decrypts with AES-256-GCM
83
+ 3. Saves the encrypted ZIP file
84
+ 4. **Auto-extracts** the ZIP with 7-Zip (if `--zip-password` provided)
85
+ 5. Your original files are restored
86
+
87
+ **Manual extraction** (if 7z not available or `--no-extract` used):
88
+
89
+ ```bash
90
+ 7z x -p<zip-password> recovered.zip -o./output
91
+ ```
92
+
93
+ ## πŸ“– Examples
94
+
95
+ ### Basic workflow
96
+
97
+ ```bash
98
+ # Squeeze: Hide a folder
99
+ tomato-ketchup squeeze ~/Documents/secrets \
100
+ --password hideMe2024 \
101
+ --zip-password zipSecret123 \
102
+ --output ./ketchup-bottles
103
+
104
+ # Output:
105
+ # === Tomato ketchup is ready! ===
106
+ # Shelf: ./ketchup-bottles
107
+ # Bottles: 3
108
+ # Contents:
109
+ # ./ketchup-bottles/video_0.mp4
110
+ # ./ketchup-bottles/video_1.mp4
111
+ # ./ketchup-bottles/video_2.mp4
112
+
113
+ # Pour: Extract the data
114
+ tomato-ketchup pour ./ketchup-bottles \
115
+ --password hideMe2024 \
116
+ --zip-password zipSecret123
117
+
118
+ # Output:
119
+ # === Tomato ketchup is poured! ===
120
+ # Output: ./ketchup-bottles/recovered.zip
121
+ # Bottles: 3
122
+ # Size: 15.24 MB
123
+ #
124
+ # Extracting...
125
+ # Extracted: ./ketchup-bottles/recovered/
126
+ ```
127
+
128
+ ### Without auto-extraction
129
+
130
+ ```bash
131
+ # Pour without extraction
132
+ tomato-ketchup pour ./ketchup-bottles \
133
+ --password hideMe2024 \
134
+ --no-extract
135
+
136
+ # Then manually extract:
137
+ 7z x -pzipSecret123 ./ketchup-bottles/recovered.zip
138
+ ```
139
+
140
+ ### Using custom videos
141
+
142
+ ```bash
143
+ # Provide your own MP4 files
144
+ tomato-ketchup squeeze ./data \
145
+ --password secret \
146
+ --zip-password zipSecret \
147
+ --video-dir ./my-videos
148
+ ```
149
+
150
+ ## πŸ” How it works
151
+
152
+ ### Dual-layer encryption
153
+
154
+ 1. **ZIP Layer (AES-256)**
155
+ - Your files are compressed into an AES-256-encrypted ZIP
156
+ - Password-protected with `--zip-password`
157
+ - Even if extracted, ZIP cannot be opened without password
158
+
159
+ 2. **Embedding Layer (AES-256-GCM)**
160
+ - ZIP is encrypted again with AES-256-GCM
161
+ - Password-protected with `--password`
162
+ - PBKDF2 (100,000 iterations) for key derivation
163
+ - Authentication tag prevents tampering
164
+
165
+ ### MP4 steganography
166
+
167
+ - **`free` atoms**: MP4 files contain `free`/`skip` atoms used for padding
168
+ - **Standards-compliant**: Video players ignore unknown atoms
169
+ - **Chunk distribution**: Data is split and distributed across multiple videos
170
+ - **HMAC markers**: Chunks are identified using password-derived HMAC (no fixed magic bytes)
171
+ - **Playback intact**: Videos remain fully playable after embedding
172
+
173
+ ### Workflow diagram
174
+
175
+ ```text
176
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
177
+ β”‚ Your Files β”‚
178
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
179
+ β”‚
180
+ β–Ό
181
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
182
+ β”‚ AES-256 ZIP β”‚ (archiver-zip-encrypted)
183
+ β”‚ --zip-password β”‚
184
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
185
+ β”‚
186
+ β–Ό
187
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
188
+ β”‚ AES-256-GCM β”‚ (crypto.ts)
189
+ β”‚ --password β”‚
190
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
191
+ β”‚
192
+ β–Ό
193
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
194
+ β”‚ Split chunks β”‚ (chunk.ts)
195
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
196
+ β”‚
197
+ β–Ό
198
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
199
+ β”‚ MP4 free atoms β”‚ (squeeze.ts)
200
+ β”‚ Multiple videos β”‚
201
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
202
+ ```
203
+
204
+ ## πŸ› οΈ Development
205
+
206
+ ```bash
207
+ # Install dependencies
208
+ pnpm install
209
+
210
+ # Run tests
211
+ pnpm test
212
+
213
+ # Build
214
+ pnpm build
215
+
216
+ # Run locally
217
+ tsx src/core-cli.ts squeeze ./test-data --password test --zip-password test
218
+ ```
219
+
220
+ ## 🚒 Release
221
+
222
+ Feature λ³€κ²½ ν›„ μƒˆ 버전을 λ°°ν¬ν•˜λŠ” μˆœμ„œ:
223
+
224
+ ```bash
225
+ # 1. μ½”λ“œ μˆ˜μ • ν›„ ν…ŒμŠ€νŠΈ & νƒ€μž…μ²΄ν¬ 확인
226
+ pnpm test
227
+ pnpm typecheck
228
+
229
+ # 2. release-it으둜 버전 bump + λΉŒλ“œ + npm publish + git tag ν•œλ²ˆμ— 처리
230
+ pnpm release
231
+ ```
232
+
233
+ `pnpm release` μ‹€ν–‰ μ‹œ μžλ™μœΌλ‘œ μˆ˜ν–‰λ˜λŠ” μž‘μ—…:
234
+
235
+ 1. ν…ŒμŠ€νŠΈ & νƒ€μž…μ²΄ν¬ (before:init hook)
236
+ 2. 버전 bump (package.json)
237
+ 3. λΉŒλ“œ (after:bump hook)
238
+ 4. npm publish
239
+ 5. git commit & tag 생성
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import{createRequire as e}from"node:module";import{createWriteStream as t,existsSync as n}from"node:fs";import{cac as r}from"cac";import{chmod as i,mkdir as a,readFile as o,writeFile as s}from"node:fs/promises";import{dirname as c,join as l}from"node:path";import{spawn as u}from"node:child_process";import{get as d}from"node:https";import{pipeline as f}from"node:stream/promises";function p(){let e=process.env.HOME||process.env.USERPROFILE;if(!e)throw Error(`Could not determine home directory`);return l(e,`.tomato-ketchup`,`config.json`)}async function m(){let e=p();if(!n(e))return null;try{let t=await o(e,`utf-8`);return JSON.parse(t)}catch(e){return console.warn(`Failed to load config: ${e}`),null}}async function h(e){let t=p();await a(c(t),{recursive:!0}),await s(t,JSON.stringify(e,null,2),`utf-8`)}async function g(){return(await m())?.binaryPath||null}async function _(){return(await m())?.githubToken||null}function v(e){switch(e){case`darwin`:return`macos`;case`linux`:return`linux`;case`win32`:return`win`;default:throw Error(`Unsupported platform: ${e}`)}}function y(e){if(e===`x64`||e===`arm64`)return e;throw Error(`Unsupported architecture: ${e}`)}function b(e,t){return`tomato-ketchup-${e}-${t}${e===`win`?`.exe`:``}`}function x(){let e=v(process.platform),t=y(process.arch);return{platform:e,arch:t,binaryName:b(e,t)}}function S(e){let t=process.env.HOME||process.env.USERPROFILE;if(!t)throw Error(`Could not determine home directory`);switch(e){case`macos`:case`linux`:return`${t}/.local/bin`;case`win`:return`${process.env.LOCALAPPDATA}\\Programs\\tomato-ketchup`}}const C=e(import.meta.url);function w(){let{version:e}=C(`../../package.json`);return e}function T(e){return e||process.env.GITHUB_TOKEN||null}async function E(e,n,r={}){let i=new URL(e);return new Promise((e,a)=>{d({hostname:i.hostname,path:i.pathname+i.search,headers:{"User-Agent":`tomato-ketchup-installer`,...r}},r=>{if(r.statusCode===302||r.statusCode===301){let t=r.headers.location;if(!t){a(Error(`Redirect location not found`));return}E(t,n).then(e).catch(a);return}if(r.statusCode!==200){a(Error(`HTTP ${r.statusCode}: ${r.statusMessage}`));return}let i=Number.parseInt(r.headers[`content-length`]||`0`,10),o=0;r.on(`data`,e=>{o+=e.length;let t=(o/i*100).toFixed(1),n=(o/1024/1024).toFixed(1),r=(i/1024/1024).toFixed(1);process.stdout.write(`\r Downloading... ${t}% (${n}/${r} MB)`)}),f(r,t(n)).then(()=>{console.log(``),e()}).catch(a)}).on(`error`,a)})}function D(e,t){let n=l(e,`tomato-ketchup`);console.log(`
3
+ βœ… Installation complete!
4
+ `),console.log(` Binary installed at: ${n}`),t===`win`?(console.log(`
5
+ Add to PATH manually:`),console.log(` 1. Open 'Environment Variables' settings`),console.log(` 2. Add '${e}' to your PATH`),console.log(` 3. Restart your terminal`)):(console.log(`
6
+ Add to PATH if not already:`),console.log(` export PATH="${e}:$PATH"`),console.log(`
7
+ Restart your terminal or run:`),console.log(` source ~/.zshrc # or ~/.bashrc`)),console.log(`
8
+ Usage:`),console.log(` tomato-ketchup squeeze ./data --password secret --zip-password zip123`),console.log(` tomato-ketchup pour ./videos --password secret --zip-password zip123`)}async function O(e={}){let{platform:t,binaryName:n}=x(),r=w(),o=await _(),s=T(e.token)||o;console.log(`πŸ… Installing tomato-ketchup binary...
9
+ `),console.log(` Platform: ${t}`),console.log(` Version: v${r}`),console.log(` Binary: ${n}`),console.log(` Auth: ${s?`GitHub token`:`none (public)`}\n`);let c=await g();if(c&&!e.force){console.log(`βœ“ Already installed at: ${c}`),console.log(`
10
+ Use --force to reinstall`);return}s||(console.log(`⚠️ No GitHub token provided. If the repo is private, download will fail.`),console.log(` Use: tomato-ketchup init --token ghp_xxx or set GITHUB_TOKEN env var
11
+ `));let u=S(t);await a(u,{recursive:!0});let d=l(u,t===`win`?`tomato-ketchup.exe`:`tomato-ketchup`);console.log(`Fetching release info...`);let f=await k(r,n,s);console.log(`Downloading: ${n}`);let p={Accept:`application/octet-stream`};s&&(p.Authorization=`token ${s}`),await E(f,d,p),t!==`win`&&(await i(d,493),console.log(`βœ“ Executable permissions granted`)),await h({version:r,installedAt:new Date().toISOString(),binaryPath:d,...s?{githubToken:s}:{}}),D(u,t)}async function k(e,t,n){let r=`https://api.github.com/repos/thilllon/tomato-ketchup/releases/tags/v${e}`,i=new URL(r);return new Promise((r,a)=>{let o={"User-Agent":`tomato-ketchup-installer`,Accept:`application/vnd.github.v3+json`};n&&(o.Authorization=`token ${n}`),d({hostname:i.hostname,path:i.pathname,headers:o},n=>{let i=``;n.on(`data`,e=>{i+=e.toString()}),n.on(`end`,()=>{if(n.statusCode!==200){a(Error(`Failed to fetch release info: HTTP ${n.statusCode}\n${i}`));return}try{let n=JSON.parse(i),o=n.assets?.find(e=>e.name===t);if(!o){a(Error(`Binary "${t}" not found in release v${e}.\nAvailable: ${(n.assets||[]).map(e=>e.name).join(`, `)||`none`}`));return}r(o.url)}catch(e){a(Error(`Failed to parse release info: ${e}`))}})}).on(`error`,a)})}async function A(){let e=await m();e||(console.error(`❌ tomato-ketchup not installed. Run: tomato-ketchup init`),process.exit(1));let t=w(),n=e.version;if(console.log(`Current version: ${n}`),console.log(`Latest version: ${t}`),n===t){console.log(`
12
+ βœ“ Already up to date!`);return}console.log(`
13
+ πŸ… Updating...
14
+ `),await O({force:!0,token:e.githubToken})}async function j(){return await g()}const{version:M}=e(import.meta.url)(`../../package.json`),N=r(`tomato-ketchup`);N.command(`init`,`Install tomato-ketchup binary`).option(`--force`,`Force reinstall even if already installed`).option(`--token <token>`,`GitHub personal access token (for private repos)`).action(async e=>{try{await O({force:e.force,token:e.token})}catch(e){console.error(`❌ Installation failed:`,e instanceof Error?e.message:e),process.exit(1)}}),N.command(`update`,`Update to the latest version`).action(async()=>{try{await A()}catch(e){console.error(`❌ Update failed:`,e instanceof Error?e.message:e),process.exit(1)}}),N.command(`[...args]`,`Forward command to installed binary`,{ignoreOptionDefaultValue:!0}).allowUnknownOptions().action(async e=>{let t=await j();t||(console.error(`❌ tomato-ketchup not installed.
15
+ `),console.log(`Run first:`),console.log(` npx tomato-ketchup init
16
+ `),console.log(`Or:`),console.log(` npm install -g tomato-ketchup`),console.log(` tomato-ketchup init`),process.exit(1));let n=u(t,e,{stdio:`inherit`,env:process.env});n.on(`exit`,e=>{process.exit(e??0)}),n.on(`error`,e=>{console.error(`Failed to execute binary:`,e),process.exit(1)})}),N.help(),N.version(M);const P=N.parse();!N.matchedCommand&&!P.options.help&&!P.options.version&&(console.error(`❌ No command specified.
17
+ `),console.log(`Available commands:`),console.log(` init - Install binary`),console.log(` update - Update to latest version`),console.log(` squeeze - Hide data in MP4 videos`),console.log(` pour - Extract data from MP4 videos
18
+ `),console.log(`Run 'tomato-ketchup --help' for more information`),process.exit(1));export{};
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import{a as e,n as t,o as n,t as r}from"./squeeze-CaREQmbk.mjs";import{createRequire as i}from"node:module";import{unlinkSync as a}from"node:fs";import{cac as o}from"cac";const{version:s}=i(import.meta.url)(`../package.json`),c=o(`tomato-ketchup`);c.command(`squeeze <path>`,`Squeeze tomatoes into bottles`).option(`-p, --password <password>`,`Secret recipe for hiding (required)`).option(`--zip-password <password>`,`Password for the ZIP file (required)`).option(`-r, --ratio <ratio>`,`How much tomato per bottle (0.01~0.5)`,{default:.1}).option(`-o, --output <dir>`,`Output shelf (default: <path>.ketchup)`).option(`--video-dir <dir>`,`Bring your own bottles`).action(async(e,t)=>{t.password??(console.error(`Error: --password is required. Every recipe needs a secret ingredient.`),process.exit(1)),t.zipPassword??(console.error(`Error: --zip-password is required. The ZIP needs its own lock.`),process.exit(1));let n=String(t.password),i=String(t.zipPassword),a=Number(t.ratio);(Number.isNaN(a)||a<.01||a>.5)&&(console.error(`Error: --ratio must be between 0.01 and 0.5.`),process.exit(1));let o=t.output??`${e}.ketchup`;try{let s=await r({dataPath:e,outputDir:o,password:n,zipPassword:i,ratio:a,videoDir:t.videoDir});console.log(``),console.log(`=== Tomato ketchup is ready! ===`),console.log(` Shelf: ${s.outputDir}`),console.log(` Bottles: ${s.totalVideos}`),console.log(` Contents:`);for(let e of s.outputFiles)console.log(` ${e}`)}catch(e){console.error(`Error:`,e instanceof Error?e.message:e),process.exit(1)}}),c.command(`pour <dir>`,`Pour ketchup out of bottles`).option(`-p, --password <password>`,`Secret recipe (required)`).option(`-o, --output <path>`,`Output path (default: <dir>/recovered.zip)`).option(`--zip-password <password>`,`Password for the ZIP file (required for auto-extraction)`).option(`--no-extract`,`Skip auto-extraction, keep recovered.zip only`,{default:!1}).action(async(r,i)=>{i.password??(console.error(`Error: --password is required. Wrong recipe, no ketchup.`),process.exit(1));let o=String(i.password),s=i.output??`${r}/recovered.zip`;try{let c=await t(r,o,s);if(console.log(``),console.log(`=== Tomato ketchup is poured! ===`),console.log(` Output: ${c.outputPath}`),console.log(` Bottles: ${c.videoCount}`),console.log(` Size: ${(c.dataSize/1024/1024).toFixed(2)} MB`),i.noExtract)console.log(`\n Use 7-Zip to unzip with password: 7z x -p<password> "${c.outputPath}"`);else{i.zipPassword??(console.error(`
3
+ ❌ Error: --zip-password is required for auto-extraction.`),console.log(`
4
+ Your encrypted ZIP is ready at:`),console.log(` ${c.outputPath}`),console.log(`
5
+ Options:`),console.log(` 1. Re-run with --zip-password to auto-extract:`),console.log(` tomato-ketchup pour "${r}" --password <password> --zip-password <zip-password>`),console.log(`
6
+ 2. Or extract manually with 7-Zip:`),console.log(` 7z x -p<zip-password> "${c.outputPath}" -o<output_dir>`),console.log(`
7
+ 3. Or use --no-extract to skip this message:`),console.log(` tomato-ketchup pour "${r}" --password <password> --no-extract`),process.exit(1));let t=String(i.zipPassword),o=s.replace(/\.zip$/i,``);if(n()){console.log(`
8
+ Extracting...`);try{e(s,t,o),a(s),console.log(` βœ… Extracted: ${o}`)}catch(e){console.error(`\n❌ Extraction failed: ${e instanceof Error?e.message:e}`),console.log(`
9
+ Possible causes:`),console.log(` β€’ Wrong --zip-password`),console.log(` β€’ Corrupted ZIP file`),console.log(` β€’ Insufficient disk space`),console.log(`
10
+ Your encrypted ZIP is still available at:`),console.log(` ${s}`),console.log(`
11
+ Try extracting manually:`),console.log(` 7z x -p<zip-password> "${s}" -o"${o}"`),process.exit(1)}}else console.warn(`
12
+ ⚠️ 7-Zip not found β€” auto-extraction skipped`),console.log(`
13
+ Your encrypted ZIP is ready at:`),console.log(` ${s}`),console.log(`
14
+ To extract it, install 7-Zip first:`),console.log(` macOS: brew install p7zip`),console.log(` Linux: apt install p7zip-full`),console.log(` Windows: https://www.7-zip.org/download.html`),console.log(`
15
+ Then extract with:`),console.log(` 7z x -p<zip-password> "${s}" -o"${o}"`)}}catch(e){console.error(`Error:`,e instanceof Error?e.message:e),process.exit(1)}}),c.help(),c.version(s);const l=c.parse();!c.matchedCommand&&!l.options.help&&!l.options.version&&(console.error(`Error: command is required. Use --help to see available commands.
16
+ `),c.outputHelp(),process.exit(1));export{};
@@ -0,0 +1,170 @@
1
+ //#region src/crypto.d.ts
2
+ interface EncryptedData {
3
+ /** PBKDF2 salt (16λ°”μ΄νŠΈ) */
4
+ salt: Buffer;
5
+ /** AES-GCM IV/nonce (12λ°”μ΄νŠΈ) */
6
+ iv: Buffer;
7
+ /** GCM 인증 νƒœκ·Έ (16λ°”μ΄νŠΈ) */
8
+ authTag: Buffer;
9
+ /** μ•”ν˜Έλ¬Έ */
10
+ ciphertext: Buffer;
11
+ }
12
+ declare class Encryptor {
13
+ private readonly password;
14
+ constructor(password: string);
15
+ /**
16
+ * 데이터λ₯Ό AES-256-GCM으둜 μ•”ν˜Έν™”ν•©λ‹ˆλ‹€.
17
+ */
18
+ encrypt(data: Buffer): EncryptedData;
19
+ /**
20
+ * μ•”ν˜Έν™” ν›„ 단일 Buffer둜 νŒ¨ν‚Ήν•©λ‹ˆλ‹€.
21
+ * λ ˆμ΄μ•„μ›ƒ: [salt 16B][iv 12B][authTag 16B][ciphertext NB]
22
+ */
23
+ encryptToBuffer(data: Buffer): Buffer;
24
+ /**
25
+ * νŠΉμ • chunk index에 λŒ€ν•œ HMAC 기반 마컀λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
26
+ * νŒ¨μŠ€μ›Œλ“œ μ—†μ΄λŠ” 마컀λ₯Ό μž¬ν˜„ν•  수 μ—†μœΌλ―€λ‘œ 은닉 chunk 식별에 μ‚¬μš©λ©λ‹ˆλ‹€.
27
+ */
28
+ generateMarker(chunkIndex: number): Buffer;
29
+ }
30
+ declare class Decryptor {
31
+ private readonly password;
32
+ constructor(password: string);
33
+ /**
34
+ * AES-256-GCM μ•”ν˜Έλ¬Έμ„ λ³΅ν˜Έν™”ν•©λ‹ˆλ‹€.
35
+ * 잘λͺ»λœ νŒ¨μŠ€μ›Œλ“œλ‚˜ λ³€μ‘°λœ λ°μ΄ν„°λŠ” GCM 인증 μ‹€νŒ¨λ‘œ μ—λŸ¬κ°€ λ°œμƒν•©λ‹ˆλ‹€.
36
+ */
37
+ decrypt(encrypted: EncryptedData): Buffer;
38
+ /**
39
+ * νŒ¨ν‚Ήλœ Bufferμ—μ„œ salt, iv, authTag, ciphertextλ₯Ό μΆ”μΆœν•˜κ³  λ³΅ν˜Έν™”ν•©λ‹ˆλ‹€.
40
+ * λ ˆμ΄μ•„μ›ƒ: [salt 16B][iv 12B][authTag 16B][ciphertext NB]
41
+ */
42
+ decryptFromBuffer(packed: Buffer): Buffer;
43
+ /**
44
+ * νŠΉμ • chunk index에 λŒ€ν•œ HMAC 기반 마컀λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
45
+ * Encryptor.generateMarker와 λ™μΌν•œ κ²°κ³Όλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
46
+ */
47
+ generateMarker(chunkIndex: number): Buffer;
48
+ /**
49
+ * μ£Όμ–΄μ§„ λ°μ΄ν„°μ˜ μ•ž 8λ°”μ΄νŠΈκ°€ νŠΉμ • chunk index의 λ§ˆμ»€μ™€ μΌμΉ˜ν•˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.
50
+ */
51
+ verifyMarker(data: Buffer, chunkIndex: number): boolean;
52
+ }
53
+ //#endregion
54
+ //#region src/pour.d.ts
55
+ interface PourResult {
56
+ outputPath: string;
57
+ chunkCount: number;
58
+ dataSize: number;
59
+ videoCount?: number;
60
+ }
61
+ /**
62
+ * 디렉토리 λ‚΄ μ—¬λŸ¬ MP4μ—μ„œ λΆ„μ‚° μ€λ‹‰λœ 데이터λ₯Ό μΆ”μΆœν•©λ‹ˆλ‹€.
63
+ *
64
+ * 1. 디렉토리 λ‚΄ λͺ¨λ“  MP4 μŠ€μΊ”
65
+ * 2. 각 MP4μ—μ„œ free atom μΆ”μΆœ + 마컀 검증
66
+ * 3. video_index κΈ°μ€€ μ •λ ¬
67
+ * 4. λΉ„λ””μ˜€λ³„ 데이터λ₯Ό μˆœμ„œλŒ€λ‘œ μ—°κ²°
68
+ * 5. AES-256-GCM λ³΅ν˜Έν™” -> ZIP 좜λ ₯
69
+ */
70
+ declare function pourFromDirectory(dirPath: string, password: string, outputPath: string): Promise<PourResult>;
71
+ //#endregion
72
+ //#region src/squeeze.d.ts
73
+ interface SqueezeOptions {
74
+ /** 숨길 파일 λ˜λŠ” 폴더 경둜 */
75
+ dataPath: string;
76
+ /** 좜λ ₯ 디렉토리 경둜 */
77
+ outputDir: string;
78
+ /** 은닉 μ•”ν˜Έν™” νŒ¨μŠ€μ›Œλ“œ (MP4에 숨길 λ•Œ μ‚¬μš©) */
79
+ password: string;
80
+ /** ZIP 파일 μ•”ν˜Έν™” νŒ¨μŠ€μ›Œλ“œ (μΆ”μΆœ ν›„ ZIP을 μ—΄ λ•Œ ν•„μš”) */
81
+ zipPassword: string;
82
+ /** 은닉 λΉ„μœ¨ (0.05 ~ 0.5, κΈ°λ³Έ 0.1) */
83
+ ratio: number;
84
+ /** μ‚¬μš©μž 제곡 λΉ„λ””μ˜€ 디렉토리 (선택) */
85
+ videoDir?: string;
86
+ }
87
+ interface SqueezeResult {
88
+ outputDir: string;
89
+ totalVideos: number;
90
+ outputFiles: string[];
91
+ originalDataSize: number;
92
+ encryptedDataSize: number;
93
+ totalOutputSize: number;
94
+ }
95
+ /**
96
+ * 파일/폴더λ₯Ό μ—¬λŸ¬ MP4 λΉ„λ””μ˜€μ— λΆ„μ‚° μ€λ‹‰ν•©λ‹ˆλ‹€.
97
+ *
98
+ * 1. 데이터 μ••μΆ• + μ•”ν˜Έν™”
99
+ * 2. λΉ„μœ¨μ— 맞게 ν•„μš”ν•œ λΉ„λ””μ˜€ 수 계산
100
+ * 3. μƒ˜ν”Œ λΉ„λ””μ˜€ λ‹€μš΄λ‘œλ“œ (λ˜λŠ” μ‚¬μš©μž λΉ„λ””μ˜€ μ‚¬μš©)
101
+ * 4. μ•”ν˜Έν™” 데이터λ₯Ό λΉ„λ””μ˜€λ³„λ‘œ λΆ„ν• 
102
+ * 5. 각 λΉ„λ””μ˜€μ— ν•΄λ‹Ή 파트 μ‚½μž…
103
+ * 6. 번호 λΆ™μ—¬ 좜λ ₯ 디렉토리에 μ €μž₯
104
+ */
105
+ declare function squeeze(options: SqueezeOptions): Promise<SqueezeResult>;
106
+ //#endregion
107
+ //#region src/utils.d.ts
108
+ interface CompressAndEncryptOptions {
109
+ /** μ••μΆ•ν•  파일 λ˜λŠ” 폴더 경둜 (μƒλŒ€ 경둜 λ˜λŠ” μ ˆλŒ€ 경둜) */
110
+ inputPath: string;
111
+ /** 좜λ ₯ 파일 경둜. μ§€μ •ν•˜λ©΄ 파일둜 μ €μž₯됨 */
112
+ outputPath?: string;
113
+ /** AES-256-GCM 은닉 μ•”ν˜Έν™” νŒ¨μŠ€μ›Œλ“œ (MP4에 숨길 λ•Œ μ‚¬μš©) */
114
+ password: string;
115
+ /** ZIP 파일 μ•”ν˜Έν™” νŒ¨μŠ€μ›Œλ“œ (μΆ”μΆœ ν›„ ZIP을 μ—΄ λ•Œ ν•„μš”) */
116
+ zipPassword: string;
117
+ }
118
+ interface CompressAndEncryptResult {
119
+ /** μ•”ν˜Έν™”λœ 데이터 (Buffer) */
120
+ encrypted: Buffer;
121
+ /** 원본 ZIP 크기 (λ°”μ΄νŠΈ) */
122
+ originalSize: number;
123
+ /** μ•”ν˜Έν™” ν›„ 크기 (λ°”μ΄νŠΈ) */
124
+ encryptedSize: number;
125
+ /** 좜λ ₯ 파일 경둜 (outputPath μ§€μ • μ‹œ) */
126
+ outputPath?: string;
127
+ }
128
+ interface DecryptAndDecompressOptions {
129
+ /** μ•”ν˜Έν™”λœ 파일 경둜 */
130
+ inputPath: string;
131
+ /** 좜λ ₯ 경둜 (ZIP 파일둜 μ €μž₯됨, 이후 7z으둜 ν’€κΈ°) */
132
+ outputPath: string;
133
+ /** AES-256-GCM λ³΅ν˜Έν™” νŒ¨μŠ€μ›Œλ“œ */
134
+ password: string;
135
+ }
136
+ /**
137
+ * 파일 λ˜λŠ” 폴더λ₯Ό AES-256 μ•”ν˜Έν™” ZIP으둜 μ••μΆ•ν•˜κ³  AES-256-GCM으둜 이쀑 μ•”ν˜Έν™”ν•©λ‹ˆλ‹€.
138
+ *
139
+ * - ZIP μžμ²΄κ°€ AES-256으둜 μ•”ν˜Έν™”λ˜μ–΄, μΆ”μΆœ 후에도 λΉ„λ°€λ²ˆν˜Έ 없이 μ—΄ 수 μ—†μŒ
140
+ * - AES-256-GCM: 256λΉ„νŠΈ ν‚€ + 인증 νƒœκ·Έλ‘œ λ³€μ‘° 감지 (이쀑 보호)
141
+ * - PBKDF2(100,000회) ν‚€ μœ λ„λ‘œ 브루트포슀 λ°©μ–΄
142
+ * - archiver + archiver-zip-encrypted μ‚¬μš©
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * const result = await compressAndEncrypt({
147
+ * inputPath: './my-folder',
148
+ * outputPath: './my-folder.enc',
149
+ * password: 'super-secret',
150
+ * });
151
+ * ```
152
+ */
153
+ declare function compressAndEncrypt(options: CompressAndEncryptOptions): Promise<CompressAndEncryptResult>;
154
+ /**
155
+ * AES-256-GCM으둜 μ•”ν˜Έν™”λœ νŒŒμΌμ„ λ³΅ν˜Έν™”ν•˜μ—¬ 원본 ZIP을 λ³΅μ›ν•©λ‹ˆλ‹€.
156
+ * λ³΅μ›λœ ZIP은 AES-256으둜 μ•”ν˜Έν™”λ˜μ–΄ μžˆμ–΄, 7-Zip λ“±μœΌλ‘œ λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•΄μ•Ό μ—΄ 수 μžˆμŠ΅λ‹ˆλ‹€.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * await decryptAndSave({
161
+ * inputPath: './my-folder.enc',
162
+ * outputPath: './recovered.zip',
163
+ * password: 'super-secret',
164
+ * });
165
+ * // 이후: 7z x -p<password> recovered.zip
166
+ * ```
167
+ */
168
+ declare function decryptAndSave(options: DecryptAndDecompressOptions): Promise<void>;
169
+ //#endregion
170
+ export { Decryptor, Encryptor, type PourResult, type SqueezeOptions, type SqueezeResult, compressAndEncrypt, decryptAndSave, pourFromDirectory, squeeze };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import{c as e,i as t,n,r,s as i,t as a}from"./squeeze-CaREQmbk.mjs";export{i as Decryptor,e as Encryptor,r as compressAndEncrypt,t as decryptAndSave,n as pourFromDirectory,a as squeeze};
@@ -0,0 +1 @@
1
+ import{createWriteStream as e}from"node:fs";import{mkdir as t,open as n,readFile as r,readdir as i,stat as a,writeFile as o}from"node:fs/promises";import{basename as s,dirname as c,join as l,resolve as u}from"node:path";import d from"debug";import{createCipheriv as f,createDecipheriv as p,createHmac as m,hkdfSync as h,pbkdf2Sync as g,randomBytes as _,randomInt as v}from"node:crypto";import{execFileSync as y}from"node:child_process";import{PassThrough as b}from"node:stream";import x from"archiver";import S from"archiver-zip-encrypted";import{homedir as C}from"node:os";const w=`sha256`;var T=class{password;constructor(e){if(!e||e.length===0)throw Error(`νŒ¨μŠ€μ›Œλ“œλŠ” λΉ„μ–΄ μžˆμ„ 수 μ—†μŠ΅λ‹ˆλ‹€.`);this.password=e}encrypt(e){let t=_(16),n=_(12),r=f(`aes-256-gcm`,D(this.password,t),n),i=Buffer.concat([r.update(e),r.final()]);return{salt:t,iv:n,authTag:r.getAuthTag(),ciphertext:i}}encryptToBuffer(e){let{salt:t,iv:n,authTag:r,ciphertext:i}=this.encrypt(e);return Buffer.concat([t,n,r,i])}generateMarker(e){let t=O(this.password),n=Buffer.alloc(4);n.writeUInt32LE(e);let r=m(`sha256`,t);return r.update(n),r.digest().subarray(0,8)}},E=class{password;constructor(e){if(!e||e.length===0)throw Error(`νŒ¨μŠ€μ›Œλ“œλŠ” λΉ„μ–΄ μžˆμ„ 수 μ—†μŠ΅λ‹ˆλ‹€.`);this.password=e}decrypt(e){let t=p(`aes-256-gcm`,D(this.password,e.salt),e.iv);return t.setAuthTag(e.authTag),Buffer.concat([t.update(e.ciphertext),t.final()])}decryptFromBuffer(e){if(e.length<44)throw Error(`νŒ¨ν‚Ήλœ 데이터가 λ„ˆλ¬΄ μ§§μŠ΅λ‹ˆλ‹€. μ΅œμ†Œ 44λ°”μ΄νŠΈ ν•„μš”, μ‹€μ œ ${e.length}λ°”μ΄νŠΈ`);let t=0,n=e.subarray(t,t+16);t+=16;let r=e.subarray(t,t+12);t+=12;let i=e.subarray(t,t+16);t+=16;let a=e.subarray(t);return this.decrypt({salt:n,iv:r,authTag:i,ciphertext:a})}generateMarker(e){let t=O(this.password),n=Buffer.alloc(4);n.writeUInt32LE(e);let r=m(`sha256`,t);return r.update(n),r.digest().subarray(0,8)}verifyMarker(e,t){if(e.length<8)return!1;let n=this.generateMarker(t);return e.subarray(0,8).equals(n)}};function D(e,t){return g(e,t,1e5,32,w)}function O(e){return Buffer.from(h(w,e,``,`tomato-ketchup-marker`,32))}function k(e,t,n,r,i){let a=i?.minChunkSize??524288,o=i?.maxChunkSize??2097152,s=te(e.length,a,o),c=s.length,l=new T(t),u=[],d=0;for(let t=0;t<c;t++){let i=e.subarray(d,d+s[t]);d+=s[t];let a=l.generateMarker(t+n*1e4),o=Buffer.alloc(24);a.copy(o,0),o.writeUInt32LE(t,8),o.writeUInt32LE(c,12),o.writeUInt32LE(n,16),o.writeUInt32LE(r,20),u.push({index:t,total:c,videoIndex:n,totalVideos:r,payload:Buffer.concat([o,i])})}return u}function ee(e,t){let n=new T(t),r=[];for(let t of e){if(t.length<24)continue;let e=t.readUInt32LE(8),i=t.readUInt32LE(16);if(e>1e4)continue;let a=e+i*1e4,o=n.generateMarker(a);if(!t.subarray(0,8).equals(o))continue;let s=t.readUInt32LE(12),c=t.readUInt32LE(20),l=t.subarray(24);r.push({chunkIndex:e,totalChunks:s,videoIndex:i,totalVideos:c,data:l})}return r}function te(e,t,n){if(e===0)return[0];if(e<=t)return[e];let r=[],i=e;for(;i>0;)if(i<=n)r.push(i),i=0;else{let e=v(t,n+1);r.push(Math.min(e,i)),i-=e,i<0&&(i=0)}return r}x.registerFormat(`zip-encrypted`,S);function A(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MB`:`${(e/(1024*1024*1024)).toFixed(1)} GB`}const j=d(`tomato:compress`);async function M(e){let{inputPath:t,outputPath:n,password:r,zipPassword:i}=e,s=u(t),c=await a(s),l=c.isDirectory();j(`μž…λ ₯: %s (%s, %s)`,s,l?`디렉토리`:`파일`,A(c.size)),j(`AES-256 μ•”ν˜Έν™” ZIP 생성 쀑 (archiver, ZIP μ „μš© λΉ„λ°€λ²ˆν˜Έ)...`);let d=await I(s,l,i);j(` ZIP μ™„λ£Œ: %s β†’ %s`,A(c.size),A(d.length)),j(`AES-256-GCM 이쀑 μ•”ν˜Έν™” 쀑 (PBKDF2 ν‚€ μœ λ„)...`);let f=new T(r).encryptToBuffer(d);if(j(` μ•”ν˜Έν™” μ™„λ£Œ: %s β†’ %s (μ˜€λ²„ν—€λ“œ: +%s)`,A(d.length),A(f.length),A(f.length-d.length)),n){let e=u(n);await o(e,f),j(` 파일 μ €μž₯: %s`,e)}return{encrypted:f,originalSize:d.length,encryptedSize:f.length,outputPath:n?u(n):void 0}}async function N(e){let{inputPath:t,outputPath:n,password:i}=e,a=await r(u(t)),s=new E(i).decryptFromBuffer(a);await o(u(n),s)}function P(){try{return y(`7z`,[`--help`],{stdio:`ignore`}),!0}catch{return!1}}function F(e,t,n){try{y(`7z`,[`x`,`-p${t}`,`-o${n}`,e],{stdio:`inherit`})}catch(e){throw Error(`Failed to extract ZIP: ${e instanceof Error?e.message:e}`)}}async function I(e,t,n){return new Promise((r,i)=>{let a=[],o=new b;o.on(`data`,e=>a.push(e)),o.on(`end`,()=>r(Buffer.concat(a))),o.on(`error`,i);let c=x.create(`zip-encrypted`,{zlib:{level:8},encryptionMethod:`aes256`,password:n});c.on(`error`,i),c.pipe(o),t?c.directory(e,s(e)):c.file(e,{name:s(e)}),c.finalize()})}async function L(e){let t=(await a(e)).size,r=[],i=await n(e,`r`);try{let e=0;for(;e<t;){let n=Buffer.alloc(8),{bytesRead:a}=await i.read(n,0,8,e);if(a<8)break;let o=n.readUInt32BE(0),s=n.toString(`ascii`,4,8),c,l;if(o===0)c=t-e,l=8;else if(o===1){let t=Buffer.alloc(8);await i.read(t,0,8,e+8),c=Number(t.readBigUInt64BE(0)),l=16}else c=o,l=8;if(c<l)throw Error(`잘λͺ»λœ box 크기: offset=${e}, type=${s}, size=${c}, headerSize=${l}`);r.push({type:s,offset:e,size:c,headerSize:l,dataSize:c-l}),e+=c}}finally{await i.close()}return{fileSize:t,boxes:r}}const R=d(`tomato:pour`);async function z(e,n,r){let a=u(e),d=u(r);R(`─── pour μ‹œμž‘ ───`),R(` μž…λ ₯ 디렉토리: %s`,a),R(` 좜λ ₯ 경둜: %s`,d);let f=(await i(a,{withFileTypes:!0})).filter(e=>e.isFile()&&e.name.endsWith(`.mp4`)).map(e=>l(a,e.name)).sort();if(f.length===0)throw Error(`No bottles found in ${a}`);R(`1단계: MP4 파일 μˆ˜μ§‘ μ™„λ£Œ β€” %d개 발견`,f.length);for(let e of f)R(` - %s`,e);R(`2단계: 각 MP4μ—μ„œ free atom μŠ€μΊ” + chunk μΆ”μΆœ...`);let p=[];for(let e of f){let t=await B(e);R(` %s: free atom %d개 발견`,s(e),t.length);let r=ee(t,n);if(r.length>0){let e=r.reduce((e,t)=>e+t.data.length,0);R(` β†’ 유효 chunk %d개 (bottle #%d/%d, 데이터 %s)`,r.length,r[0].videoIndex,r[0].totalVideos,A(e)),p.push(...r)}else R(` β†’ 유효 chunk μ—†μŒ (마컀 뢈일치)`)}if(p.length===0)throw Error(`No tomato ketchup found. Wrong recipe (password)?`);p.sort((e,t)=>e.videoIndex===t.videoIndex?e.chunkIndex-t.chunkIndex:e.videoIndex-t.videoIndex);let m=p[0].totalVideos,h=new Set(p.map(e=>e.videoIndex));if(R(`3단계: μ •λ ¬ μ™„λ£Œ β€” 전체 chunk %d개, λΉ„λ””μ˜€ %d/%d개 확인`,p.length,h.size,m),h.size!==m){let e=[];for(let t=0;t<m;t++)h.has(t)||e.push(t);throw Error(`Missing bottles: #${e.join(`, #`)} (${h.size}/${m} found)`)}R(`4단계: λΉ„λ””μ˜€λ³„ 데이터 μž¬μ‘°ν•©...`);let g=[];for(let e=0;e<m;e++){let t=p.filter(t=>t.videoIndex===e);t.sort((e,t)=>e.chunkIndex-t.chunkIndex);let n=t[0].totalChunks;if(t.length!==n)throw Error(`Bottle #${e}: chunks missing (${t.length}/${n})`);let r=Buffer.concat(t.map(e=>e.data));g.push(r),R(` bottle #%d: chunk %d/%d개 검증 OK, 데이터 %s`,e,t.length,n,A(r.length))}let _=Buffer.concat(g);R(`5단계: 전체 데이터 μ—°κ²° μ™„λ£Œ β€” %s (%d개 파트)`,A(_.length),g.length),R(`6단계: AES-256-GCM λ³΅ν˜Έν™” 쀑...`);let v=new E(n).decryptFromBuffer(_);return R(` λ³΅ν˜Έν™” μ™„λ£Œ: %s β†’ %s (μ•”ν˜Έν™” ZIP 데이터)`,A(_.length),A(v.length)),await t(c(d),{recursive:!0}),await o(d,v),R(`─── pour μ™„λ£Œ: %s (%s) ───`,d,A(v.length)),{outputPath:d,chunkCount:p.length,dataSize:v.length,videoCount:m}}async function B(e){let t=(await L(e)).boxes.filter(e=>e.type===`free`),r=[],i=await n(e,`r`);try{for(let e of t){let t=Buffer.alloc(e.dataSize);await i.read(t,0,e.dataSize,e.offset+e.headerSize),r.push(t)}}finally{await i.close()}return r}function V(e,t){t!==0&&H(e,0,e.length,t)}function H(e,t,n,r){let i=t;for(;i+8<=n;){let t=e.readUInt32BE(i),a=e.toString(`ascii`,i+4,i+8),o,s;if(t===0)o=n-i,s=8;else if(t===1){if(i+16>n)break;o=Number(e.readBigUInt64BE(i+8)),s=16}else o=t,s=8;if(o<s||i+o>n)break;let c=i+s,l=i+o;a===`stco`?U(e,c,l,r):a===`co64`&&W(e,c,l,r),[`moov`,`trak`,`mdia`,`minf`,`stbl`,`edts`,`dinf`,`udta`].includes(a)&&H(e,c,l,r),i+=o}}function U(e,t,n,r){let i=t+4;if(i+4>n)return;let a=e.readUInt32BE(i),o=i+4;for(let t=0;t<a;t++){let i=o+t*4;if(i+4>n)break;let a=e.readUInt32BE(i),s=a+r;if(s>4294967295||s<0)throw Error(`stco μ˜€ν”„μ…‹ μ˜€λ²„ν”Œλ‘œμš°: ${a} + ${r} = ${s}. co64둜 λ³€ν™˜μ΄ ν•„μš”ν•©λ‹ˆλ‹€.`);e.writeUInt32BE(s>>>0,i)}}function W(e,t,n,r){let i=t+4;if(i+4>n)return;let a=e.readUInt32BE(i),o=i+4;for(let t=0;t<a;t++){let i=o+t*8;if(i+8>n)break;let a=e.readBigUInt64BE(i)+BigInt(r);e.writeBigUInt64BE(a,i)}}const G=d(`tomato:mp4`),K=8*1024*1024;async function ne(t){let{inputPath:n,outputPath:r,chunks:i}=t;G(`MP4 ꡬ쑰 νŒŒμ‹±: %s`,n);let a=await L(n),o=a.boxes.find(e=>e.type===`ftyp`),s=a.boxes.find(e=>e.type===`moov`),c=a.boxes.find(e=>e.type===`mdat`);if(!o||!s||!c)throw Error(`MP4 νŒŒμΌμ— ftyp, moov, mdat boxκ°€ λͺ¨λ‘ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.`);G(` 원본 ꡬ쑰: ftyp(%s) + moov(%s) + mdat(%s) = 총 %d개 box`,A(o.size),A(s.size),A(c.size),a.boxes.length);let l=i.map(e=>re(e.payload)),u=l.reduce((e,t)=>e+t.length,0);G(` free atom 생성: %d개, 총 %s`,l.length,A(u));let d=Math.max(1,Math.floor(l.length/2)),f=l.slice(0,d),p=l.slice(d),m=f.reduce((e,t)=>e+t.length,0),h=p.reduce((e,t)=>e+t.length,0);G(` λΆ„μ‚° 배치: mdat μ•ž %d개 (%s), mdat λ’€ %d개 (%s)`,f.length,A(m),p.length,A(h)),G(` moov μ˜€ν”„μ…‹ μ‘°μ •: +%s (mdat μ•ž free atom 크기만큼)`,A(m));let g=await q(n,s);V(g.subarray(s.headerSize),m),G(` 좜λ ₯ ꡬ쑰: ftyp β†’ moov β†’ freeΓ—%d β†’ mdat β†’ freeΓ—%d`,f.length,p.length),G(` 좜λ ₯ 경둜: %s`,r);let _=e(r);try{await J(_,await q(n,o)),await J(_,g);for(let e of f)await J(_,e);await ie(_,n,c);for(let e of p)await J(_,e)}finally{await new Promise((e,t)=>{_.end(()=>e()),_.on(`error`,t)})}G(` MP4 μž¬κ΅¬μ„± μ™„λ£Œ: %s`,r)}function re(e){let t=8+e.length,n=Buffer.alloc(8);if(t>4294967295){let t=Buffer.alloc(16);return t.writeUInt32BE(1,0),t.write(`free`,4,4,`ascii`),t.writeBigUInt64BE(BigInt(16+e.length),8),Buffer.concat([t,e])}return n.writeUInt32BE(t,0),n.write(`free`,4,4,`ascii`),Buffer.concat([n,e])}async function q(e,t){let r=await n(e,`r`);try{let e=Buffer.alloc(t.size);return await r.read(e,0,t.size,t.offset),e}finally{await r.close()}}async function ie(e,t,r){let i=await n(t,`r`);try{let t=Buffer.alloc(K),n=r.size,a=r.offset;for(;n>0;){let r=Math.min(n,K),{bytesRead:o}=await i.read(t,0,r,a);if(o===0)break;await J(e,t.subarray(0,o)),a+=o,n-=o}}finally{await i.close()}}function J(e,t){return new Promise((n,r)=>{if(e.write(t))n();else{let t=()=>{e.removeListener(`error`,i),n()},i=n=>{e.removeListener(`drain`,t),r(n)};e.once(`drain`,t),e.once(`error`,i)}})}const Y=d(`tomato:download`),X=d(`tomato:cache`),Z=[{size:2252313,name:`ForBiggerJoyrides.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4`},{size:2252313,name:`ForBiggerMeltdowns.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4`},{size:2299653,name:`ForBiggerBlazes.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4`},{size:2372820,name:`ForBiggerEscapes.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4`},{size:12917485,name:`ForBiggerFun.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4`},{size:13183260,name:`WeAreGoingOnBullrun.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4`},{size:43780763,name:`VolkswagenGTIReview.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4`},{size:45577410,name:`WhatCarCanYouGetForAGrand.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4`},{size:48051822,name:`SubaruOutbackOnStreetAndDirt.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4`},{size:158008374,name:`BigBuckBunny.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4`},{size:169612362,name:`ElephantsDream.mp4`,url:`http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4`}],Q=l(C(),`.tomato-ketchup`,`videos`);async function ae(e){X(`μΊμ‹œ 디렉토리: %s`,Q),await t(Q,{recursive:!0});let n=[...Z].sort((e,t)=>t.size-e.size).slice(0,5);X(`μ‚¬μš© κ°€λŠ₯ν•œ μƒ˜ν”Œ λΉ„λ””μ˜€ ν’€ (%d개, 전체 %d개 쀑 μƒμœ„ 크기순):`,n.length,Z.length);for(let e of n)X(` - %s (%s)`,e.name,A(e.size));let r=Math.min(e,n.length);X(`μš”μ²­ λΉ„λ””μ˜€ 수: %d, μ‹€μ œ 고유 ν•„μš”: %d`,e,r),await Promise.all(n.slice(0,r).map(async e=>{let t=l(Q,e.name);if(await le(t)){let n=await a(t);X(`%s μΊμ‹œ 히트 (경둜: %s, 크기: %s)`,e.name,t,A(n.size))}else{Y(`%s λ‹€μš΄λ‘œλ“œ μ‹œμž‘ (μ˜ˆμƒ 크기: %s, URL: %s)`,e.name,A(e.size),e.url),await ce(e.url,t);let n=await a(t);Y(`%s λ‹€μš΄λ‘œλ“œ μ™„λ£Œ (μ €μž₯ 경둜: %s, μ‹€μ œ 크기: %s)`,e.name,t,A(n.size))}}));let i=[];for(let t=0;t<e;t++){let e=n[t%n.length];i.push({name:e.name,path:l(Q,e.name),size:e.size})}return X(`λΉ„λ””μ˜€ ν’€ μ€€λΉ„ μ™„λ£Œ: %d개 (총 %s)`,i.length,A(i.reduce((e,t)=>e+t.size,0))),i}async function oe(e){let t=u(e);X(`μ‚¬μš©μž λΉ„λ””μ˜€ 디렉토리 μŠ€μΊ”: %s`,t);let n=await i(t,{withFileTypes:!0}),r=[];for(let e of n)if(e.isFile()&&e.name.endsWith(`.mp4`)){let n=l(t,e.name),i=await a(n);r.push({name:e.name,path:n,size:i.size})}r.sort((e,t)=>t.size-e.size),X(`발견된 MP4 파일: %d개`,r.length);for(let e of r)X(` - %s (%s) β†’ %s`,e.name,A(e.size),e.path);return r}function se(e,t,n){if(n.length===0)return-1;let r=e,i=0;for(;r>0&&i<1e3;){let e=n[i%n.length],a=Math.floor(e*t);if(a<=0)return-1;r-=a,i++}return i}async function ce(t,n){let r=await fetch(t);if(!r.ok||!r.body)throw Error(`λ‹€μš΄λ‘œλ“œ μ‹€νŒ¨: ${t} (${r.status})`);let i=e(n),a=r.body.getReader();try{for(;;){let{done:e,value:t}=await a.read();if(e)break;i.write(Buffer.from(t))||await new Promise(e=>i.once(`drain`,e))}}finally{await new Promise((e,t)=>{i.end(()=>e()),i.on(`error`,t)})}}async function le(e){try{return await a(e),!0}catch{return!1}}const $=d(`tomato:squeeze`);async function ue(e){let{dataPath:n,outputDir:r,password:i,zipPassword:o,ratio:c,videoDir:d}=e,f=u(r),p=u(n);await t(f,{recursive:!0}),$(`─── squeeze μ‹œμž‘ ───`),$(` μž…λ ₯ 데이터: %s`,p),$(` 좜λ ₯ 디렉토리: %s`,f),$(` 은닉 λΉ„μœ¨: %s%%`,(c*100).toFixed(0)),d?$(` λΉ„λ””μ˜€ μ†ŒμŠ€: μ‚¬μš©μž 디렉토리 β†’ %s`,u(d)):$(` λΉ„λ””μ˜€ μ†ŒμŠ€: λ‚΄μž₯ μƒ˜ν”Œ λΉ„λ””μ˜€ (μžλ™ λ‹€μš΄λ‘œλ“œ/μΊμ‹œ)`),$(`1단계: 데이터 μ••μΆ• + μ•”ν˜Έν™”...`);let m=await M({inputPath:p,password:i,zipPassword:o}),h=m.encrypted;$(` 원본: %s β†’ ZIP μ••μΆ•: %s β†’ μ•”ν˜Έν™”: %s (μ˜€λ²„ν—€λ“œ: +%s)`,A(m.originalSize),A(m.originalSize),A(m.encryptedSize),A(m.encryptedSize-m.originalSize)),$(`2단계: λΉ„λ””μ˜€ μ†ŒμŠ€ ν’€ μ€€λΉ„...`);let g;if(d){if(g=await oe(d),g.length===0)throw Error(`No bottles found in ${d}`)}else g=await ae(5);$(` λΉ„λ””μ˜€ ν’€ (%d개):`,g.length);for(let e of g){let t=Math.floor(e.size*c);$(` - %s: 크기 %s, 은닉 κ°€λŠ₯ μš©λŸ‰ %s (ratio %s%%)`,e.name,A(e.size),A(t),(c*100).toFixed(0))}$(`3단계: ν•„μš” λΉ„λ””μ˜€ 수 계산...`);let _=g.map(e=>e.size),v=_.reduce((e,t)=>e+Math.floor(t*c),0);$(` μ•”ν˜Έν™” 데이터: %s, ν’€ 1νšŒμ „ μš©λŸ‰: %s`,A(h.length),A(v));let y=se(h.length,c,_);if(y<0)throw Error(`Bottles too small for this much tomato. Try a higher --ratio. Tomato size: ${A(h.length)}`);let b=[];for(let e=0;e<y;e++)b.push(g[e%g.length]);$(` ν•„μš” λΉ„λ””μ˜€: %d개 (고유 %dκ°œμ—μ„œ μˆœν™˜ μž¬μ‚¬μš©)`,y,g.length),$(`4단계: 데이터 λΆ„λ°°...`);let x=de(h,b,c);for(let e=0;e<x.length;e++){let t=b[e],n=Math.floor(t.size*c);$(` λΉ„λ””μ˜€[%d] %s: %s / %s μš©λŸ‰ μ‚¬μš© (%.1f%%)`,e,t.name,A(x[e].length),A(n),x[e].length/n*100)}$(`5단계: 각 λΉ„λ””μ˜€μ— 데이터 μ‚½μž…...`);let S=[],C=0;for(let e=0;e<b.length;e++){let t=b[e],n=x[e],r=String(e+1).padStart(3,`0`),o=l(f,`${r}_${s(t.name,`.mp4`)}.mp4`);$(`[%s] 처리 쀑: %s β†’ %s`,r,t.name,o),$(`[%s] 원본 λΉ„λ””μ˜€: %s, μ‚½μž… 데이터: %s`,r,A(t.size),A(n.length));let c=Math.max(64*1024,Math.floor(n.length/5)),u=Math.max(128*1024,Math.floor(n.length/2)),d=k(n,i,e,b.length,{minChunkSize:c,maxChunkSize:u});$(`[%s] chunk: %d개 (크기 λ²”μœ„: %s ~ %s)`,r,d.length,A(c),A(u)),await ne({inputPath:t.path,outputPath:o,chunks:d});let p=await a(o);S.push(o),C+=p.size,$(`[%s] μ™„λ£Œ: %s β†’ %s (증가: +%s, +%.1f%%)`,r,A(t.size),A(p.size),A(p.size-t.size),(p.size-t.size)/t.size*100)}return $(`─── squeeze μ™„λ£Œ ───`),$(` 좜λ ₯ 파일: %d개, 총 크기: %s, 좜λ ₯ 디렉토리: %s`,S.length,A(C),f),{outputDir:f,totalVideos:b.length,outputFiles:S,originalDataSize:m.originalSize,encryptedDataSize:m.encryptedSize,totalOutputSize:C}}function de(e,t,n){let r=[],i=0;for(let a=0;a<t.length;a++){let o=Math.floor(t[a].size*n),s=a===t.length-1?e.length-i:Math.min(o,e.length-i);r.push(e.subarray(i,i+s)),i+=s}return r}export{F as a,T as c,N as i,z as n,P as o,M as r,E as s,ue as t};
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "tomato-ketchup",
3
+ "version": "0.4.0",
4
+ "description": "A recipe for making ketchup β€” squeeze your tomatoes into bottles",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "tomato-ketchup": "./dist/bootstrapper/cli.mjs"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.mjs",
13
+ "types": "./dist/index.d.mts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "squeeze": "tsx src/core-cli.ts squeeze ~/git/my-app --password 1234 --zip-password myZipSecret --ratio 0.1 --output ./__table__",
21
+ "pour": "tsx src/core-cli.ts pour ./__table__ --password 1234 --output ./__output__/recovered.zip",
22
+ "format": "biome check . --write --unsafe",
23
+ "typecheck": "tsc --noEmit",
24
+ "build": "tsdown",
25
+ "build:binary": "tsx scripts/build-binary.ts",
26
+ "test": "vitest run",
27
+ "prepare": "lefthook install",
28
+ "release": "release-it"
29
+ },
30
+ "devDependencies": {
31
+ "@biomejs/biome": "^2.3.14",
32
+ "@types/archiver": "^7.0.0",
33
+ "@types/debug": "^4.1.12",
34
+ "@types/node": "^25.2.2",
35
+ "@yao-pkg/pkg": "^6.13.1",
36
+ "dotenv": "^17.2.4",
37
+ "javascript-obfuscator": "^4.1.1",
38
+ "lefthook": "^2.1.0",
39
+ "octokit": "^5.0.5",
40
+ "release-it": "^19.2.4",
41
+ "tsdown": "^0.20.3",
42
+ "tsx": "^4.21.0",
43
+ "typescript": "^5.9.3",
44
+ "vitest": "^4.0.18"
45
+ },
46
+ "dependencies": {
47
+ "archiver": "^7.0.1",
48
+ "archiver-zip-encrypted": "^2.0.0",
49
+ "cac": "^6.7.14",
50
+ "debug": "^4.4.3"
51
+ }
52
+ }