├── .editorconfig ├── .gitignore ├── license ├── package.json ├── readme.md ├── resources ├── demo.gif └── demo.mp4 ├── src ├── bin.ts ├── constants.ts └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.err 3 | *.log 4 | ._* 5 | .cache 6 | .fseventsd 7 | .DocumentRevisions* 8 | .DS_Store 9 | .TemporaryItems 10 | .Trashes 11 | Thumbs.db 12 | 13 | dist 14 | node_modules 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-present Fabio Spampinato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fabiospampinato/secret", 3 | "repository": "github:fabiospampinato/secret", 4 | "description": "The simplest command to encrypt/decrypt a file, useful for committing encrypted \".env\" files to version control, among other things.", 5 | "version": "3.0.2", 6 | "type": "module", 7 | "bin": "dist/bin.js", 8 | "main": "dist/index.js", 9 | "exports": "./dist/index.js", 10 | "types": "dist/index.d.ts", 11 | "scripts": { 12 | "clean": "tsex clean", 13 | "compile": "tsex compile", 14 | "compile:watch": "tsex compile --watch", 15 | "prepublishOnly": "tsex prepare" 16 | }, 17 | "keywords": [ 18 | "encrypt", 19 | "decrypt", 20 | "file", 21 | "env", 22 | "version control", 23 | "vcs", 24 | "git", 25 | "secret", 26 | "secrets" 27 | ], 28 | "dependencies": { 29 | "prask": "^2.0.0", 30 | "specialist": "^1.3.0", 31 | "tiny-encryptor": "^1.0.0" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^20.4.10", 35 | "tsex": "^3.0.1", 36 | "typescript": "^5.1.6" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | Demo 4 |

5 | 6 | # Secret 7 | 8 | The simplest command to encrypt/decrypt a file, useful for committing encrypted ".env" files to version control, among other things. 9 | 10 | Secret automatically detects if you want to encrypt or decrypt a file: if the file ends with the ".secret" extension then it will try to decrypt it, otherwise it will try to encrypt it. 11 | 12 | ## Install 13 | 14 | ```sh 15 | npm install -g @fabiospampinato/secret 16 | ``` 17 | 18 | ## Usage 19 | 20 | Encrypt a file: 21 | 22 | ```sh 23 | secret myFile.txt 24 | ``` 25 | 26 | Decrypt a file: 27 | 28 | ```sh 29 | secret myFile.txt.secret 30 | ``` 31 | 32 | ## License 33 | 34 | MIT © Fabio Spampinato 35 | -------------------------------------------------------------------------------- /resources/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/secret/2febdb0dcf881a41a591606d4acb0b860d77a8a7/resources/demo.gif -------------------------------------------------------------------------------- /resources/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiospampinato/secret/2febdb0dcf881a41a591606d4acb0b860d77a8a7/resources/demo.mp4 -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* IMPORT */ 4 | 5 | import fs from 'node:fs'; 6 | import path from 'node:path'; 7 | import process from 'node:process'; 8 | import * as prask from 'prask'; 9 | import {bin} from 'specialist'; 10 | import {SECRET_SUFFIX} from './constants'; 11 | import Secret from '.'; 12 | 13 | /* MAIN */ 14 | 15 | bin ( 'secret', 'The simplest command to encrypt/decrypt a file' ) 16 | /* DEFAULT COMMAND */ 17 | .argument ( '', 'The file to encrypt or descrypt' ) 18 | .action ( async ( options, inputFiles ) => { 19 | /* INPUT */ 20 | const inputFile = inputFiles[0]; 21 | const inputPath = path.resolve ( process.cwd (), inputFile ); 22 | const inputExists = fs.existsSync ( inputPath ); 23 | if ( !inputExists ) return prask.log.error ( `"${inputFile}" does not exist!` ); 24 | const inputStat = fs.lstatSync ( inputPath ); 25 | if ( !inputStat.isFile () ) return prask.log.error ( `"${inputFile}" is not a file!` ); 26 | const inputBuffer = fs.readFileSync ( inputPath ); 27 | /* PASSWORD */ 28 | const isSecret = inputFile.endsWith ( SECRET_SUFFIX ); 29 | const password = await prask.password ({ message: 'Password:', required: true }); 30 | if ( !password ) return; 31 | const passwordConfirmation = isSecret ? password : await prask.password ({ message: 'Password confirmation:', required: true }); 32 | if ( !passwordConfirmation ) return; 33 | if ( password !== passwordConfirmation ) return prask.log.error ( 'The two passwords do not match!' ); 34 | /* OUTPUT */ 35 | try { 36 | const transform = isSecret ? Secret.decrypt : Secret.encrypt; 37 | const outputBuffer = await transform ( inputBuffer, password ); 38 | const outputFile = isSecret ? path.basename ( inputFile, path.extname ( inputFile ) ) : `${inputFile}${SECRET_SUFFIX}`; 39 | const outputPath = path.resolve ( process.cwd (), outputFile ); 40 | const outputExists = fs.existsSync ( outputPath ); 41 | const outputOverwrite = !outputExists || await prask.toggle ({ message: `"${outputFile}" already exists, do you want to overwrite it?` }); 42 | if ( !outputOverwrite ) return; 43 | fs.writeFileSync ( outputPath, outputBuffer ); 44 | prask.log.success ( `${isSecret ? 'Decrypted' : 'Encrypted'}, "${inputFile}" -> "${outputFile}"` ); 45 | } catch { 46 | prask.log.error ( `${isSecret ? 'Decryption' : 'Encryption'} failed, try again!` ); 47 | } 48 | }) 49 | /* RETURN */ 50 | .run (); 51 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | const ENCRYPTION_PBKDF2_ROUNDS = 500_000; 5 | 6 | const SECRET_SUFFIX = '.secret'; 7 | 8 | /* EXPORT */ 9 | 10 | export {ENCRYPTION_PBKDF2_ROUNDS, SECRET_SUFFIX}; 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Encryptor from 'tiny-encryptor'; 5 | import {ENCRYPTION_PBKDF2_ROUNDS} from './constants'; 6 | 7 | /* MAIN */ 8 | 9 | const Secret = { 10 | 11 | /* API */ 12 | 13 | encrypt: ( data: Uint8Array, secret: string ): Promise => { 14 | 15 | return Encryptor.encrypt ( data, secret, undefined, ENCRYPTION_PBKDF2_ROUNDS ); 16 | 17 | }, 18 | 19 | decrypt: ( data: Uint8Array, secret: string ): Promise => { 20 | 21 | return Encryptor.decrypt ( data, secret ); 22 | 23 | } 24 | 25 | }; 26 | 27 | /* EXPORT */ 28 | 29 | export default Secret; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsex/tsconfig.json", 3 | "compilerOptions": { 4 | "noUnusedParameters": false 5 | } 6 | } 7 | --------------------------------------------------------------------------------