@typinghare/trick 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc.yml +6 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/Project.xml +52 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/jsLibraryMappings.xml +6 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/misc.xml +6 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/modules.xml +8 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/prettier.xml +6 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/trick.iml +14 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/vcs.xml +6 -0
- package/.wander/jameschan312.cn@gmail.com/.idea/webResources.xml +14 -0
- package/README.md +7 -0
- package/bin/trick +3 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +65 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +51 -0
- package/dist/constant.d.ts +2 -0
- package/dist/constant.js +3 -0
- package/dist/encrypt.d.ts +12 -0
- package/dist/encrypt.js +86 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/secret.d.ts +5 -0
- package/dist/secret.js +14 -0
- package/dist/utility.d.ts +1 -0
- package/dist/utility.js +27 -0
- package/package.json +27 -0
- package/src/app.ts +84 -0
- package/src/config.ts +73 -0
- package/src/constant.ts +4 -0
- package/src/encrypt.ts +112 -0
- package/src/index.ts +6 -0
- package/src/secret.ts +14 -0
- package/src/utility.ts +38 -0
- package/test/resources/really.json +4 -0
- package/test/resources/task.yml +4 -0
- package/trick.config.json +12 -0
- package/tsconfig.json +17 -0
package/.prettierrc.yml
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<component name="ProjectCodeStyleConfiguration">
|
|
2
|
+
<code_scheme name="Project" version="173">
|
|
3
|
+
<option name="LINE_SEPARATOR" value=" " />
|
|
4
|
+
<option name="RIGHT_MARGIN" value="80" />
|
|
5
|
+
<HTMLCodeStyleSettings>
|
|
6
|
+
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
|
7
|
+
</HTMLCodeStyleSettings>
|
|
8
|
+
<JSCodeStyleSettings version="0">
|
|
9
|
+
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
|
10
|
+
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
|
11
|
+
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
|
12
|
+
<option name="USE_DOUBLE_QUOTES" value="false" />
|
|
13
|
+
<option name="FORCE_QUOTE_STYlE" value="true" />
|
|
14
|
+
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
|
15
|
+
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
|
16
|
+
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
|
17
|
+
</JSCodeStyleSettings>
|
|
18
|
+
<TypeScriptCodeStyleSettings version="0">
|
|
19
|
+
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
|
20
|
+
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
|
21
|
+
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
|
22
|
+
<option name="USE_DOUBLE_QUOTES" value="false" />
|
|
23
|
+
<option name="FORCE_QUOTE_STYlE" value="true" />
|
|
24
|
+
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
|
25
|
+
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
|
26
|
+
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
|
27
|
+
</TypeScriptCodeStyleSettings>
|
|
28
|
+
<VueCodeStyleSettings>
|
|
29
|
+
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
|
30
|
+
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
|
31
|
+
</VueCodeStyleSettings>
|
|
32
|
+
<codeStyleSettings language="HTML">
|
|
33
|
+
<option name="SOFT_MARGINS" value="80" />
|
|
34
|
+
<indentOptions>
|
|
35
|
+
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
|
36
|
+
</indentOptions>
|
|
37
|
+
</codeStyleSettings>
|
|
38
|
+
<codeStyleSettings language="JavaScript">
|
|
39
|
+
<option name="SOFT_MARGINS" value="80" />
|
|
40
|
+
</codeStyleSettings>
|
|
41
|
+
<codeStyleSettings language="TypeScript">
|
|
42
|
+
<option name="SOFT_MARGINS" value="80" />
|
|
43
|
+
</codeStyleSettings>
|
|
44
|
+
<codeStyleSettings language="Vue">
|
|
45
|
+
<option name="SOFT_MARGINS" value="80" />
|
|
46
|
+
<indentOptions>
|
|
47
|
+
<option name="INDENT_SIZE" value="4" />
|
|
48
|
+
<option name="TAB_SIZE" value="4" />
|
|
49
|
+
</indentOptions>
|
|
50
|
+
</codeStyleSettings>
|
|
51
|
+
</code_scheme>
|
|
52
|
+
</component>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
|
6
|
+
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
7
|
+
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
|
8
|
+
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
9
|
+
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
10
|
+
</content>
|
|
11
|
+
<orderEntry type="inheritedJdk" />
|
|
12
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
13
|
+
</component>
|
|
14
|
+
</module>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="WebResourcesPaths">
|
|
4
|
+
<contentEntries>
|
|
5
|
+
<entry url="file://$PROJECT_DIR$">
|
|
6
|
+
<entryData>
|
|
7
|
+
<resourceRoots>
|
|
8
|
+
<path value="file://$PROJECT_DIR$/src" />
|
|
9
|
+
</resourceRoots>
|
|
10
|
+
</entryData>
|
|
11
|
+
</entry>
|
|
12
|
+
</contentEntries>
|
|
13
|
+
</component>
|
|
14
|
+
</project>
|
package/README.md
ADDED
package/bin/trick
ADDED
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getTargetFromConfig, updateConfig } from './config.js';
|
|
3
|
+
import { decryptFiles, encryptFiles } from './encrypt.js';
|
|
4
|
+
import { getSecret } from './secret.js';
|
|
5
|
+
import { TRICK_ENCRYPTED_DIR } from './constant.js';
|
|
6
|
+
import fsExtra from 'fs-extra';
|
|
7
|
+
import { resolve_error } from './utility.js';
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program.version('Trick v1.0.0 \nby James Chen (jameschan312.cn@gmail.com)');
|
|
10
|
+
program.description('Save credential files to remote safely.');
|
|
11
|
+
program
|
|
12
|
+
.command('add')
|
|
13
|
+
.description('Adds a target.')
|
|
14
|
+
.argument('<secret-name>', 'The name of secret in the environment')
|
|
15
|
+
.argument('[files...]', 'Files this target will encrypt')
|
|
16
|
+
.action(async (secretName, files) => {
|
|
17
|
+
await updateConfig((config) => {
|
|
18
|
+
try {
|
|
19
|
+
getTargetFromConfig(config, secretName);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
config.targets.push({
|
|
23
|
+
secret_name: secretName,
|
|
24
|
+
files,
|
|
25
|
+
});
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
console.error(`Target with the secret name already exists: ${secretName}`);
|
|
29
|
+
console.error('Abort adding target');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
program
|
|
34
|
+
.command('encrypt')
|
|
35
|
+
.description('Encrypt the credential files.')
|
|
36
|
+
.argument('<secret-name>', 'The name of secret in the environment')
|
|
37
|
+
.action(async (secretName) => {
|
|
38
|
+
await updateConfig((config) => {
|
|
39
|
+
const target = getTargetFromConfig(config, secretName);
|
|
40
|
+
const secret = getSecret(target.secret_name);
|
|
41
|
+
const srcFilePaths = target.files;
|
|
42
|
+
fsExtra.ensureDir(TRICK_ENCRYPTED_DIR);
|
|
43
|
+
encryptFiles(srcFilePaths, TRICK_ENCRYPTED_DIR, secret, config.iteration_count);
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
program
|
|
48
|
+
.command('decrypt')
|
|
49
|
+
.description('Decrypt the credential files.')
|
|
50
|
+
.argument('<secret-name>', 'The name of secret in the environment')
|
|
51
|
+
.action(async (secretName) => {
|
|
52
|
+
await updateConfig((config) => {
|
|
53
|
+
const target = getTargetFromConfig(config, secretName);
|
|
54
|
+
const secret = getSecret(target.secret_name);
|
|
55
|
+
const srcFilePaths = target.files;
|
|
56
|
+
fsExtra.ensureDir(TRICK_ENCRYPTED_DIR);
|
|
57
|
+
decryptFiles(srcFilePaths, TRICK_ENCRYPTED_DIR, secret, config.iteration_count);
|
|
58
|
+
return false;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
program.parse();
|
|
62
|
+
process.on('uncaughtException', (err) => {
|
|
63
|
+
resolve_error(err);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const CONFIG_FILE_NAME: string;
|
|
2
|
+
export interface Config {
|
|
3
|
+
iteration_count: number;
|
|
4
|
+
targets: Target[];
|
|
5
|
+
}
|
|
6
|
+
export interface Target {
|
|
7
|
+
secret_name: string;
|
|
8
|
+
files: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare class WriteConfigError extends Error {
|
|
11
|
+
}
|
|
12
|
+
export declare class ReadConfigError extends Error {
|
|
13
|
+
}
|
|
14
|
+
export declare function writeConfig(config: Config): Promise<void>;
|
|
15
|
+
export declare function readConfig(): Promise<Config | null>;
|
|
16
|
+
export declare function updateConfig(callback: UpdateConfigCallback): Promise<void>;
|
|
17
|
+
export type UpdateConfigCallback = (config: Config) => boolean;
|
|
18
|
+
export declare class TargetNotFoundError extends Error {
|
|
19
|
+
readonly secretName: string;
|
|
20
|
+
constructor(secretName: string);
|
|
21
|
+
}
|
|
22
|
+
export declare function getTargetFromConfig(config: Config, secretName: string): Target;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fsExtra from 'fs-extra';
|
|
2
|
+
export const CONFIG_FILE_NAME = 'trick.config.json';
|
|
3
|
+
const defaultConfig = {
|
|
4
|
+
iteration_count: 114514,
|
|
5
|
+
targets: [],
|
|
6
|
+
};
|
|
7
|
+
export class WriteConfigError extends Error {
|
|
8
|
+
}
|
|
9
|
+
export class ReadConfigError extends Error {
|
|
10
|
+
}
|
|
11
|
+
export async function writeConfig(config) {
|
|
12
|
+
try {
|
|
13
|
+
await fsExtra.writeJson(CONFIG_FILE_NAME, config);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
throw new WriteConfigError();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function readConfig() {
|
|
20
|
+
if (!fsExtra.existsSync(CONFIG_FILE_NAME)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return (await fsExtra.readJSON(CONFIG_FILE_NAME));
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
throw new ReadConfigError();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function updateConfig(callback) {
|
|
31
|
+
const config = (await readConfig()) || defaultConfig;
|
|
32
|
+
if (callback(config)) {
|
|
33
|
+
await writeConfig(config);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class TargetNotFoundError extends Error {
|
|
37
|
+
secretName;
|
|
38
|
+
constructor(secretName) {
|
|
39
|
+
super(`Target not found: ${secretName}`);
|
|
40
|
+
this.secretName = secretName;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function getTargetFromConfig(config, secretName) {
|
|
44
|
+
const targets = config.targets;
|
|
45
|
+
for (const target of targets) {
|
|
46
|
+
if (target.secret_name === secretName) {
|
|
47
|
+
return target;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw new TargetNotFoundError(secretName);
|
|
51
|
+
}
|
package/dist/constant.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class FailToEncryptFileError extends Error {
|
|
2
|
+
readonly srcFilePath: string;
|
|
3
|
+
constructor(srcFilePath: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class FailToDecryptFileError extends Error {
|
|
6
|
+
readonly destFilePath: string;
|
|
7
|
+
constructor(destFilePath: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function encryptFile(srcFilePath: string, destFilePath: string, secret: string, iteration_count: number): Promise<void>;
|
|
10
|
+
export declare function decryptFile(srcFilePath: string, destFilePath: string, secret: string, iteration_count: number): Promise<void>;
|
|
11
|
+
export declare function encryptFiles(srcFilePaths: string[], destDir: string, secret: string, iteration_count: number): Promise<void>;
|
|
12
|
+
export declare function decryptFiles(srcFilePaths: string[], destDir: string, secret: string, iteration_count: number): Promise<void>;
|
package/dist/encrypt.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import fsExtra from 'fs-extra';
|
|
4
|
+
export class FailToEncryptFileError extends Error {
|
|
5
|
+
srcFilePath;
|
|
6
|
+
constructor(srcFilePath) {
|
|
7
|
+
super(`Fail to encrypt source file: ${srcFilePath}`);
|
|
8
|
+
this.srcFilePath = srcFilePath;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class FailToDecryptFileError extends Error {
|
|
12
|
+
destFilePath;
|
|
13
|
+
constructor(destFilePath) {
|
|
14
|
+
super(`Fail to decrypt destination file: ${destFilePath}`);
|
|
15
|
+
this.destFilePath = destFilePath;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function encryptFile(srcFilePath, destFilePath, secret, iteration_count) {
|
|
19
|
+
if (!(await fsExtra.pathExists(srcFilePath))) {
|
|
20
|
+
throw new FailToEncryptFileError(srcFilePath);
|
|
21
|
+
}
|
|
22
|
+
const command = [
|
|
23
|
+
'openssl',
|
|
24
|
+
'enc',
|
|
25
|
+
'-aes-256-cbc',
|
|
26
|
+
'-salt',
|
|
27
|
+
'-pbkdf2',
|
|
28
|
+
'-iter',
|
|
29
|
+
iteration_count,
|
|
30
|
+
'-in',
|
|
31
|
+
srcFilePath,
|
|
32
|
+
'-out',
|
|
33
|
+
destFilePath,
|
|
34
|
+
'-pass',
|
|
35
|
+
`pass:${secret}`,
|
|
36
|
+
].join(' ');
|
|
37
|
+
await fsExtra.ensureDir(path.dirname(destFilePath));
|
|
38
|
+
try {
|
|
39
|
+
await execa(`${command}`, { shell: true });
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new FailToEncryptFileError(srcFilePath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function decryptFile(srcFilePath, destFilePath, secret, iteration_count) {
|
|
46
|
+
if (!(await fsExtra.pathExists(destFilePath))) {
|
|
47
|
+
throw new FailToDecryptFileError(destFilePath);
|
|
48
|
+
}
|
|
49
|
+
const command = [
|
|
50
|
+
'openssl',
|
|
51
|
+
'enc',
|
|
52
|
+
'-d',
|
|
53
|
+
'-aes-256-cbc',
|
|
54
|
+
'-salt',
|
|
55
|
+
'-pbkdf2',
|
|
56
|
+
'-iter',
|
|
57
|
+
iteration_count,
|
|
58
|
+
'-in',
|
|
59
|
+
destFilePath,
|
|
60
|
+
'-out',
|
|
61
|
+
srcFilePath,
|
|
62
|
+
'-pass',
|
|
63
|
+
`pass:${secret}`,
|
|
64
|
+
].join(' ');
|
|
65
|
+
await fsExtra.ensureDir(path.dirname(srcFilePath));
|
|
66
|
+
try {
|
|
67
|
+
await execa(`${command}`, { shell: true });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw new FailToDecryptFileError(destFilePath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function encryptFiles(srcFilePaths, destDir, secret, iteration_count) {
|
|
74
|
+
for (const srcFilePath of srcFilePaths) {
|
|
75
|
+
const destFilePath = path.join(destDir, srcFilePath);
|
|
76
|
+
await encryptFile(srcFilePath, destFilePath, secret, iteration_count);
|
|
77
|
+
console.log(`[ENCRYPTED] ${srcFilePath} -> ${destFilePath}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function decryptFiles(srcFilePaths, destDir, secret, iteration_count) {
|
|
81
|
+
for (const srcFilePath of srcFilePaths) {
|
|
82
|
+
const destFilePath = path.join(destDir, srcFilePath);
|
|
83
|
+
await decryptFile(srcFilePath, destFilePath, secret, iteration_count);
|
|
84
|
+
console.log(`[DECRYPTED] ${destFilePath} -> ${srcFilePath}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/secret.d.ts
ADDED
package/dist/secret.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class SecretNotFoundError extends Error {
|
|
2
|
+
secretName;
|
|
3
|
+
constructor(secretName) {
|
|
4
|
+
super(`Secret ${secretName} is not presented in the environment`);
|
|
5
|
+
this.secretName = secretName;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function getSecret(secretName) {
|
|
9
|
+
const secret = process.env[secretName];
|
|
10
|
+
if (secret === undefined) {
|
|
11
|
+
throw new SecretNotFoundError(secretName);
|
|
12
|
+
}
|
|
13
|
+
return secret;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolve_error(err: any): void;
|
package/dist/utility.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ReadConfigError, TargetNotFoundError, WriteConfigError, } from './config.js';
|
|
2
|
+
import { FailToDecryptFileError, FailToEncryptFileError } from './encrypt.js';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
export function resolve_error(err) {
|
|
5
|
+
if (!(err instanceof Error)) {
|
|
6
|
+
console.error(`Unknown error: ${err}`);
|
|
7
|
+
process.exit(2);
|
|
8
|
+
}
|
|
9
|
+
if (err instanceof WriteConfigError) {
|
|
10
|
+
console.error(chalk.red('Fail to write Trick config file'));
|
|
11
|
+
}
|
|
12
|
+
else if (err instanceof ReadConfigError) {
|
|
13
|
+
console.error(chalk.red('Fail to read Trick config file'));
|
|
14
|
+
}
|
|
15
|
+
else if (err instanceof TargetNotFoundError) {
|
|
16
|
+
console.error(chalk.red(err.message));
|
|
17
|
+
}
|
|
18
|
+
else if (err instanceof FailToEncryptFileError) {
|
|
19
|
+
console.error(chalk.red(err.message));
|
|
20
|
+
console.error(chalk.yellow('Make sure the file exists and you have enough permission to access'));
|
|
21
|
+
}
|
|
22
|
+
else if (err instanceof FailToDecryptFileError) {
|
|
23
|
+
console.error(chalk.red(err.message));
|
|
24
|
+
console.error(chalk.yellow('Make sure the file exists and you have enough permission to access'));
|
|
25
|
+
}
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typinghare/trick",
|
|
3
|
+
"description": "Save credential files to remote safely.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "James Chen",
|
|
10
|
+
"email": "jameschan312.cn@gmail.com"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/typinghare/trick",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"chalk": "^5.3.0",
|
|
16
|
+
"commander": "^12.1.0",
|
|
17
|
+
"execa": "^9.3.0",
|
|
18
|
+
"fs-extra": "^11.2.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/fs-extra": "^11.0.4",
|
|
22
|
+
"prettier": "^3.3.3"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { getTargetFromConfig, Target, updateConfig } from './config.js'
|
|
3
|
+
import { decryptFiles, encryptFiles } from './encrypt.js'
|
|
4
|
+
import { getSecret } from './secret.js'
|
|
5
|
+
import { TRICK_ENCRYPTED_DIR } from './constant.js'
|
|
6
|
+
import fsExtra from 'fs-extra'
|
|
7
|
+
import { resolve_error } from './utility.js'
|
|
8
|
+
|
|
9
|
+
const program = new Command()
|
|
10
|
+
|
|
11
|
+
program.version('Trick v1.0.0 \nby James Chen (jameschan312.cn@gmail.com)')
|
|
12
|
+
program.description('Save credential files to remote safely.')
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('add')
|
|
16
|
+
.description('Adds a target.')
|
|
17
|
+
.argument('<secret-name>', 'The name of secret in the environment')
|
|
18
|
+
.argument('[files...]', 'Files this target will encrypt')
|
|
19
|
+
.action(async (secretName: string, files: string[]): Promise<void> => {
|
|
20
|
+
await updateConfig((config) => {
|
|
21
|
+
try {
|
|
22
|
+
getTargetFromConfig(config, secretName)
|
|
23
|
+
} catch (err) {
|
|
24
|
+
config.targets.push({
|
|
25
|
+
secret_name: secretName,
|
|
26
|
+
files,
|
|
27
|
+
})
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.error(
|
|
32
|
+
`Target with the secret name already exists: ${secretName}`
|
|
33
|
+
)
|
|
34
|
+
console.error('Abort adding target')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command('encrypt')
|
|
41
|
+
.description('Encrypt the credential files.')
|
|
42
|
+
.argument('<secret-name>', 'The name of secret in the environment')
|
|
43
|
+
.action(async (secretName: string): Promise<void> => {
|
|
44
|
+
await updateConfig((config) => {
|
|
45
|
+
const target: Target = getTargetFromConfig(config, secretName)
|
|
46
|
+
const secret: string = getSecret(target.secret_name)
|
|
47
|
+
const srcFilePaths: string[] = target.files
|
|
48
|
+
fsExtra.ensureDir(TRICK_ENCRYPTED_DIR)
|
|
49
|
+
encryptFiles(
|
|
50
|
+
srcFilePaths,
|
|
51
|
+
TRICK_ENCRYPTED_DIR,
|
|
52
|
+
secret,
|
|
53
|
+
config.iteration_count
|
|
54
|
+
)
|
|
55
|
+
return false
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command('decrypt')
|
|
61
|
+
.description('Decrypt the credential files.')
|
|
62
|
+
.argument('<secret-name>', 'The name of secret in the environment')
|
|
63
|
+
.action(async (secretName: string): Promise<void> => {
|
|
64
|
+
await updateConfig((config) => {
|
|
65
|
+
const target: Target = getTargetFromConfig(config, secretName)
|
|
66
|
+
const secret: string = getSecret(target.secret_name)
|
|
67
|
+
const srcFilePaths: string[] = target.files
|
|
68
|
+
fsExtra.ensureDir(TRICK_ENCRYPTED_DIR)
|
|
69
|
+
decryptFiles(
|
|
70
|
+
srcFilePaths,
|
|
71
|
+
TRICK_ENCRYPTED_DIR,
|
|
72
|
+
secret,
|
|
73
|
+
config.iteration_count
|
|
74
|
+
)
|
|
75
|
+
return false
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
program.parse()
|
|
80
|
+
|
|
81
|
+
process.on('uncaughtException', (err) => {
|
|
82
|
+
resolve_error(err)
|
|
83
|
+
process.exit(1)
|
|
84
|
+
})
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fsExtra from 'fs-extra'
|
|
2
|
+
|
|
3
|
+
export const CONFIG_FILE_NAME: string = 'trick.config.json'
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
iteration_count: number
|
|
7
|
+
targets: Target[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Target {
|
|
11
|
+
secret_name: string
|
|
12
|
+
files: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const defaultConfig: Config = {
|
|
16
|
+
iteration_count: 114514,
|
|
17
|
+
targets: [],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class WriteConfigError extends Error {}
|
|
21
|
+
|
|
22
|
+
export class ReadConfigError extends Error {}
|
|
23
|
+
|
|
24
|
+
export async function writeConfig(config: Config): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
await fsExtra.writeJson(CONFIG_FILE_NAME, config)
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new WriteConfigError()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readConfig(): Promise<Config | null> {
|
|
33
|
+
if (!fsExtra.existsSync(CONFIG_FILE_NAME)) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return (await fsExtra.readJSON(CONFIG_FILE_NAME)) as Config
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw new ReadConfigError()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function updateConfig(
|
|
45
|
+
callback: UpdateConfigCallback
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const config: Config = (await readConfig()) || defaultConfig
|
|
48
|
+
if (callback(config)) {
|
|
49
|
+
await writeConfig(config)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type UpdateConfigCallback = (config: Config) => boolean
|
|
54
|
+
|
|
55
|
+
export class TargetNotFoundError extends Error {
|
|
56
|
+
public constructor(public readonly secretName: string) {
|
|
57
|
+
super(`Target not found: ${secretName}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getTargetFromConfig(
|
|
62
|
+
config: Config,
|
|
63
|
+
secretName: string
|
|
64
|
+
): Target {
|
|
65
|
+
const targets = config.targets
|
|
66
|
+
for (const target of targets) {
|
|
67
|
+
if (target.secret_name === secretName) {
|
|
68
|
+
return target
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new TargetNotFoundError(secretName)
|
|
73
|
+
}
|
package/src/constant.ts
ADDED
package/src/encrypt.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import fsExtra from 'fs-extra'
|
|
4
|
+
|
|
5
|
+
export class FailToEncryptFileError extends Error {
|
|
6
|
+
public constructor(public readonly srcFilePath: string) {
|
|
7
|
+
super(`Fail to encrypt source file: ${srcFilePath}`)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class FailToDecryptFileError extends Error {
|
|
12
|
+
public constructor(public readonly destFilePath: string) {
|
|
13
|
+
super(`Fail to decrypt destination file: ${destFilePath}`)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function encryptFile(
|
|
18
|
+
srcFilePath: string,
|
|
19
|
+
destFilePath: string,
|
|
20
|
+
secret: string,
|
|
21
|
+
iteration_count: number
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
if (!(await fsExtra.pathExists(srcFilePath))) {
|
|
24
|
+
throw new FailToEncryptFileError(srcFilePath)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const command: string = [
|
|
28
|
+
'openssl',
|
|
29
|
+
'enc',
|
|
30
|
+
'-aes-256-cbc',
|
|
31
|
+
'-salt',
|
|
32
|
+
'-pbkdf2',
|
|
33
|
+
'-iter',
|
|
34
|
+
iteration_count,
|
|
35
|
+
'-in',
|
|
36
|
+
srcFilePath,
|
|
37
|
+
'-out',
|
|
38
|
+
destFilePath,
|
|
39
|
+
'-pass',
|
|
40
|
+
`pass:${secret}`,
|
|
41
|
+
].join(' ')
|
|
42
|
+
|
|
43
|
+
await fsExtra.ensureDir(path.dirname(destFilePath))
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await execa(`${command}`, { shell: true })
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new FailToEncryptFileError(srcFilePath)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function decryptFile(
|
|
53
|
+
srcFilePath: string,
|
|
54
|
+
destFilePath: string,
|
|
55
|
+
secret: string,
|
|
56
|
+
iteration_count: number
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
if (!(await fsExtra.pathExists(destFilePath))) {
|
|
59
|
+
throw new FailToDecryptFileError(destFilePath)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const command: string = [
|
|
63
|
+
'openssl',
|
|
64
|
+
'enc',
|
|
65
|
+
'-d',
|
|
66
|
+
'-aes-256-cbc',
|
|
67
|
+
'-salt',
|
|
68
|
+
'-pbkdf2',
|
|
69
|
+
'-iter',
|
|
70
|
+
iteration_count,
|
|
71
|
+
'-in',
|
|
72
|
+
destFilePath,
|
|
73
|
+
'-out',
|
|
74
|
+
srcFilePath,
|
|
75
|
+
'-pass',
|
|
76
|
+
`pass:${secret}`,
|
|
77
|
+
].join(' ')
|
|
78
|
+
|
|
79
|
+
await fsExtra.ensureDir(path.dirname(srcFilePath))
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await execa(`${command}`, { shell: true })
|
|
83
|
+
} catch (err) {
|
|
84
|
+
throw new FailToDecryptFileError(destFilePath)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function encryptFiles(
|
|
89
|
+
srcFilePaths: string[],
|
|
90
|
+
destDir: string,
|
|
91
|
+
secret: string,
|
|
92
|
+
iteration_count: number
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
for (const srcFilePath of srcFilePaths) {
|
|
95
|
+
const destFilePath: string = path.join(destDir, srcFilePath)
|
|
96
|
+
await encryptFile(srcFilePath, destFilePath, secret, iteration_count)
|
|
97
|
+
console.log(`[ENCRYPTED] ${srcFilePath} -> ${destFilePath}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function decryptFiles(
|
|
102
|
+
srcFilePaths: string[],
|
|
103
|
+
destDir: string,
|
|
104
|
+
secret: string,
|
|
105
|
+
iteration_count: number
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
for (const srcFilePath of srcFilePaths) {
|
|
108
|
+
const destFilePath: string = path.join(destDir, srcFilePath)
|
|
109
|
+
await decryptFile(srcFilePath, destFilePath, secret, iteration_count)
|
|
110
|
+
console.log(`[DECRYPTED] ${destFilePath} -> ${srcFilePath}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/index.ts
ADDED
package/src/secret.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class SecretNotFoundError extends Error {
|
|
2
|
+
public constructor(public readonly secretName: string) {
|
|
3
|
+
super(`Secret ${secretName} is not presented in the environment`)
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getSecret(secretName: string): string {
|
|
8
|
+
const secret = process.env[secretName]
|
|
9
|
+
if (secret === undefined) {
|
|
10
|
+
throw new SecretNotFoundError(secretName)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return secret
|
|
14
|
+
}
|
package/src/utility.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReadConfigError,
|
|
3
|
+
TargetNotFoundError,
|
|
4
|
+
WriteConfigError,
|
|
5
|
+
} from './config.js'
|
|
6
|
+
import { FailToDecryptFileError, FailToEncryptFileError } from './encrypt.js'
|
|
7
|
+
import chalk from 'chalk'
|
|
8
|
+
|
|
9
|
+
export function resolve_error(err: any): void {
|
|
10
|
+
if (!(err instanceof Error)) {
|
|
11
|
+
console.error(`Unknown error: ${err}`)
|
|
12
|
+
process.exit(2)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (err instanceof WriteConfigError) {
|
|
16
|
+
console.error(chalk.red('Fail to write Trick config file'))
|
|
17
|
+
} else if (err instanceof ReadConfigError) {
|
|
18
|
+
console.error(chalk.red('Fail to read Trick config file'))
|
|
19
|
+
} else if (err instanceof TargetNotFoundError) {
|
|
20
|
+
console.error(chalk.red(err.message))
|
|
21
|
+
} else if (err instanceof FailToEncryptFileError) {
|
|
22
|
+
console.error(chalk.red(err.message))
|
|
23
|
+
console.error(
|
|
24
|
+
chalk.yellow(
|
|
25
|
+
'Make sure the file exists and you have enough permission to access'
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
} else if (err instanceof FailToDecryptFileError) {
|
|
29
|
+
console.error(chalk.red(err.message))
|
|
30
|
+
console.error(
|
|
31
|
+
chalk.yellow(
|
|
32
|
+
'Make sure the file exists and you have enough permission to access'
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipDefaultLibCheck": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"moduleResolution": "NodeNext",
|
|
12
|
+
"declaration": true
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"src/**/*.ts"
|
|
16
|
+
]
|
|
17
|
+
}
|