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 +239 -0
- package/dist/bootstrapper/cli.d.mts +1 -0
- package/dist/bootstrapper/cli.mjs +18 -0
- package/dist/core-cli.d.mts +1 -0
- package/dist/core-cli.mjs +16 -0
- package/dist/index.d.mts +170 -0
- package/dist/index.mjs +1 -0
- package/dist/squeeze-CaREQmbk.mjs +1 -0
- package/package.json +52 -0
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{};
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|