Skip to content

Commit f218077

Browse files
committed
feat: 支持 age 加密解密
1 parent 182adfd commit f218077

27 files changed

Lines changed: 1921 additions & 127 deletions

backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.24.0",
3+
"version": "2.24.1",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"packageManager": "pnpm@11.0.9",
@@ -24,6 +24,7 @@
2424
},
2525
"dependencies": {
2626
"@maxmind/geoip2-node": "^5.0.0",
27+
"age-encryption": "^0.3.0",
2728
"body-parser": "^1.19.0",
2829
"buffer": "^6.0.3",
2930
"connect-history-api-fallback": "^2.0.0",

backend/pnpm-lock.yaml

Lines changed: 67 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/src/products/cron-sync-artifacts.js

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import $ from '@/core/app';
99
import {
1010
markArtifactProducedWithoutUpload,
1111
produceArtifact,
12+
produceSyncArtifactOutput,
1213
shouldUploadArtifact,
1314
uploadArtifactBatches,
1415
} from '@/restful/sync';
@@ -196,17 +197,9 @@ async function doSync(arg = {}, { canUpload = true } = {}) {
196197
`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,
197198
);
198199
}
199-
const output = await produceArtifact({
200-
type: artifact.type,
201-
name: artifact.source,
202-
platform: artifact.platform,
203-
produceOpts: {
204-
'include-unsupported-proxy':
205-
artifact.includeUnsupportedProxy,
206-
useMihomoExternal,
207-
prettyYaml: artifact.prettyYaml,
208-
},
209-
});
200+
const output = await produceSyncArtifactOutput(
201+
artifact,
202+
);
210203

211204
// if (!output || output.length === 0)
212205
// throw new Error('该配置的结果为空 不进行上传');

backend/src/products/sub-store-0.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import registerTokenRoutes from '@/restful/token';
2525
import registerArchiveRoutes from '@/restful/archives';
2626
import registerModuleRoutes from '@/restful/module';
2727
import registerLogRoutes from '@/restful/logs';
28+
import registerAgeRoutes from '@/restful/age';
2829

2930
migrate();
3031
serve();
@@ -44,6 +45,7 @@ function serve() {
4445
registerArchiveRoutes($app);
4546
registerMiscRoutes($app);
4647
registerLogRoutes($app);
48+
registerAgeRoutes($app);
4749

4850
$app.start();
4951
}

backend/src/restful/age-output.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { AGE_PUBLIC_KEY, encryptArmor } from '@/utils/age';
2+
3+
const SHARE_AGE_PUBLIC_KEY_HEADER = 'x-sub-store-share-age-public-key';
4+
5+
function normalizeAgePublicKey(value) {
6+
return String(value ?? '').trim();
7+
}
8+
9+
function createAgePublicKeyConfig(publicKey) {
10+
const key = normalizeAgePublicKey(publicKey);
11+
return key ? { [AGE_PUBLIC_KEY]: key } : undefined;
12+
}
13+
14+
function resolveAgePublicKey(...configs) {
15+
for (const config of configs) {
16+
const key = normalizeAgePublicKey(config?.[AGE_PUBLIC_KEY]);
17+
if (key) return key;
18+
}
19+
20+
return '';
21+
}
22+
23+
function resolveShareAgeConfig({ req, type, name, findShareToken }) {
24+
if (!req?.path?.startsWith('/share/')) {
25+
return undefined;
26+
}
27+
28+
const consumedToken = req.subStoreShareToken;
29+
const token =
30+
consumedToken ||
31+
(req.query?.token && typeof findShareToken === 'function'
32+
? findShareToken({
33+
token: req.query.token,
34+
type,
35+
name,
36+
pathname: req.path,
37+
})
38+
: null);
39+
const headerKey = req.headers?.[SHARE_AGE_PUBLIC_KEY_HEADER];
40+
41+
return createAgePublicKeyConfig(token?.[AGE_PUBLIC_KEY] || headerKey);
42+
}
43+
44+
async function applyAgeOutputEncryption({ body, res, configs = [] }) {
45+
const publicKey = resolveAgePublicKey(...configs);
46+
47+
if (!publicKey) {
48+
return body;
49+
}
50+
51+
const armored = await encryptArmor(body ?? '', publicKey);
52+
if (res?.set) {
53+
res.set('Content-Type', 'text/plain; charset=utf-8');
54+
}
55+
56+
return armored;
57+
}
58+
59+
export {
60+
SHARE_AGE_PUBLIC_KEY_HEADER,
61+
applyAgeOutputEncryption,
62+
createAgePublicKeyConfig,
63+
resolveAgePublicKey,
64+
resolveShareAgeConfig,
65+
};

backend/src/restful/age.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { failed, success } from '@/restful/response';
2+
import {
3+
AGE_KEY_TYPES,
4+
AGE_PUBLIC_KEY,
5+
AGE_SECRET_KEY,
6+
derivePublicKey,
7+
generateKeyPair,
8+
} from '@/utils/age';
9+
10+
export default function register($app) {
11+
$app.post('/api/utils/age/key-pair', createAgeKeyPair);
12+
$app.post('/api/utils/age/public-key', createAgePublicKey);
13+
}
14+
15+
async function createAgeKeyPair(req, res) {
16+
try {
17+
const pair = await generateKeyPair(req.body?.type || AGE_KEY_TYPES.X25519);
18+
success(res, {
19+
type: pair.type,
20+
[AGE_SECRET_KEY]: pair[AGE_SECRET_KEY],
21+
[AGE_PUBLIC_KEY]: pair[AGE_PUBLIC_KEY],
22+
});
23+
} catch (error) {
24+
failed(res, error);
25+
}
26+
}
27+
28+
async function createAgePublicKey(req, res) {
29+
try {
30+
const publicKey = await derivePublicKey(req.body?.[AGE_SECRET_KEY]);
31+
success(res, {
32+
[AGE_PUBLIC_KEY]: publicKey,
33+
});
34+
} catch (error) {
35+
failed(res, error);
36+
}
37+
}

backend/src/restful/artifacts.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
normalizeArtifactCron,
2525
refreshArtifactCronJobs,
2626
} from '@/utils/artifact-cron';
27+
import { normalizeAgePublicKeyConfig } from '@/utils/age';
2728

2829
const ARTIFACT_GIST_PLACEHOLDER_FILENAME = '.sub-store-placeholder';
2930
const ARTIFACT_GIST_PLACEHOLDER_CONTENT = [
@@ -140,6 +141,7 @@ function getAllArtifacts(req, res) {
140141
function replaceArtifact(req, res) {
141142
try {
142143
const allArtifacts = req.body;
144+
allArtifacts.forEach(normalizeAgePublicKeyConfig);
143145
allArtifacts.forEach(normalizeArtifactCron);
144146
$.write(allArtifacts, ARTIFACTS_KEY);
145147
refreshArtifactCronJobs();
@@ -189,6 +191,7 @@ function updateArtifact(req, res) {
189191
...oldArtifact,
190192
...artifact,
191193
};
194+
normalizeAgePublicKeyConfig(newArtifact);
192195
if (!validateArtifactName(newArtifact.name)) {
193196
failed(
194197
res,
@@ -254,6 +257,7 @@ function validateArtifactName(name) {
254257
}
255258

256259
function createArtifactItem(artifact) {
260+
normalizeAgePublicKeyConfig(artifact);
257261
if (!validateArtifactName(artifact.name)) {
258262
throw new RequestInvalidError(
259263
'INVALID_ARTIFACT_NAME',

backend/src/restful/collections.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { failed, success } from '@/restful/response';
1111
import $ from '@/core/app';
1212
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
1313
import { formatDateTime } from '@/utils';
14+
import { normalizeAgePublicKeyConfig } from '@/utils/age';
1415

1516
export default function register($app) {
1617
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
@@ -79,6 +80,7 @@ function updateCollection(req, res) {
7980
...oldCol,
8081
...collection,
8182
};
83+
normalizeAgePublicKeyConfig(newCol);
8284
$.info(`正在更新组合订阅:${name}...`);
8385

8486
if (name !== newCol.name) {
@@ -141,12 +143,18 @@ function getAllCollections(req, res) {
141143
}
142144

143145
function replaceCollection(req, res) {
144-
const allCols = req.body;
145-
$.write(allCols, COLLECTIONS_KEY);
146-
success(res);
146+
try {
147+
const allCols = req.body;
148+
allCols.forEach(normalizeAgePublicKeyConfig);
149+
$.write(allCols, COLLECTIONS_KEY);
150+
success(res);
151+
} catch (error) {
152+
failed(res, error);
153+
}
147154
}
148155

149156
function createCollectionItem(collection) {
157+
normalizeAgePublicKeyConfig(collection);
150158
$.info(`正在创建组合订阅:${collection.name}`);
151159
if (/\//.test(collection.name)) {
152160
throw new RequestInvalidError(

0 commit comments

Comments
 (0)