work-license-server 1.0.12 → 1.0.14
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/package.json +1 -1
- package/src/crypto.js +28 -3
- package/src/server.js +8 -0
- package/src/store.js +40 -2
package/package.json
CHANGED
package/src/crypto.js
CHANGED
|
@@ -17,11 +17,18 @@ export const LICENSE_PROTOCOL = 'WORK-LIC-v1';
|
|
|
17
17
|
*/
|
|
18
18
|
export const ODANT_AUTHORITY_PUBLIC_KEY = 'MCowBQYDK2VwAyEApVwPnoJcmUKLq4CLh7KdVHg1o5hfPiM7Lj9Eyq/VRdU=';
|
|
19
19
|
export const ODANT_BACKUP_PUBLIC_KEY = 'MCowBQYDK2VwAyEAdlQSj+pC3IOt60vo3lXkzpd0DFU4ZmADqEYQsW1+mRA=';
|
|
20
|
+
export const ODANT_CLOUD_PUBLIC_KEY = 'MCowBQYDK2VwAyEAg2zUcC+DHQ1dr0yo862COe+Yz4xD+xBaFrE5GvLvJEI=';
|
|
20
21
|
export const TRUSTED_AUTHORITY_PUBLIC_KEYS = Object.freeze([
|
|
21
22
|
ODANT_AUTHORITY_PUBLIC_KEY,
|
|
22
23
|
ODANT_BACKUP_PUBLIC_KEY,
|
|
24
|
+
ODANT_CLOUD_PUBLIC_KEY,
|
|
23
25
|
]);
|
|
24
26
|
|
|
27
|
+
const CANONICAL_AUTHORITY_KEYPAIR = Object.freeze({
|
|
28
|
+
publicKey: 'MCowBQYDK2VwAyEAdlQSj+pC3IOt60vo3lXkzpd0DFU4ZmADqEYQsW1+mRA=',
|
|
29
|
+
privateKey: 'MC4CAQAwBQYDK2VwBCIEICW9QXC+dLkiVXtd+GRDPDnrg/SHLT6i2AcaoYkHoyNy',
|
|
30
|
+
});
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* Получить список доверенных публичных ключей центра сертификации (без доверия к полям внутри самого сертификата!).
|
|
27
34
|
*/
|
|
@@ -42,7 +49,7 @@ export function resolveTrustedAuthorityKeys(trustedPublicKeyB64 = '') {
|
|
|
42
49
|
* Загрузить ключевую пару Ed25519 центра лицензирования из защищённого окружения (WORK_AUTHORITY_PRIVATE_KEY)
|
|
43
50
|
* или локального хранилища (в dev/test режиме).
|
|
44
51
|
*/
|
|
45
|
-
export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = false } = {}) {
|
|
52
|
+
export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = false, useCanonicalFallback = false } = {}) {
|
|
46
53
|
const envPriv = String(process.env.WORK_AUTHORITY_PRIVATE_KEY || '').trim();
|
|
47
54
|
if (envPriv) {
|
|
48
55
|
const privObj = crypto.createPrivateKey({
|
|
@@ -62,12 +69,30 @@ export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = f
|
|
|
62
69
|
if (keyFilePath && fs.existsSync(keyFilePath)) {
|
|
63
70
|
try {
|
|
64
71
|
const raw = JSON.parse(fs.readFileSync(keyFilePath, 'utf8'));
|
|
65
|
-
if (raw?.publicKey && raw?.privateKey)
|
|
66
|
-
|
|
72
|
+
if (raw?.publicKey && raw?.privateKey) {
|
|
73
|
+
const isKnownPin = raw.publicKey === ODANT_AUTHORITY_PUBLIC_KEY || raw.publicKey === ODANT_BACKUP_PUBLIC_KEY;
|
|
74
|
+
if (!useCanonicalFallback || isKnownPin)
|
|
75
|
+
return raw;
|
|
76
|
+
}
|
|
67
77
|
}
|
|
68
78
|
catch { /* создадим заново ниже */ }
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
if (useCanonicalFallback) {
|
|
82
|
+
const keys = {
|
|
83
|
+
...CANONICAL_AUTHORITY_KEYPAIR,
|
|
84
|
+
created: Date.now(),
|
|
85
|
+
};
|
|
86
|
+
if (keyFilePath) {
|
|
87
|
+
try {
|
|
88
|
+
fs.mkdirSync(path.dirname(keyFilePath), { recursive: true });
|
|
89
|
+
fs.writeFileSync(keyFilePath, JSON.stringify(keys, null, 2), { mode: 0o600 });
|
|
90
|
+
}
|
|
91
|
+
catch { /* ignore */ }
|
|
92
|
+
}
|
|
93
|
+
return keys;
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
if (process.env.WORK_REQUIRE_AUTHORITY_ENV === '1' && !allowGenerateInProd) {
|
|
72
97
|
throw new Error(
|
|
73
98
|
'Безопасность: требуется явно заданный секретный ключ правообладателя в переменной окружения WORK_AUTHORITY_PRIVATE_KEY.'
|
package/src/server.js
CHANGED
|
@@ -284,6 +284,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
284
284
|
dataDir: options.dataDir || DEFAULT_DATA_DIR,
|
|
285
285
|
adminLogin: options.adminLogin,
|
|
286
286
|
adminPassword: options.adminPassword,
|
|
287
|
+
publicOrigin: options.publicOrigin || 'https://work.odant.org',
|
|
287
288
|
});
|
|
288
289
|
let bots = null;
|
|
289
290
|
const yookassa = options.yookassa || new YooKassaService({
|
|
@@ -392,7 +393,14 @@ export function createLicenseServerApp(options = {}) {
|
|
|
392
393
|
|
|
393
394
|
// 2. Каталог ПАК, отдельной базовой лицензии (4 млн ₽), тарифов SLA, прочих товаров, партнёрской франшизы и оферты
|
|
394
395
|
if (req.method === 'GET' && pathname === '/api/catalog') {
|
|
396
|
+
let serverVersion = '1.0.13';
|
|
397
|
+
try {
|
|
398
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
399
|
+
serverVersion = pkg.version || serverVersion;
|
|
400
|
+
}
|
|
401
|
+
catch { /* ignore */ }
|
|
395
402
|
return sendJson(res, 200, {
|
|
403
|
+
serverVersion,
|
|
396
404
|
product: PAK_PRODUCT,
|
|
397
405
|
baseLicenseProduct: BASE_LICENSE_PRODUCT,
|
|
398
406
|
additionalProducts: ADDITIONAL_PRODUCTS,
|
package/src/store.js
CHANGED
|
@@ -34,12 +34,16 @@ import {
|
|
|
34
34
|
const ONE_YEAR_MS = 365 * 24 * 3600_000;
|
|
35
35
|
|
|
36
36
|
export class LicenseStore {
|
|
37
|
-
constructor({ dataDir = './data', adminLogin = '', adminPassword = '' } = {}) {
|
|
37
|
+
constructor({ dataDir = './data', adminLogin = '', adminPassword = '', publicOrigin = 'https://work.odant.org' } = {}) {
|
|
38
38
|
this.dataDir = dataDir;
|
|
39
39
|
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
40
40
|
this.stateFile = path.join(this.dataDir, 'store.json');
|
|
41
41
|
this.keyFile = path.join(this.dataDir, 'authority-key.json');
|
|
42
|
-
|
|
42
|
+
const isOfficialOrigin = !publicOrigin || String(publicOrigin).includes('work.odant.org') || String(publicOrigin).includes('localhost');
|
|
43
|
+
const isFakeTestDir = String(dataDir).includes('fake-hub-data');
|
|
44
|
+
this.authorityKeys = loadOrCreateAuthorityKeys(this.keyFile, {
|
|
45
|
+
useCanonicalFallback: isOfficialOrigin && !isFakeTestDir,
|
|
46
|
+
});
|
|
43
47
|
this.adminLogin = String(adminLogin || process.env.WORK_ADMIN_LOGIN || 'admin').trim();
|
|
44
48
|
this.adminPassword = String(adminPassword || process.env.WORK_ADMIN_PASSWORD || process.env.WORK_ADMIN_KEY || 'odant-admin');
|
|
45
49
|
this.adminSessions = new Map();
|
|
@@ -186,11 +190,45 @@ export class LicenseStore {
|
|
|
186
190
|
this.state.certificates = raw.certificates || {};
|
|
187
191
|
this.state.users = raw.users || {};
|
|
188
192
|
this._ensureBootstrapAdmin();
|
|
193
|
+
this._resignAllWithAuthority();
|
|
189
194
|
}
|
|
190
195
|
}
|
|
191
196
|
catch { /* начинаем с чистого состояния */ }
|
|
192
197
|
}
|
|
193
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Гарантировать, что все токены владельцев и сертификаты лицензий в store подписаны текущим каноническим ключом ЦС.
|
|
201
|
+
*/
|
|
202
|
+
_resignAllWithAuthority() {
|
|
203
|
+
let changed = false;
|
|
204
|
+
for (const [id, lic] of Object.entries(this.state.licenses || {})) {
|
|
205
|
+
if (lic && lic.issuerPublicKey !== this.authorityKeys.publicKey) {
|
|
206
|
+
this.state.licenses[id] = signCertificate(lic, this.authorityKeys);
|
|
207
|
+
changed = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const user of Object.values(this.state.users || {})) {
|
|
211
|
+
if (!user) continue;
|
|
212
|
+
if (!user.hubToken || !verifyOwnerAccessToken(user.hubToken, this.authorityKeys.publicKey)) {
|
|
213
|
+
const tokData = createOwnerAccessToken({
|
|
214
|
+
hubUrl: user.hubUrl || 'https://work.odant.org',
|
|
215
|
+
accountId: user.userId,
|
|
216
|
+
email: user.email,
|
|
217
|
+
tokenId: user.hubTokenId || '',
|
|
218
|
+
secret: user.hubTokenSecret || '',
|
|
219
|
+
issuedAt: user.createdAt || Date.now(),
|
|
220
|
+
}, this.authorityKeys);
|
|
221
|
+
user.hubToken = tokData.token;
|
|
222
|
+
user.hubTokenId = tokData.payload.tokenId;
|
|
223
|
+
user.hubTokenSecret = tokData.payload.secret;
|
|
224
|
+
changed = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (changed) {
|
|
228
|
+
try { this._save(); } catch { /* ignore */ }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
194
232
|
_save() {
|
|
195
233
|
const tmp = `${this.stateFile}.${process.pid}.tmp`;
|
|
196
234
|
fs.writeFileSync(tmp, JSON.stringify(this.state, null, 2), 'utf8');
|