1- // This script encrypts/decrypts your secret commands with multiple passwords
2- // Each password will decrypt to different content
3- // Run it with Node.js:
4- // To encrypt: node encrypt-commands.js encrypt [master_password]
5- // To decrypt: node encrypt-commands.js decrypt password output.js
6- // or node encrypt-commands.js decrypt master_password src/ (recreates all files)
1+ // Encrypt/decrypt multi-part commands; binary-safe; auto-injects __SECRETS__
2+ // Usage:
3+ // node encrypt-commands.js encrypt [master_password]
4+ // node encrypt-commands.js decrypt password output.js // single
5+ // node encrypt-commands.js decrypt master_password src/ // restore all
76
87const crypto = require ( "crypto" ) ;
98const path = require ( "path" ) ;
@@ -14,191 +13,174 @@ const ENCRYPTED_FILE = path.resolve(
1413 "../static/terminal-window/encrypted-commands.js.enc"
1514) ;
1615
17- // Simplified encryption configuration
18- const CONFIG = {
19- iterations : 1000000 ,
20- keyLength : 32 ,
21- ivLength : 16 ,
22- saltLength : 32 ,
23- tagLength : 16 ,
24- } ;
16+ const CONFIG = { iterations : 1_000_000 , keyLength : 32 , ivLength : 16 , saltLength : 32 , tagLength : 16 } ;
17+ const BINARY_EXT = new Set ( [ ".png" , ".jpg" , ".jpeg" , ".gif" , ".webp" , ".webm" ] ) ;
2518
2619function deriveKey ( password , salt ) {
27- return crypto . pbkdf2Sync (
28- password ,
29- salt ,
30- CONFIG . iterations ,
31- CONFIG . keyLength ,
32- "sha512"
33- ) ;
20+ return crypto . pbkdf2Sync ( password , salt , CONFIG . iterations , CONFIG . keyLength , "sha512" ) ;
3421}
3522
36- function encryptContent ( content , password , salt , iv ) {
23+ function encryptBuffer ( buffer , password , salt , iv ) {
3724 const key = deriveKey ( password , salt ) ;
3825 const cipher = crypto . createCipheriv ( "aes-256-gcm" , key , iv ) ;
39- const encrypted = Buffer . concat ( [
40- cipher . update ( content , "utf8" ) ,
41- cipher . final ( ) ,
42- ] ) ;
26+ const encrypted = Buffer . concat ( [ cipher . update ( buffer ) , cipher . final ( ) ] ) ;
4327 return { encrypted, authTag : cipher . getAuthTag ( ) } ;
4428}
4529
30+ function walk ( dir , basedir , out = [ ] ) {
31+ for ( const ent of fs . readdirSync ( dir , { withFileTypes : true } ) ) {
32+ const abs = path . join ( dir , ent . name ) ;
33+ if ( ent . isDirectory ( ) ) walk ( abs , basedir , out ) ;
34+ else if ( ent . isFile ( ) ) {
35+ const rel = path . posix . normalize ( path . relative ( basedir , abs ) . split ( path . sep ) . join ( "/" ) ) ;
36+ out . push ( rel ) ;
37+ }
38+ }
39+ return out ;
40+ }
41+
42+ function isBinaryByExt ( rel ) {
43+ return BINARY_EXT . has ( path . extname ( rel ) . toLowerCase ( ) ) ;
44+ }
45+
46+ function makeUint8Literal ( buf ) {
47+ const nums = Array . from ( buf ) ;
48+ return `new Uint8Array([${ nums . join ( "," ) } ])` ;
49+ }
50+
51+ function buildSecretsBlock ( assets ) {
52+ const lines = [ ] ;
53+ lines . push ( "(function(){" ) ;
54+ lines . push ( " try { if (!window.__SECRETS__) window.__SECRETS__ = Object.create(null);" ) ;
55+ for ( const a of assets ) {
56+ lines . push ( ` window.__SECRETS__[${ JSON . stringify ( a . rel ) } ] = ${ makeUint8Literal ( a . buffer ) } ;` ) ;
57+ }
58+ lines . push ( " } catch(e){ console.error('SECRETS inject failed', e); }" ) ;
59+ lines . push ( "})();" ) ;
60+ return lines . join ( "\n" ) ;
61+ }
62+
4663async function encryptCommands ( masterPassword ) {
4764 const srcDir = path . resolve ( __dirname , "src" ) ;
48- const files = fs . readdirSync ( srcDir ) ;
49-
50- // Each file in src becomes a password:content pair
51- const entries = files . map ( ( filename ) => {
52- const password = path . parse ( filename ) . name ; // Use filename without extension as password
53- const file = path . resolve ( srcDir , filename ) ;
54- const content = fs . readFileSync ( file , "utf8" ) ;
55- return { password , content , filename } ;
65+ if ( ! fs . existsSync ( srcDir ) ) throw new Error ( "Missing src/ directory" ) ;
66+
67+ const relPaths = walk ( srcDir , srcDir ) ;
68+ if ( ! relPaths . length ) throw new Error ( "No files found in src/" ) ;
69+
70+ const files = relPaths . map ( ( rel ) => {
71+ const abs = path . join ( srcDir , rel ) ;
72+ return { rel , buffer : fs . readFileSync ( abs ) , isBinary : isBinaryByExt ( rel ) } ;
5673 } ) ;
5774
58- if ( entries . length === 0 ) {
59- throw new Error ( "No files found in src directory" ) ;
60- }
75+ // All custom cat assets to embed
76+ const onekoAssets = files . filter ( ( f ) => f . rel . startsWith ( "oneko_custom/" ) ) ;
77+
78+ // Build encrypted parts (password = filename without extension, as before)
79+ const entries = files . map ( ( f ) => {
80+ const parsed = path . parse ( f . rel ) ;
81+ const password = parsed . name ;
82+
83+ // For su:* login payloads, **prepend** the secrets so they exist before file runs
84+ if ( / ^ s u : [ ^ : ] + : .+ $ / i. test ( password ) && parsed . ext === ".js" ) {
85+ const jsText = f . buffer . toString ( "utf8" ) ;
86+ const secretsCode = buildSecretsBlock ( onekoAssets ) ;
87+ const merged = Buffer . from (
88+ "// --- AUTO-INJECTED (oneko_custom assets) ---\n" +
89+ secretsCode +
90+ "\n// --- END AUTO-INJECTED ---\n\n" +
91+ jsText ,
92+ "utf8"
93+ ) ;
94+ return { password, buffer : merged , rel : f . rel } ;
95+ }
96+ return { password, buffer : f . buffer , rel : f . rel } ;
97+ } ) ;
6198
62- // Use the same salt and IV for all encryptions
99+ // Single salt+ IV for whole bundle (keeps legacy layout)
63100 const salt = crypto . randomBytes ( CONFIG . saltLength ) ;
64101 const iv = crypto . randomBytes ( CONFIG . ivLength ) ;
65102
66- // If master password provided, add master entry with all files info
67- if ( masterPassword ) {
68- const masterContent = JSON . stringify (
69- entries . map ( ( e ) => ( {
70- filename : e . filename ,
71- content : e . content ,
72- } ) )
73- ) ;
74- entries . push ( { password : masterPassword , content : masterContent } ) ;
75- }
76-
77- // Encrypt each content with its password
78- const encryptedParts = entries . map ( ( { password, content } ) => {
79- const { encrypted, authTag } = encryptContent ( content , password , salt , iv ) ;
103+ const encryptedParts = entries . map ( ( { password, buffer } ) => {
104+ const { encrypted, authTag } = encryptBuffer ( buffer , password , salt , iv ) ;
80105 return { encrypted, authTag } ;
81106 } ) ;
82107
83- // Combine all encrypted parts
84- // Format: [salt][iv][size1][enc1][tag1][size2][enc2][tag2]...
85- const parts = [ salt , iv ] ;
86- encryptedParts . forEach ( ( { encrypted, authTag } ) => {
87- // Add 4-byte size header for each part
88- const sizeBuffer = Buffer . alloc ( 4 ) ;
89- sizeBuffer . writeUInt32BE ( encrypted . length ) ;
90- parts . push ( sizeBuffer , encrypted , authTag ) ;
91- } ) ;
108+ // Optional master archive (JSON manifest, binary-safe)
109+ if ( masterPassword ) {
110+ const masterPayload = files . map ( ( f ) => ( {
111+ filename : f . rel ,
112+ isBinary : f . isBinary ,
113+ content : f . isBinary ? f . buffer . toString ( "base64" ) : f . buffer . toString ( "utf8" ) ,
114+ } ) ) ;
115+ const masterBuf = Buffer . from ( JSON . stringify ( masterPayload ) , "utf8" ) ;
116+ const { encrypted, authTag } = encryptBuffer ( masterBuf , masterPassword , salt , iv ) ;
117+ encryptedParts . push ( { encrypted, authTag } ) ;
118+ }
92119
93- const result = Buffer . concat ( parts ) ;
94- fs . writeFileSync ( ENCRYPTED_FILE , result ) ;
95-
96- console . log ( `Encrypted commands saved to ${ ENCRYPTED_FILE } ` ) ;
97- console . log ( `\nSecurity parameters:` ) ;
98- console . log ( `- Key derivation: SHA-512` ) ;
99- console . log ( `- Iterations: ${ CONFIG . iterations . toLocaleString ( ) } ` ) ;
100- console . log (
101- `- Salt/Key/IV lengths: ${ CONFIG . saltLength * 8 } /${ CONFIG . keyLength * 8 } /${
102- CONFIG . ivLength * 8
103- } bits`
104- ) ;
105- console . log ( `- Number of encrypted parts: ${ entries . length } ` ) ;
120+ // Write bundle: [salt][iv][sizeN][encN][tagN]...
121+ const parts = [ salt , iv ] ;
122+ for ( const { encrypted, authTag } of encryptedParts ) {
123+ const size = Buffer . alloc ( 4 ) ;
124+ size . writeUInt32BE ( encrypted . length ) ;
125+ parts . push ( size , encrypted , authTag ) ;
126+ }
127+ fs . writeFileSync ( ENCRYPTED_FILE , Buffer . concat ( parts ) ) ;
128+ console . log ( `Encrypted → ${ ENCRYPTED_FILE } (parts=${ encryptedParts . length } )` ) ;
106129}
107130
108131async function decryptCommands ( password , outputPath ) {
109- const encryptedData = fs . readFileSync ( ENCRYPTED_FILE ) ;
110-
111- // Extract common components
112- let offset = 0 ;
113- const salt = encryptedData . slice ( offset , ( offset += CONFIG . saltLength ) ) ;
114- const iv = encryptedData . slice ( offset , ( offset += CONFIG . ivLength ) ) ;
115-
132+ const buf = fs . readFileSync ( ENCRYPTED_FILE ) ;
133+ let off = 0 ;
134+ const salt = buf . slice ( off , ( off += CONFIG . saltLength ) ) ;
135+ const iv = buf . slice ( off , ( off += CONFIG . ivLength ) ) ;
116136 const key = deriveKey ( password , salt ) ;
117- const decryptedParts = [ ] ;
118137
119- // Try to decrypt each part
120- while ( offset < encryptedData . length ) {
138+ const outs = [ ] ;
139+ while ( off < buf . length ) {
121140 try {
122- // Read size of next encrypted part
123- const size = encryptedData . readUInt32BE ( offset ) ;
124- offset += 4 ;
125-
126- // Extract encrypted content and tag
127- const encrypted = encryptedData . slice ( offset , offset + size ) ;
128- offset += size ;
129- const authTag = encryptedData . slice ( offset , offset + CONFIG . tagLength ) ;
130- offset += CONFIG . tagLength ;
131-
132- // Try to decrypt this part
133- const decipher = crypto . createDecipheriv ( "aes-256-gcm" , key , iv ) ;
134- decipher . setAuthTag ( authTag ) ;
135- const decrypted = Buffer . concat ( [
136- decipher . update ( encrypted ) ,
137- decipher . final ( ) ,
138- ] ) ;
139-
140- decryptedParts . push ( decrypted ) ;
141- } catch ( error ) {
142- // Try next part
143- continue ;
144- }
141+ const size = buf . readUInt32BE ( off ) ; off += 4 ;
142+ const enc = buf . slice ( off , off + size ) ; off += size ;
143+ const tag = buf . slice ( off , off + CONFIG . tagLength ) ; off += CONFIG . tagLength ;
144+ const d = crypto . createDecipheriv ( "aes-256-gcm" , key , iv ) ;
145+ d . setAuthTag ( tag ) ;
146+ outs . push ( Buffer . concat ( [ d . update ( enc ) , d . final ( ) ] ) ) ;
147+ } catch { /* skip wrong key part */ }
145148 }
146-
147- if ( decryptedParts . length === 0 ) {
148- console . error ( "Decryption failed. Invalid password or corrupted file." ) ;
149+ if ( ! outs . length ) {
150+ console . error ( "Decryption failed (bad password or file)." ) ;
149151 process . exit ( 1 ) ;
150152 }
151153
152- // If output is a directory, try to parse as master password result
153154 if ( outputPath . endsWith ( "/" ) ) {
154- try {
155- // Try to parse as JSON (master password result)
156- const files = JSON . parse ( decryptedParts [ 0 ] ) ;
157-
158- // Ensure directory exists
159- if ( ! fs . existsSync ( outputPath ) ) {
160- fs . mkdirSync ( outputPath , { recursive : true } ) ;
161- }
162-
163- // Write each file
164- files . forEach ( ( { filename, content } ) => {
165- const outputFile = path . join ( outputPath , filename ) ;
166- fs . writeFileSync ( outputFile , content ) ;
167- console . log ( `Decrypted ${ filename } saved to ${ outputFile } ` ) ;
168- } ) ;
169- return ;
170- } catch ( e ) {
171- // Not master password result, fall through to single file
155+ // Master payload restore
156+ const list = JSON . parse ( outs [ 0 ] . toString ( "utf8" ) ) ;
157+ if ( ! fs . existsSync ( outputPath ) ) fs . mkdirSync ( outputPath , { recursive : true } ) ;
158+ for ( const f of list ) {
159+ const outFile = path . join ( outputPath , f . filename ) ;
160+ fs . mkdirSync ( path . dirname ( outFile ) , { recursive : true } ) ;
161+ const data = f . isBinary ? Buffer . from ( f . content , "base64" ) : Buffer . from ( f . content , "utf8" ) ;
162+ fs . writeFileSync ( outFile , data ) ;
163+ console . log ( "Decrypted" , f . filename ) ;
172164 }
165+ return ;
173166 }
174167
175- // Normal single-file decryption
176- fs . writeFileSync ( outputPath , decryptedParts [ 0 ] ) ;
177- console . log ( `Decrypted commands saved to ${ outputPath } ` ) ;
168+ fs . writeFileSync ( outputPath , outs [ 0 ] ) ;
169+ console . log ( `Decrypted → ${ outputPath } ` ) ;
178170}
179171
180172const action = process . argv [ 2 ] ;
181173const args = process . argv . slice ( 3 ) ;
182-
183- if ( ! action || ! [ "encrypt" , "decrypt" ] . includes ( action ) ) {
184- console . error (
185- "Usage:\n" +
186- " Encrypt: node encrypt-commands.js encrypt [master_password]\n" +
187- " Decrypt: node encrypt-commands.js decrypt password output.js"
188- ) ;
174+ if ( ! [ "encrypt" , "decrypt" ] . includes ( action ) ) {
175+ console . error ( "Usage:\n node encrypt-commands.js encrypt [master_password]\n node encrypt-commands.js decrypt password output.js\n node encrypt-commands.js decrypt master_password src/" ) ;
189176 process . exit ( 1 ) ;
190177}
191-
192178if ( action === "encrypt" ) {
193- const masterPassword = args [ 0 ] ; // Optional master password
194- encryptCommands ( masterPassword ) ;
179+ encryptCommands ( args [ 0 ] ) ;
195180} else {
196181 const [ password , outputPath ] = args ;
197182 if ( ! password || ! outputPath ) {
198- console . error ( "Decrypt requires password and output path" ) ;
199- console . error (
200- "Use directory path ending with / for master password to recreate all files"
201- ) ;
183+ console . error ( "decrypt requires: password outputPath (use a dir ending with / for master)" ) ;
202184 process . exit ( 1 ) ;
203185 }
204186 decryptCommands ( password , outputPath ) ;
0 commit comments