32a5479086
Use OS machine identifiers (Windows MachineGuid, macOS IOPlatformUUID, Linux machine-id) hashed as deviceId so all accounts on one PC share one license slot. Keep legacy userData/device.id for migration and retire it on re-activation. Co-authored-by: Cursor <cursoragent@cursor.com>
71 lines
2.5 KiB
TypeScript
71 lines
2.5 KiB
TypeScript
import { createPublicKey, verify as cryptoVerify } from 'node:crypto';
|
|
|
|
import { LICENSE_ED25519_SPKI_DER_B64 } from '../../shared/license/bundledPublicKey';
|
|
import { canonicalJson } from '../../shared/license/canonicalJson';
|
|
import { isLicensePayloadV1, type LicensePayloadV1 } from '../../shared/license/payloadV1';
|
|
import { splitSignedLicenseToken } from '../../shared/license/tokenFormat';
|
|
import type { LicenseVerifyFailure } from '../../shared/license/verifyReasons';
|
|
|
|
export type LicenseVerifyResult =
|
|
| { ok: true; payload: LicensePayloadV1 }
|
|
| { ok: false; reason: LicenseVerifyFailure };
|
|
|
|
let cachedPublicKey: ReturnType<typeof createPublicKey> | null = null;
|
|
|
|
function getBundledPublicKey() {
|
|
cachedPublicKey ??= createPublicKey({
|
|
key: Buffer.from(LICENSE_ED25519_SPKI_DER_B64, 'base64'),
|
|
format: 'der',
|
|
type: 'spki',
|
|
});
|
|
return cachedPublicKey;
|
|
}
|
|
|
|
export function verifyLicenseToken(
|
|
token: string,
|
|
opts: {
|
|
nowSec: number;
|
|
deviceId: string;
|
|
/** Старые deviceId (например UUID из userData) — принимаются до повторной активации. */
|
|
alsoAcceptDeviceIds?: readonly string[];
|
|
publicKeyOverrideSpkiDerB64?: string;
|
|
},
|
|
): LicenseVerifyResult {
|
|
const parts = splitSignedLicenseToken(token);
|
|
if (!parts) return { ok: false, reason: 'malformed' };
|
|
|
|
let payload: unknown;
|
|
try {
|
|
payload = JSON.parse(parts.bodyUtf8) as unknown;
|
|
} catch {
|
|
return { ok: false, reason: 'bad_payload' };
|
|
}
|
|
if (!isLicensePayloadV1(payload)) return { ok: false, reason: 'bad_payload' };
|
|
|
|
const body = canonicalJson(payload);
|
|
const msg = Buffer.from(body, 'utf8');
|
|
const publicKey =
|
|
opts.publicKeyOverrideSpkiDerB64 !== undefined
|
|
? createPublicKey({
|
|
key: Buffer.from(opts.publicKeyOverrideSpkiDerB64, 'base64'),
|
|
format: 'der',
|
|
type: 'spki',
|
|
})
|
|
: getBundledPublicKey();
|
|
const okSig = cryptoVerify(null, msg, publicKey, Buffer.from(parts.signature));
|
|
if (!okSig) return { ok: false, reason: 'bad_signature' };
|
|
|
|
if (payload.nbf !== undefined && opts.nowSec < payload.nbf) {
|
|
return { ok: false, reason: 'not_yet_valid' };
|
|
}
|
|
if (opts.nowSec >= payload.exp) return { ok: false, reason: 'expired' };
|
|
if (payload.did !== null) {
|
|
const accepted = new Set<string>([opts.deviceId, ...(opts.alsoAcceptDeviceIds ?? [])]);
|
|
if (!accepted.has(payload.did)) {
|
|
return { ok: false, reason: 'wrong_device' };
|
|
}
|
|
}
|
|
|
|
return { ok: true, payload };
|
|
}
|