mossring 1.0.1 → 2.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/README.md CHANGED
@@ -1,50 +1,60 @@
1
- # anonymoustest
1
+ # mossring
2
2
 
3
- 用密钥文件对本地文件进行 **AES-256-GCM** 加密 / 解密的命令行工具。零第三方依赖,仅使用 Node.js 内置 `crypto`。
3
+ 非对称加密本地文件的命令行工具:**X25519 + AES-256-GCM**(密封盒 / sealed box)。零第三方依赖,仅用 Node.js 内置 `crypto`。
4
4
 
5
- 隐私保护靠的是**真正的加密**,而不是改后缀或伪装——没有密钥,文件内容就是无法读取的密文。
5
+ - **加密**用内置公钥,**无需指定任何密钥**,方便。
6
+ - **解密**必须用你本地保存的**私钥**,安全。
7
+ - 公钥可以公开(包里就内置了一个);只有持有私钥的人能解密。
8
+
9
+ > 隐私靠真正的加密,而非伪装。没有私钥,文件内容就是无法读取的密文。
6
10
 
7
11
  ## 安装
8
12
 
9
13
  ```bash
10
14
  npm install -g mossring
11
15
  # 或免安装直接用:
12
- npx mossring --help
16
+ npx mossring help
13
17
  ```
14
18
 
15
19
  ## 使用
16
20
 
17
21
  ```bash
18
- # 1. 生成一把密钥(请离线备份)
19
- mossring keygen -o my.key
22
+ # 用内置公钥加密 → 生成 secret.pdf.enc(不需要任何密钥)
23
+ mossring encrypt secret.pdf
24
+
25
+ # 用你的私钥解密
26
+ mossring decrypt secret.pdf.enc -k mossring.priv
27
+ ```
20
28
 
21
- # 2. 加密文件 → 生成 secret.pdf.enc
22
- mossring encrypt secret.pdf -k my.key
29
+ ### 自己生成密钥对
23
30
 
24
- # 3. 解密 → 还原 secret.pdf
25
- mossring decrypt secret.pdf.enc -k my.key
31
+ ```bash
32
+ mossring keygen -o mykey # 生成 mykey.priv(私钥)和 mykey.pub(公钥)
33
+ mossring encrypt data.zip -p mykey.pub # 用指定公钥加密
34
+ mossring decrypt data.zip.enc -k mykey.priv
26
35
  ```
27
36
 
28
37
  ### 选项
29
38
 
30
39
  | 选项 | 说明 |
31
40
  | --- | --- |
32
- | `-k, --key` | 密钥文件路径 |
41
+ | `-p, --pub` | 接收方公钥文件(加密时不指定则用内置公钥) |
42
+ | `-k, --key` | 私钥文件(解密时必需) |
33
43
  | `-o, --out` | 输出文件路径 |
34
44
  | `-f, --force` | 覆盖已存在的输出文件 |
35
45
  | `-h, --help` | 显示帮助 |
36
46
 
37
47
  ## 加密细节
38
48
 
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`)
49
+ - **密钥协商**:X25519 ECDH,每次加密用一次性临时密钥对(前向保密)
50
+ - **对称加密**:AES-256-GCM(带完整性校验,篡改会在解密时报错)
51
+ - **密钥派生**:HKDF-SHA256,salt 绑定临时公钥与接收方公钥
52
+ - **文件格式**:`MAGIC(5) | VERSION(1) | EPK_LEN(2) | 临时公钥 | IV(12) | TAG(16) | 密文`
43
53
 
44
54
  ## 重要提示
45
55
 
46
- - **密钥丢失 = 数据永久无法恢复**,请离线备份密钥文件。
47
- - 密钥文件不要和加密文件放在一起,也不要提交到代码仓库。
56
+ - **私钥丢失 = 数据永久无法恢复**,请离线备份私钥文件。
57
+ - 公钥可公开;私钥绝不要提交到仓库或发布到 npm。
48
58
  - 当前实现会将整个文件读入内存,超大文件请注意内存占用。
49
59
 
50
60
  ## License
package/bin/cli.js CHANGED
@@ -1,27 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, writeFileSync, existsSync, chmodSync } from "node:fs";
3
3
  import { argv, exit } from "node:process";
4
- import { generateKey, encodeKey, decodeKey, encrypt, decrypt } from "../src/crypto.js";
4
+ import { generateKeyPair, encrypt, decrypt } from "../src/crypto.js";
5
+ import { BUILTIN_PUBLIC_KEY } from "../src/recipient.js";
5
6
 
6
- const HELP = `mossring — 用密钥文件加密/解密本地文件 (AES-256-GCM)
7
+ const HELP = `mossring — 非对称加密本地文件 (X25519 + AES-256-GCM)
8
+
9
+ 加密用内置公钥,无需指定密钥;解密用你本地保存的私钥。
7
10
 
8
11
  用法:
9
- mossring keygen [-o <密钥文件>] [-f]
10
- mossring encrypt <文件> -k <密钥文件> [-o <输出>] [-f]
11
- mossring decrypt <文件> -k <密钥文件> [-o <输出>] [-f]
12
+ mossring keygen [-o <前缀>] [-f] 生成密钥对(<前缀>.priv / <前缀>.pub)
13
+ mossring encrypt <文件> [-p <公钥文件>] [-o <输出>] [-f]
14
+ mossring decrypt <文件> -k <私钥文件> [-o <输出>] [-f]
12
15
 
13
16
  选项:
14
- -k, --key 密钥文件路径
17
+ -p, --pub 指定接收方公钥文件(不指定则用内置公钥)
18
+ -k, --key 私钥文件路径(解密用)
15
19
  -o, --out 输出文件路径
16
20
  -f, --force 覆盖已存在的输出文件
17
21
  -h, --help 显示帮助
18
22
 
19
23
  示例:
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
24
+ mossring encrypt secret.pdf # 用内置公钥加密 → secret.pdf.enc
25
+ mossring decrypt secret.pdf.enc -k mossring.priv
23
26
 
24
- 注意: 密钥文件丢失后,加密文件无法恢复,请务必离线备份。`;
27
+ 注意: 公钥可公开,私钥务必离线保存。私钥丢失 = 加密文件无法恢复。`;
25
28
 
26
29
  function fail(msg) {
27
30
  console.error("错误:" + msg);
@@ -34,6 +37,7 @@ function parseArgs(args) {
34
37
  for (let i = 0; i < args.length; i++) {
35
38
  const a = args[i];
36
39
  if (a === "-k" || a === "--key") opts.key = args[++i];
40
+ else if (a === "-p" || a === "--pub") opts.pub = args[++i];
37
41
  else if (a === "-o" || a === "--out") opts.out = args[++i];
38
42
  else if (a === "-f" || a === "--force") opts.force = true;
39
43
  else if (a === "-h" || a === "--help") opts.help = true;
@@ -48,14 +52,9 @@ function ensureWritable(path, force) {
48
52
  }
49
53
  }
50
54
 
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
- }
55
+ function readKeyFile(path, what) {
56
+ if (!existsSync(path)) fail(`${what}不存在:${path}`);
57
+ return readFileSync(path, "utf8").trim();
59
58
  }
60
59
 
61
60
  const [, , cmd, ...rest] = argv;
@@ -73,44 +72,66 @@ if (opts.help) {
73
72
  }
74
73
 
75
74
  if (cmd === "keygen") {
76
- const out = opts.out || "mossring.key";
77
- ensureWritable(out, opts.force);
78
- writeFileSync(out, encodeKey(generateKey()) + "\n", { mode: 0o600 });
75
+ const prefix = opts.out || "mossring";
76
+ const privPath = `${prefix}.priv`;
77
+ const pubPath = `${prefix}.pub`;
78
+ ensureWritable(privPath, opts.force);
79
+ ensureWritable(pubPath, opts.force);
80
+ const { publicKeyB64, privateKeyB64 } = generateKeyPair();
81
+ writeFileSync(privPath, privateKeyB64 + "\n", { mode: 0o600 });
79
82
  try {
80
- chmodSync(out, 0o600);
83
+ chmodSync(privPath, 0o600);
81
84
  } catch {
82
- /* Windows 等平台忽略权限设置 */
85
+ /* POSIX 平台忽略 */
83
86
  }
84
- console.log(`已生成密钥:${out}`);
85
- console.log("⚠️ 请离线备份此密钥。密钥丢失 = 加密文件永久无法恢复。");
87
+ writeFileSync(pubPath, publicKeyB64 + "\n");
88
+ console.log(`已生成密钥对:`);
89
+ console.log(` 私钥 → ${privPath}(请离线保存,切勿公开)`);
90
+ console.log(` 公钥 → ${pubPath}(可公开)`);
91
+ console.log("⚠️ 私钥丢失 = 加密文件永久无法恢复。");
86
92
  exit(0);
87
93
  }
88
94
 
89
- if (cmd === "encrypt" || cmd === "decrypt") {
95
+ if (cmd === "encrypt") {
90
96
  const input = positional[0];
91
97
  if (!input) fail("缺少输入文件");
92
98
  if (!existsSync(input)) fail(`输入文件不存在:${input}`);
93
- const masterKey = loadKey(opts.key);
99
+ const pub = opts.pub ? readKeyFile(opts.pub, "公钥文件") : BUILTIN_PUBLIC_KEY;
100
+ if (!pub) {
101
+ fail("没有可用的公钥:包内未内置公钥,请用 -p <公钥文件> 指定,或先运行 mossring keygen");
102
+ }
103
+ const out = opts.out || input + ".enc";
104
+ ensureWritable(out, opts.force);
94
105
  const data = readFileSync(input);
106
+ let blob;
107
+ try {
108
+ blob = encrypt(data, pub);
109
+ } catch (e) {
110
+ fail(e.message);
111
+ }
112
+ writeFileSync(out, blob);
113
+ console.log(`已加密:${input} → ${out}`);
114
+ exit(0);
115
+ }
95
116
 
96
- if (cmd === "encrypt") {
97
- const out = opts.out || input + ".enc";
98
- ensureWritable(out, opts.force);
99
- writeFileSync(out, encrypt(data, masterKey));
100
- console.log(`已加密:${input} ${out}`);
101
- } else {
102
- const out =
103
- opts.out || (input.endsWith(".enc") ? input.slice(0, -4) : input + ".dec");
104
- ensureWritable(out, opts.force);
105
- let plain;
106
- try {
107
- plain = decrypt(data, masterKey);
108
- } catch (e) {
109
- fail(e.message);
110
- }
111
- writeFileSync(out, plain);
112
- console.log(`已解密:${input} → ${out}`);
117
+ if (cmd === "decrypt") {
118
+ const input = positional[0];
119
+ if (!input) fail("缺少输入文件");
120
+ if (!existsSync(input)) fail(`输入文件不存在:${input}`);
121
+ if (!opts.key) fail("缺少私钥文件,请用 -k <私钥文件> 指定");
122
+ const priv = readKeyFile(opts.key, "私钥文件");
123
+ const out =
124
+ opts.out || (input.endsWith(".enc") ? input.slice(0, -4) : input + ".dec");
125
+ ensureWritable(out, opts.force);
126
+ const data = readFileSync(input);
127
+ let plain;
128
+ try {
129
+ plain = decrypt(data, priv);
130
+ } catch (e) {
131
+ fail(e.message);
113
132
  }
133
+ writeFileSync(out, plain);
134
+ console.log(`已解密:${input} → ${out}`);
114
135
  exit(0);
115
136
  }
116
137
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mossring",
3
- "version": "1.0.1",
4
- "description": "用密钥文件对本地文件进行 AES-256-GCM 加密/解密的命令行工具",
3
+ "version": "2.0.0",
4
+ "description": "非对称加密本地文件 (X25519 + AES-256-GCM):内置公钥加密,本地私钥解密",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mossring": "bin/cli.js"
package/src/crypto.js CHANGED
@@ -1,47 +1,85 @@
1
- import { createCipheriv, createDecipheriv, randomBytes, hkdfSync } from "node:crypto";
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ createPublicKey,
5
+ createPrivateKey,
6
+ generateKeyPairSync,
7
+ diffieHellman,
8
+ randomBytes,
9
+ hkdfSync,
10
+ } from "node:crypto";
2
11
 
3
- // 文件格式:MAGIC(5) | VERSION(1) | SALT(16) | IV(12) | TAG(16) | CIPHERTEXT
4
- const MAGIC = Buffer.from("ANON1", "utf8");
5
- const VERSION = 1;
12
+ // 非对称「密封盒」格式 v2(X25519 + AES-256-GCM):
13
+ // MAGIC(5) | VERSION(1) | EPK_LEN(2) | 临时公钥(DER) | IV(12) | TAG(16) | 密文
14
+ const MAGIC = Buffer.from("ANON2", "utf8");
15
+ const VERSION = 2;
6
16
  const KEY_LEN = 32; // AES-256
7
- const SALT_LEN = 16;
8
- const IV_LEN = 12; // GCM 推荐 96-bit
17
+ const IV_LEN = 12;
9
18
  const TAG_LEN = 16;
10
- const HKDF_INFO = Buffer.from("anonymoustest/file-encryption/v1", "utf8");
19
+ const HKDF_INFO = Buffer.from("mossring/sealed-box/v2", "utf8");
11
20
 
12
- // 生成一把全新的 256-bit 主密钥
13
- export function generateKey() {
14
- return randomBytes(KEY_LEN);
21
+ // 生成一对 X25519 密钥;公钥可公开内置,私钥务必本地保存
22
+ export function generateKeyPair() {
23
+ const { publicKey, privateKey } = generateKeyPairSync("x25519");
24
+ return {
25
+ publicKeyB64: publicKey.export({ type: "spki", format: "der" }).toString("base64"),
26
+ privateKeyB64: privateKey.export({ type: "pkcs8", format: "der" }).toString("base64"),
27
+ };
15
28
  }
16
29
 
17
- export function encodeKey(keyBuf) {
18
- return keyBuf.toString("base64");
30
+ function importPublicKey(b64) {
31
+ try {
32
+ return createPublicKey({
33
+ key: Buffer.from(String(b64).trim(), "base64"),
34
+ format: "der",
35
+ type: "spki",
36
+ });
37
+ } catch {
38
+ throw new Error("无效的公钥");
39
+ }
19
40
  }
20
41
 
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} 字节`);
42
+ function importPrivateKey(b64) {
43
+ try {
44
+ return createPrivateKey({
45
+ key: Buffer.from(String(b64).trim(), "base64"),
46
+ format: "der",
47
+ type: "pkcs8",
48
+ });
49
+ } catch {
50
+ throw new Error("无效的私钥文件");
25
51
  }
26
- return buf;
27
52
  }
28
53
 
29
- // 每个文件用随机 salt 派生独立子密钥,避免在同一主密钥下重用 IV 的风险
30
- function deriveKey(masterKey, salt) {
31
- return Buffer.from(hkdfSync("sha256", masterKey, salt, HKDF_INFO, KEY_LEN));
54
+ // 把双方公钥并入 HKDF salt,将密钥绑定到本次收发上下文
55
+ function deriveKey(sharedSecret, ephemeralPubDer, recipientPubDer) {
56
+ const salt = Buffer.concat([ephemeralPubDer, recipientPubDer]);
57
+ return Buffer.from(hkdfSync("sha256", sharedSecret, salt, HKDF_INFO, KEY_LEN));
32
58
  }
33
59
 
34
- export function encrypt(plaintext, masterKey) {
35
- const salt = randomBytes(SALT_LEN);
60
+ export function encrypt(plaintext, recipientPubB64) {
61
+ const recipientPub = importPublicKey(recipientPubB64);
62
+ const recipientPubDer = recipientPub.export({ type: "spki", format: "der" });
63
+
64
+ // 每次加密用一把一次性临时密钥对,保证前向保密
65
+ const eph = generateKeyPairSync("x25519");
66
+ const ephPubDer = eph.publicKey.export({ type: "spki", format: "der" });
67
+
68
+ const shared = diffieHellman({ privateKey: eph.privateKey, publicKey: recipientPub });
69
+ const key = deriveKey(shared, ephPubDer, recipientPubDer);
70
+
36
71
  const iv = randomBytes(IV_LEN);
37
- const key = deriveKey(masterKey, salt);
38
72
  const cipher = createCipheriv("aes-256-gcm", key, iv);
39
73
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
40
74
  const tag = cipher.getAuthTag();
41
- return Buffer.concat([MAGIC, Buffer.from([VERSION]), salt, iv, tag, ciphertext]);
75
+
76
+ const epkLen = Buffer.alloc(2);
77
+ epkLen.writeUInt16BE(ephPubDer.length);
78
+
79
+ return Buffer.concat([MAGIC, Buffer.from([VERSION]), epkLen, ephPubDer, iv, tag, ciphertext]);
42
80
  }
43
81
 
44
- export function decrypt(blob, masterKey) {
82
+ export function decrypt(blob, privateKeyB64) {
45
83
  let offset = 0;
46
84
  const magic = blob.subarray(offset, (offset += MAGIC.length));
47
85
  if (!magic.equals(MAGIC)) {
@@ -52,16 +90,25 @@ export function decrypt(blob, masterKey) {
52
90
  if (version !== VERSION) {
53
91
  throw new Error(`不支持的文件版本:${version}`);
54
92
  }
55
- const salt = blob.subarray(offset, (offset += SALT_LEN));
93
+ const epkLen = blob.readUInt16BE(offset);
94
+ offset += 2;
95
+ const ephPubDer = Buffer.from(blob.subarray(offset, (offset += epkLen)));
56
96
  const iv = blob.subarray(offset, (offset += IV_LEN));
57
97
  const tag = blob.subarray(offset, (offset += TAG_LEN));
58
98
  const ciphertext = blob.subarray(offset);
59
- const key = deriveKey(masterKey, salt);
99
+
100
+ const priv = importPrivateKey(privateKeyB64);
101
+ const recipientPubDer = createPublicKey(priv).export({ type: "spki", format: "der" });
102
+ const ephPub = createPublicKey({ key: ephPubDer, format: "der", type: "spki" });
103
+
104
+ const shared = diffieHellman({ privateKey: priv, publicKey: ephPub });
105
+ const key = deriveKey(shared, ephPubDer, recipientPubDer);
106
+
60
107
  const decipher = createDecipheriv("aes-256-gcm", key, iv);
61
108
  decipher.setAuthTag(tag);
62
109
  try {
63
110
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
64
111
  } catch {
65
- throw new Error("解密失败:密钥不正确,或文件已损坏/被篡改");
112
+ throw new Error("解密失败:私钥不匹配,或文件已损坏/被篡改");
66
113
  }
67
114
  }
@@ -0,0 +1,3 @@
1
+ // 内置的接收方公钥(X25519, SPKI/DER 的 base64)。
2
+ // 公钥可公开,仅用于「加密给你」;解密必须用你本地保存的私钥 mossring.priv。
3
+ export const BUILTIN_PUBLIC_KEY = "MCowBQYDK2VuAyEA47OpkVNetZE6UWKuJxH3EV/YbzVR66jV89L6SjODhQs=";