156 lines
4.3 KiB
TypeScript
156 lines
4.3 KiB
TypeScript
import * as crypto from 'crypto';
|
|
|
|
export const MODULE_PULL_TOKEN_TTL_MS = 15 * 60 * 1000;
|
|
export const MODULE_PULL_TOKEN_USERNAME = 'flnxi-module';
|
|
|
|
export interface ModulePullTokenPayload {
|
|
tokenType: 'flnxi-module-pull';
|
|
projectCode: string;
|
|
licenseId: string;
|
|
activationId: string;
|
|
instanceId: string;
|
|
moduleId: string;
|
|
registryHost: string;
|
|
repository: string;
|
|
tag: string;
|
|
digest: string | null;
|
|
iat: number;
|
|
exp: number;
|
|
jti: string;
|
|
}
|
|
|
|
export interface ModulePullCredential {
|
|
registryHost: string;
|
|
repository: string;
|
|
tag: string;
|
|
digest: string | null;
|
|
username: string;
|
|
pullToken: string;
|
|
expiresAt: string;
|
|
}
|
|
|
|
function base64UrlEncode(value: string): string {
|
|
return Buffer.from(value, 'utf8')
|
|
.toString('base64')
|
|
.replace(/=/g, '')
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_');
|
|
}
|
|
|
|
function base64UrlDecode(value: string): string {
|
|
const padded = value + '='.repeat((4 - (value.length % 4)) % 4);
|
|
return Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
|
|
}
|
|
|
|
function getPullTokenSecret(): string {
|
|
const secret =
|
|
process.env.MODULE_PULL_TOKEN_SECRET ||
|
|
process.env.LICENSE_REQUEST_SIGNING_SECRET ||
|
|
process.env.AES_SECRET_KEY ||
|
|
'';
|
|
|
|
if (!secret || secret.length < 32) {
|
|
throw new Error('MODULE_PULL_TOKEN_SECRET 或 LICENSE_REQUEST_SIGNING_SECRET 未配置或长度不足');
|
|
}
|
|
|
|
return secret;
|
|
}
|
|
|
|
function signPayload(encodedPayload: string): string {
|
|
return crypto
|
|
.createHmac('sha256', getPullTokenSecret())
|
|
.update(encodedPayload)
|
|
.digest('base64')
|
|
.replace(/=/g, '')
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_');
|
|
}
|
|
|
|
function isTimingSafeEqual(left: string, right: string): boolean {
|
|
const leftBuffer = Buffer.from(left);
|
|
const rightBuffer = Buffer.from(right);
|
|
return leftBuffer.length === rightBuffer.length && crypto.timingSafeEqual(leftBuffer, rightBuffer);
|
|
}
|
|
|
|
export function hashModulePullToken(token: string): string {
|
|
return crypto.createHash('sha256').update(token).digest('hex');
|
|
}
|
|
|
|
export function createModulePullCredential(params: {
|
|
projectCode: string;
|
|
licenseId: string;
|
|
activationId: string;
|
|
instanceId: string;
|
|
moduleId: string;
|
|
registryHost: string;
|
|
repository: string;
|
|
tag: string;
|
|
digest: string | null;
|
|
now?: Date;
|
|
}): { credential: ModulePullCredential; payload: ModulePullTokenPayload } {
|
|
const nowMs = params.now?.getTime() ?? Date.now();
|
|
const expiresMs = nowMs + MODULE_PULL_TOKEN_TTL_MS;
|
|
const payload: ModulePullTokenPayload = {
|
|
tokenType: 'flnxi-module-pull',
|
|
projectCode: params.projectCode,
|
|
licenseId: params.licenseId,
|
|
activationId: params.activationId,
|
|
instanceId: params.instanceId,
|
|
moduleId: params.moduleId,
|
|
registryHost: params.registryHost,
|
|
repository: params.repository,
|
|
tag: params.tag,
|
|
digest: params.digest,
|
|
iat: Math.floor(nowMs / 1000),
|
|
exp: Math.floor(expiresMs / 1000),
|
|
jti: crypto.randomBytes(16).toString('hex'),
|
|
};
|
|
|
|
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
const signature = signPayload(encodedPayload);
|
|
const pullToken = `${encodedPayload}.${signature}`;
|
|
|
|
return {
|
|
payload,
|
|
credential: {
|
|
registryHost: params.registryHost,
|
|
repository: params.repository,
|
|
tag: params.tag,
|
|
digest: params.digest,
|
|
username: MODULE_PULL_TOKEN_USERNAME,
|
|
pullToken,
|
|
expiresAt: new Date(expiresMs).toISOString(),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function verifyModulePullToken(token: string, now: Date = new Date()): ModulePullTokenPayload | null {
|
|
const [encodedPayload, signature] = token.split('.');
|
|
if (!encodedPayload || !signature) return null;
|
|
|
|
const expectedSignature = signPayload(encodedPayload);
|
|
if (!isTimingSafeEqual(signature, expectedSignature)) return null;
|
|
|
|
try {
|
|
const payload = JSON.parse(base64UrlDecode(encodedPayload)) as ModulePullTokenPayload;
|
|
if (payload.tokenType !== 'flnxi-module-pull') return null;
|
|
if (
|
|
!payload.projectCode ||
|
|
!payload.licenseId ||
|
|
!payload.activationId ||
|
|
!payload.instanceId ||
|
|
!payload.moduleId ||
|
|
!payload.registryHost ||
|
|
!payload.repository ||
|
|
!payload.tag
|
|
) {
|
|
return null;
|
|
}
|
|
if (payload.exp * 1000 <= now.getTime()) return null;
|
|
|
|
return payload;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|