nx-sk 0.6.1 → 0.6.2
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/modules/backup/service.js +47 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
本文件格式参考 [Keep a Changelog](https://keepachangelog.com/),中文正文。
|
|
4
4
|
|
|
5
|
+
## [0.6.2] - 2026-10-12
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **加密备份:跨机器恢复后 store.json 里 secret 字段仍是密文**。0.6.1 把 secret 字段解开成明文后直接落回 store.json——丢了本机「落盘密文」防护。回归到 0.5.1 的设计:导出时用本机 vaultKey **解开** secret 字段、整包口令加密入 `.nxskbak`;导入时解开整包后**用本机 vaultKey 重新加密** secret 字段再写回 store.json。两条安全边界各自独立:本地靠 vaultKey、传递靠口令。
|
|
10
|
+
|
|
5
11
|
## [0.6.1] - 2026-10-12
|
|
6
12
|
|
|
7
13
|
### Changed
|
package/package.json
CHANGED
|
@@ -39,8 +39,8 @@ function passwordFrom(ctx) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* 导出时:用本机 vaultKey **解开** secret 栏目的密文字段。
|
|
43
|
+
* 导出文件不再依赖 A 机的 ~/.vaultkey / NX_SK_PASSPHRASE。
|
|
44
44
|
*/
|
|
45
45
|
async function unsealSecretsForExport(payload) {
|
|
46
46
|
const sections = payload.sections || [];
|
|
@@ -66,6 +66,38 @@ async function unsealSecretsForExport(payload) {
|
|
|
66
66
|
return { ...payload, entries: newEntries, _unsealed: unsealed };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* 导入时:payload 里的 secret 字段已是明文,**用本机 vaultKey 重新加密**后落盘。
|
|
71
|
+
* 这样 store.json 里仍是密文(落盘防护不丢),B 机后续 key get 走 B 机 vaultKey 解密。
|
|
72
|
+
* 历史 store 里残留的明文 secret 也会被本步统一收紧成密文。
|
|
73
|
+
*/
|
|
74
|
+
async function resealSecretsForImport(payload) {
|
|
75
|
+
const sections = payload.sections || [];
|
|
76
|
+
const entries = payload.entries || [];
|
|
77
|
+
const { key: vaultKey } = await resolveVaultKey();
|
|
78
|
+
|
|
79
|
+
const newEntries = [];
|
|
80
|
+
let resealed = 0;
|
|
81
|
+
for (const e of entries) {
|
|
82
|
+
const sec = sections.find((s) => s.id === e.section);
|
|
83
|
+
if (!sec || !e.values) { newEntries.push(e); continue; }
|
|
84
|
+
const nextValues = { ...e.values };
|
|
85
|
+
let touched = false;
|
|
86
|
+
for (const def of sec.fields || []) {
|
|
87
|
+
if (!isSensitiveField(sec, def.key)) continue;
|
|
88
|
+
const v = nextValues[def.key];
|
|
89
|
+
// 已是本机 vaultKey 加密形态就跳过;明文或旧 kid 都重加密
|
|
90
|
+
if (isCipherBlob(v) && !v.kid) continue;
|
|
91
|
+
const plain = isCipherBlob(v) ? decryptValue(vaultKey, v) : (typeof v === 'string' ? v : null);
|
|
92
|
+
if (plain === null) continue;
|
|
93
|
+
nextValues[def.key] = encryptValue(vaultKey, plain);
|
|
94
|
+
touched = true; resealed++;
|
|
95
|
+
}
|
|
96
|
+
newEntries.push(touched ? { ...e, values: nextValues } : e);
|
|
97
|
+
}
|
|
98
|
+
return { ...payload, entries: newEntries, _resealed: resealed };
|
|
99
|
+
}
|
|
100
|
+
|
|
69
101
|
/** 生成加密备份:返回归档对象(HTTP 端据此让浏览器下载)与条目计数。 */
|
|
70
102
|
export async function buildBackup(ctx) {
|
|
71
103
|
const password = passwordFrom(ctx);
|
|
@@ -156,7 +188,7 @@ export function decryptBackup(text, password) {
|
|
|
156
188
|
return { payload, createdAt: archive.createdAt };
|
|
157
189
|
}
|
|
158
190
|
|
|
159
|
-
/**
|
|
191
|
+
/** 恢复:解密整包 → 把 secret 明文用本机 vaultKey 重新加密 → 留安全快照 → 整份替换。 */
|
|
160
192
|
export async function restoreBackup({ content, password, 'dry-run': dryRun }) {
|
|
161
193
|
const { payload, createdAt } = decryptBackup(String(content ?? ''), password);
|
|
162
194
|
|
|
@@ -170,23 +202,26 @@ export async function restoreBackup({ content, password, 'dry-run': dryRun }) {
|
|
|
170
202
|
};
|
|
171
203
|
}
|
|
172
204
|
|
|
205
|
+
// 把 payload 里的 secret 明文用**本机 vaultKey** 重新加密——落回 store.json 仍是密文形态。
|
|
206
|
+
const resealed = await resealSecretsForImport(payload);
|
|
207
|
+
|
|
173
208
|
// 覆盖前留一份可回滚快照(落到 backup/ 目录),避免误操作丢数据
|
|
174
209
|
const safety = await snapshotStore('before-restore');
|
|
175
210
|
|
|
176
211
|
await saveStore({
|
|
177
|
-
version:
|
|
178
|
-
settings:
|
|
179
|
-
sections:
|
|
180
|
-
entries:
|
|
181
|
-
exports: Array.isArray(
|
|
212
|
+
version: resealed.version,
|
|
213
|
+
settings: resealed.settings,
|
|
214
|
+
sections: resealed.sections,
|
|
215
|
+
entries: resealed.entries,
|
|
216
|
+
exports: Array.isArray(resealed.exports) ? resealed.exports : [],
|
|
182
217
|
});
|
|
183
218
|
|
|
184
219
|
return {
|
|
185
220
|
status: 'restored',
|
|
186
221
|
backupCreatedAt: createdAt || '',
|
|
187
|
-
sections:
|
|
188
|
-
entries:
|
|
189
|
-
|
|
222
|
+
sections: resealed.sections.length,
|
|
223
|
+
entries: resealed.entries.length,
|
|
224
|
+
resealed: resealed._resealed || 0,
|
|
190
225
|
safetySnapshot: displayPath(safety),
|
|
191
226
|
};
|
|
192
227
|
}
|
|
@@ -226,7 +261,7 @@ export function renderRestore(d) {
|
|
|
226
261
|
}
|
|
227
262
|
return [
|
|
228
263
|
`已从加密备份恢复:${d.sections} 个栏目 / ${d.entries} 条条目`,
|
|
229
|
-
|
|
264
|
+
`用本机密钥库重新加密的密钥字段: ${d.resealed || 0} 个`,
|
|
230
265
|
`备份生成于: ${d.backupCreatedAt || '未知'}`,
|
|
231
266
|
`恢复前的当前数据已留安全快照: ${d.safetySnapshot}`,
|
|
232
267
|
].join('\n');
|