mossring 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 anonymoustest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ 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 FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # anonymoustest
2
+
3
+ 用密钥文件对本地文件进行 **AES-256-GCM** 加密 / 解密的命令行工具。零第三方依赖,仅使用 Node.js 内置 `crypto`。
4
+
5
+ 隐私保护靠的是**真正的加密**,而不是改后缀或伪装——没有密钥,文件内容就是无法读取的密文。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install -g mossring
11
+ # 或免安装直接用:
12
+ npx mossring --help
13
+ ```
14
+
15
+ ## 使用
16
+
17
+ ```bash
18
+ # 1. 生成一把密钥(请离线备份)
19
+ mossring keygen -o my.key
20
+
21
+ # 2. 加密文件 → 生成 secret.pdf.enc
22
+ mossring encrypt secret.pdf -k my.key
23
+
24
+ # 3. 解密 → 还原 secret.pdf
25
+ mossring decrypt secret.pdf.enc -k my.key
26
+ ```
27
+
28
+ ### 选项
29
+
30
+ | 选项 | 说明 |
31
+ | --- | --- |
32
+ | `-k, --key` | 密钥文件路径 |
33
+ | `-o, --out` | 输出文件路径 |
34
+ | `-f, --force` | 覆盖已存在的输出文件 |
35
+ | `-h, --help` | 显示帮助 |
36
+
37
+ ## 加密细节
38
+
39
+ - **算法**:AES-256-GCM(带完整性校验,文件被篡改会在解密时报错)
40
+ - **密钥派生**:每个文件用随机 16 字节 salt 经 HKDF-SHA256 派生独立子密钥,避免 IV 重用风险
41
+ - **文件格式**:`MAGIC(5) | VERSION(1) | SALT(16) | IV(12) | TAG(16) | 密文`
42
+ - **密钥**:256-bit 随机密钥,base64 存储于密钥文件(权限 `0600`)
43
+
44
+ ## 重要提示
45
+
46
+ - **密钥丢失 = 数据永久无法恢复**,请离线备份密钥文件。
47
+ - 密钥文件不要和加密文件放在一起,也不要提交到代码仓库。
48
+ - 当前实现会将整个文件读入内存,超大文件请注意内存占用。
49
+
50
+ ## License
51
+
52
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync, chmodSync } from "node:fs";
3
+ import { argv, exit } from "node:process";
4
+ import { generateKey, encodeKey, decodeKey, encrypt, decrypt } from "../src/crypto.js";
5
+
6
+ const HELP = `mossring — 用密钥文件加密/解密本地文件 (AES-256-GCM)
7
+
8
+ 用法:
9
+ mossring keygen [-o <密钥文件>] [-f]
10
+ mossring encrypt <文件> -k <密钥文件> [-o <输出>] [-f]
11
+ mossring decrypt <文件> -k <密钥文件> [-o <输出>] [-f]
12
+
13
+ 选项:
14
+ -k, --key 密钥文件路径
15
+ -o, --out 输出文件路径
16
+ -f, --force 覆盖已存在的输出文件
17
+ -h, --help 显示帮助
18
+
19
+ 示例:
20
+ mossring keygen -o my.key
21
+ mossring encrypt secret.pdf -k my.key # 生成 secret.pdf.enc
22
+ mossring decrypt secret.pdf.enc -k my.key # 还原 secret.pdf
23
+
24
+ 注意: 密钥文件丢失后,加密文件无法恢复,请务必离线备份。`;
25
+
26
+ function fail(msg) {
27
+ console.error("错误:" + msg);
28
+ exit(1);
29
+ }
30
+
31
+ function parseArgs(args) {
32
+ const positional = [];
33
+ const opts = {};
34
+ for (let i = 0; i < args.length; i++) {
35
+ const a = args[i];
36
+ if (a === "-k" || a === "--key") opts.key = args[++i];
37
+ else if (a === "-o" || a === "--out") opts.out = args[++i];
38
+ else if (a === "-f" || a === "--force") opts.force = true;
39
+ else if (a === "-h" || a === "--help") opts.help = true;
40
+ else positional.push(a);
41
+ }
42
+ return { positional, opts };
43
+ }
44
+
45
+ function ensureWritable(path, force) {
46
+ if (existsSync(path) && !force) {
47
+ fail(`目标文件已存在:${path}(加 -f 覆盖)`);
48
+ }
49
+ }
50
+
51
+ function loadKey(keyPath) {
52
+ if (!keyPath) fail("缺少密钥文件,请用 -k <密钥文件> 指定");
53
+ if (!existsSync(keyPath)) fail(`密钥文件不存在:${keyPath}`);
54
+ try {
55
+ return decodeKey(readFileSync(keyPath, "utf8"));
56
+ } catch (e) {
57
+ return fail(e.message);
58
+ }
59
+ }
60
+
61
+ const [, , cmd, ...rest] = argv;
62
+ const { positional, opts } = parseArgs(rest);
63
+
64
+ if (!cmd || cmd === "help" || opts.help) {
65
+ console.log(HELP);
66
+ exit(0);
67
+ }
68
+
69
+ if (cmd === "keygen") {
70
+ const out = opts.out || "mossring.key";
71
+ ensureWritable(out, opts.force);
72
+ writeFileSync(out, encodeKey(generateKey()) + "\n", { mode: 0o600 });
73
+ try {
74
+ chmodSync(out, 0o600);
75
+ } catch {
76
+ /* Windows 等平台忽略权限设置 */
77
+ }
78
+ console.log(`已生成密钥:${out}`);
79
+ console.log("⚠️ 请离线备份此密钥。密钥丢失 = 加密文件永久无法恢复。");
80
+ exit(0);
81
+ }
82
+
83
+ if (cmd === "encrypt" || cmd === "decrypt") {
84
+ const input = positional[0];
85
+ if (!input) fail("缺少输入文件");
86
+ if (!existsSync(input)) fail(`输入文件不存在:${input}`);
87
+ const masterKey = loadKey(opts.key);
88
+ const data = readFileSync(input);
89
+
90
+ if (cmd === "encrypt") {
91
+ const out = opts.out || input + ".enc";
92
+ ensureWritable(out, opts.force);
93
+ writeFileSync(out, encrypt(data, masterKey));
94
+ console.log(`已加密:${input} → ${out}`);
95
+ } else {
96
+ const out =
97
+ opts.out || (input.endsWith(".enc") ? input.slice(0, -4) : input + ".dec");
98
+ ensureWritable(out, opts.force);
99
+ let plain;
100
+ try {
101
+ plain = decrypt(data, masterKey);
102
+ } catch (e) {
103
+ fail(e.message);
104
+ }
105
+ writeFileSync(out, plain);
106
+ console.log(`已解密:${input} → ${out}`);
107
+ }
108
+ exit(0);
109
+ }
110
+
111
+ fail(`未知命令:${cmd}(运行 mossring --help 查看用法)`);
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "mossring",
3
+ "version": "1.0.0",
4
+ "description": "用密钥文件对本地文件进行 AES-256-GCM 加密/解密的命令行工具",
5
+ "type": "module",
6
+ "bin": {
7
+ "mossring": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "keywords": [
19
+ "encryption",
20
+ "aes-256-gcm",
21
+ "cli",
22
+ "privacy",
23
+ "file-encryption",
24
+ "crypto"
25
+ ],
26
+ "license": "MIT"
27
+ }
package/src/crypto.js ADDED
@@ -0,0 +1,67 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, hkdfSync } from "node:crypto";
2
+
3
+ // 文件格式:MAGIC(5) | VERSION(1) | SALT(16) | IV(12) | TAG(16) | CIPHERTEXT
4
+ const MAGIC = Buffer.from("ANON1", "utf8");
5
+ const VERSION = 1;
6
+ const KEY_LEN = 32; // AES-256
7
+ const SALT_LEN = 16;
8
+ const IV_LEN = 12; // GCM 推荐 96-bit
9
+ const TAG_LEN = 16;
10
+ const HKDF_INFO = Buffer.from("anonymoustest/file-encryption/v1", "utf8");
11
+
12
+ // 生成一把全新的 256-bit 主密钥
13
+ export function generateKey() {
14
+ return randomBytes(KEY_LEN);
15
+ }
16
+
17
+ export function encodeKey(keyBuf) {
18
+ return keyBuf.toString("base64");
19
+ }
20
+
21
+ export function decodeKey(text) {
22
+ const buf = Buffer.from(String(text).trim(), "base64");
23
+ if (buf.length !== KEY_LEN) {
24
+ throw new Error(`无效的密钥文件:期望 ${KEY_LEN} 字节,实际 ${buf.length} 字节`);
25
+ }
26
+ return buf;
27
+ }
28
+
29
+ // 每个文件用随机 salt 派生独立子密钥,避免在同一主密钥下重用 IV 的风险
30
+ function deriveKey(masterKey, salt) {
31
+ return Buffer.from(hkdfSync("sha256", masterKey, salt, HKDF_INFO, KEY_LEN));
32
+ }
33
+
34
+ export function encrypt(plaintext, masterKey) {
35
+ const salt = randomBytes(SALT_LEN);
36
+ const iv = randomBytes(IV_LEN);
37
+ const key = deriveKey(masterKey, salt);
38
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
39
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
40
+ const tag = cipher.getAuthTag();
41
+ return Buffer.concat([MAGIC, Buffer.from([VERSION]), salt, iv, tag, ciphertext]);
42
+ }
43
+
44
+ export function decrypt(blob, masterKey) {
45
+ let offset = 0;
46
+ const magic = blob.subarray(offset, (offset += MAGIC.length));
47
+ if (!magic.equals(MAGIC)) {
48
+ throw new Error("文件格式无法识别:这不是本工具加密的文件");
49
+ }
50
+ const version = blob.readUInt8(offset);
51
+ offset += 1;
52
+ if (version !== VERSION) {
53
+ throw new Error(`不支持的文件版本:${version}`);
54
+ }
55
+ const salt = blob.subarray(offset, (offset += SALT_LEN));
56
+ const iv = blob.subarray(offset, (offset += IV_LEN));
57
+ const tag = blob.subarray(offset, (offset += TAG_LEN));
58
+ const ciphertext = blob.subarray(offset);
59
+ const key = deriveKey(masterKey, salt);
60
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
61
+ decipher.setAuthTag(tag);
62
+ try {
63
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
64
+ } catch {
65
+ throw new Error("解密失败:密钥不正确,或文件已损坏/被篡改");
66
+ }
67
+ }