feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
+36
View File
@@ -0,0 +1,36 @@
{
"name": "@qinglong/local-secret",
"version": "3.0.0-alpha.0",
"private": true,
"description": "QingLong 3.0 encrypted local SecretStore and private keyring",
"license": "Apache-2.0",
"engines": {
"node": ">=24.18.0 <25"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist/**/*.js",
"dist/**/*.d.ts"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"check": "node ../../scripts/ql3-build-package-closure.cjs && tsc -p tsconfig.json --noEmit",
"test": "node ../../scripts/ql3-build-package-closure.cjs && node --test test/*.test.cjs"
},
"dependencies": {
"@qinglong/runtime-core": "workspace:*"
},
"devDependencies": {
"@qinglong/local-sqlite": "workspace:*",
"@types/node": "24.13.3",
"typescript": "5.9.3"
}
}
+16
View File
@@ -0,0 +1,16 @@
export * from './secret-custody/crypto';
export * from './secret-custody/keyring';
export * from './secret-custody/keyMaterial';
export * from './secret-custody/service';
export type {
AppendLocalSecretEnvelopeCommand,
AppendLocalSecretEnvelopeResult,
LocalSecretEnvelope,
LocalSecretEnvelopeRepository,
LocalSecretEnvironmentProvider,
LocalSecretKeyMaterial,
LocalSecretKeyProvider,
LocalSecretReference,
PutEncryptedLocalSecretCommand,
PutEncryptedLocalSecretResult,
} from '@qinglong/runtime-core/local-secret';
@@ -0,0 +1,112 @@
import {
createCipheriv,
createDecipheriv,
randomBytes,
} from 'node:crypto';
import { TextDecoder } from 'node:util';
import {
LOCAL_SECRET_ALGORITHM,
LocalSecretUnavailableError,
assertLocalSecretPlaintext,
localSecretBinary,
localSecretEnvelopeAad,
normalizeLocalSecretEnvelope,
type LocalSecretEnvelope,
} from '@qinglong/runtime-core/local-secret';
const SECRET_KEY_BYTES = 32;
const SECRET_NONCE_BYTES = 12;
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
export type LocalSecretNonceFactory = () => Uint8Array;
function ownedSecretKey(key: Uint8Array): Buffer {
if (!(key instanceof Uint8Array) || key.byteLength !== SECRET_KEY_BYTES) {
throw new LocalSecretUnavailableError();
}
return Buffer.from(key);
}
export function encryptLocalSecretEnvelope(
metadata: Omit<LocalSecretEnvelope, 'nonce' | 'ciphertext' | 'authTag'>,
plaintext: string,
key: Uint8Array,
nonceFactory: LocalSecretNonceFactory = () => randomBytes(SECRET_NONCE_BYTES),
): LocalSecretEnvelope {
assertLocalSecretPlaintext(plaintext);
const ownedKey = ownedSecretKey(key);
const plaintextBuffer = Buffer.from(plaintext, 'utf8');
let nonce: Buffer | undefined;
try {
nonce = Buffer.from(nonceFactory());
if (nonce.length !== SECRET_NONCE_BYTES) {
throw new LocalSecretUnavailableError();
}
const cipher = createCipheriv(LOCAL_SECRET_ALGORITHM, ownedKey, nonce, {
authTagLength: 16,
});
const aad = localSecretEnvelopeAad(metadata);
try {
cipher.setAAD(aad);
} finally {
aad.fill(0);
}
const ciphertext = Buffer.concat([
cipher.update(plaintextBuffer),
cipher.final(),
]);
try {
return normalizeLocalSecretEnvelope({
...metadata,
nonce: nonce.toString('base64url'),
ciphertext: ciphertext.toString('base64url'),
authTag: cipher.getAuthTag().toString('base64url'),
});
} finally {
ciphertext.fill(0);
}
} catch (error) {
if (error instanceof LocalSecretUnavailableError) throw error;
throw new LocalSecretUnavailableError();
} finally {
ownedKey.fill(0);
plaintextBuffer.fill(0);
nonce?.fill(0);
}
}
export function decryptLocalSecretEnvelopeToBuffer(
envelope: LocalSecretEnvelope,
key: Uint8Array,
): Buffer {
const normalized = normalizeLocalSecretEnvelope(envelope);
const ownedKey = ownedSecretKey(key);
const nonce = localSecretBinary('nonce', normalized.nonce);
const ciphertext = localSecretBinary('ciphertext', normalized.ciphertext);
const authTag = localSecretBinary('authTag', normalized.authTag);
const aad = localSecretEnvelopeAad(normalized);
try {
const decipher = createDecipheriv(LOCAL_SECRET_ALGORITHM, ownedKey, nonce, {
authTagLength: 16,
});
decipher.setAAD(aad);
decipher.setAuthTag(authTag);
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
} catch {
throw new LocalSecretUnavailableError();
} finally {
ownedKey.fill(0);
nonce.fill(0);
ciphertext.fill(0);
authTag.fill(0);
aad.fill(0);
}
}
export function decodeLocalSecretPlaintext(plaintext: Buffer): string {
try {
return UTF8_DECODER.decode(plaintext);
} catch {
throw new LocalSecretUnavailableError();
}
}
@@ -0,0 +1,46 @@
import { timingSafeEqual } from 'node:crypto';
import {
LocalSecretUnavailableError,
assertLocalSecretKeyId,
type LocalSecretEnvelope,
type LocalSecretKeyMaterial,
} from '@qinglong/runtime-core/local-secret';
import { decryptLocalSecretEnvelopeToBuffer } from './crypto';
export function ownedLocalSecretKeyMaterial(
material: LocalSecretKeyMaterial | null,
expectedKeyId?: string,
): { keyId: string; key: Buffer } {
if (!material || !(material.key instanceof Uint8Array)) {
throw new LocalSecretUnavailableError();
}
try {
assertLocalSecretKeyId(material.keyId);
if (
(expectedKeyId !== undefined && material.keyId !== expectedKeyId) ||
material.key.byteLength !== 32
) {
throw new LocalSecretUnavailableError();
}
return { keyId: material.keyId, key: Buffer.from(material.key) };
} catch {
throw new LocalSecretUnavailableError();
} finally {
material.key.fill(0);
}
}
export function localSecretPlaintextMatches(
envelope: LocalSecretEnvelope,
key: Uint8Array,
expected: string,
): boolean {
const actual = decryptLocalSecretEnvelopeToBuffer(envelope, key);
const wanted = Buffer.from(expected, 'utf8');
try {
return actual.length === wanted.length && timingSafeEqual(actual, wanted);
} finally {
actual.fill(0);
wanted.fill(0);
}
}
@@ -0,0 +1,362 @@
import { randomBytes, createHash } from 'node:crypto';
import { constants } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
import {
LocalSecretUnavailableError,
assertLocalSecretKeyId,
type LocalSecretKeyMaterial,
type LocalSecretKeyProvider,
} from '@qinglong/runtime-core/local-secret';
const MAX_KEYRING_BYTES = 16 * 1024;
const MAX_KEY_COUNT = 16;
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
interface LocalSecretKeyringManifest {
readonly version: 1;
readonly activeKeyId: string;
readonly keys: Readonly<Record<string, string>>;
}
export interface LocalSecretKeyringSummary {
readonly version: 1;
readonly activeKeyId: string;
readonly keyIds: readonly string[];
readonly digest: string;
}
export class LocalSecretKeyringConflictError extends Error {
readonly code = 'LOCAL_SECRET_KEYRING_CONFLICT';
constructor() {
super('Local Secret keyring state changed');
this.name = 'LocalSecretKeyringConflictError';
}
}
function exactKeys(value: object, expected: readonly string[]): boolean {
const actual = Object.keys(value).sort();
const sortedExpected = [...expected].sort();
return (
actual.length === sortedExpected.length &&
actual.every((key, index) => key === sortedExpected[index])
);
}
function assertKeyringPath(filePath: unknown): asserts filePath is string {
if (
typeof filePath !== 'string' ||
!path.isAbsolute(filePath) ||
filePath.includes('\0') ||
Buffer.byteLength(filePath, 'utf8') > 4096
) {
throw new TypeError('Local Secret keyring path must be absolute');
}
}
function parseManifest(value: Buffer): LocalSecretKeyringManifest {
let parsed: unknown;
try {
parsed = JSON.parse(value.toString('utf8'));
} catch {
throw new LocalSecretUnavailableError();
}
if (
!parsed ||
typeof parsed !== 'object' ||
Array.isArray(parsed) ||
!exactKeys(parsed, ['activeKeyId', 'keys', 'version'])
) {
throw new LocalSecretUnavailableError();
}
const record = parsed as Record<string, unknown>;
if (
record.version !== 1 ||
!record.keys ||
typeof record.keys !== 'object' ||
Array.isArray(record.keys)
) {
throw new LocalSecretUnavailableError();
}
try {
assertLocalSecretKeyId(record.activeKeyId);
} catch {
throw new LocalSecretUnavailableError();
}
const entries = Object.entries(record.keys as Record<string, unknown>);
if (entries.length < 1 || entries.length > MAX_KEY_COUNT) {
throw new LocalSecretUnavailableError();
}
const keys: Record<string, string> = Object.create(null);
for (const [keyId, encoded] of entries) {
let decoded: Buffer | undefined;
try {
assertLocalSecretKeyId(keyId);
decoded =
typeof encoded === 'string'
? Buffer.from(encoded, 'base64url')
: Buffer.alloc(0);
if (
typeof encoded !== 'string' ||
!BASE64URL_PATTERN.test(encoded) ||
decoded.length !== 32 ||
decoded.toString('base64url') !== encoded
) {
throw new LocalSecretUnavailableError();
}
keys[keyId] = encoded;
} catch {
throw new LocalSecretUnavailableError();
} finally {
decoded?.fill(0);
}
}
if (!keys[record.activeKeyId]) {
throw new LocalSecretUnavailableError();
}
return Object.freeze({
version: 1,
activeKeyId: record.activeKeyId,
keys: Object.freeze(keys),
});
}
function canonicalManifest(manifest: LocalSecretKeyringManifest): Buffer {
const keys = Object.fromEntries(
Object.entries(manifest.keys).sort(([left], [right]) =>
left.localeCompare(right),
),
);
return Buffer.from(
`${JSON.stringify({
version: 1,
activeKeyId: manifest.activeKeyId,
keys,
})}\n`,
'utf8',
);
}
function summary(manifest: LocalSecretKeyringManifest): LocalSecretKeyringSummary {
const canonical = canonicalManifest(manifest);
try {
return Object.freeze({
version: 1,
activeKeyId: manifest.activeKeyId,
keyIds: Object.freeze(Object.keys(manifest.keys).sort()),
digest: createHash('sha256').update(canonical).digest('hex'),
});
} finally {
canonical.fill(0);
}
}
async function assertRealParent(filePath: string): Promise<void> {
const stat = await fs.lstat(path.dirname(filePath));
if (!stat.isDirectory() || stat.isSymbolicLink()) {
throw new LocalSecretUnavailableError();
}
}
async function readManifest(filePath: string): Promise<LocalSecretKeyringManifest> {
let file: fs.FileHandle | undefined;
let contents: Buffer | undefined;
try {
file = await fs.open(
filePath,
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
);
const stat = await file.stat();
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
(stat.mode & 0o077) !== 0 ||
stat.size < 1 ||
stat.size > MAX_KEYRING_BYTES
) {
throw new LocalSecretUnavailableError();
}
contents = await file.readFile();
if (contents.length !== stat.size) {
throw new LocalSecretUnavailableError();
}
return parseManifest(contents);
} catch {
throw new LocalSecretUnavailableError();
} finally {
contents?.fill(0);
await file?.close().catch(() => undefined);
}
}
async function syncDirectory(directory: string): Promise<void> {
const handle = await fs.open(directory, constants.O_RDONLY);
try {
await handle.sync();
} finally {
await handle.close();
}
}
function newKeyId(): string {
return `qlsk-${randomBytes(12).toString('base64url')}`;
}
async function writeTemporary(
filePath: string,
contents: Buffer,
): Promise<string> {
const temporary = `${filePath}.tmp-${randomBytes(12).toString('hex')}`;
const handle = await fs.open(
temporary,
constants.O_CREAT |
constants.O_EXCL |
constants.O_WRONLY |
(constants.O_NOFOLLOW ?? 0),
0o600,
);
try {
await handle.writeFile(contents);
await handle.sync();
} finally {
await handle.close();
}
return temporary;
}
export class LocalSecretKeyringFileProvider implements LocalSecretKeyProvider {
private readonly filePath: string;
constructor(filePath: string) {
assertKeyringPath(filePath);
this.filePath = path.resolve(filePath);
}
async active(): Promise<LocalSecretKeyMaterial> {
const manifest = await readManifest(this.filePath);
return this.material(manifest, manifest.activeKeyId)!;
}
async resolve(keyId: string): Promise<LocalSecretKeyMaterial | null> {
try {
assertLocalSecretKeyId(keyId);
} catch {
throw new LocalSecretUnavailableError();
}
return this.material(await readManifest(this.filePath), keyId);
}
async inspect(): Promise<LocalSecretKeyringSummary> {
return summary(await readManifest(this.filePath));
}
private material(
manifest: LocalSecretKeyringManifest,
keyId: string,
): LocalSecretKeyMaterial | null {
const encoded = manifest.keys[keyId];
return encoded
? Object.freeze({
keyId,
key: Uint8Array.from(Buffer.from(encoded, 'base64url')),
})
: null;
}
}
export async function provisionLocalSecretKeyring(
filePath: string,
): Promise<LocalSecretKeyringSummary> {
assertKeyringPath(filePath);
const resolved = path.resolve(filePath);
await assertRealParent(resolved);
const keyId = newKeyId();
const key = randomBytes(32);
const manifest: LocalSecretKeyringManifest = Object.freeze({
version: 1,
activeKeyId: keyId,
keys: Object.freeze({ [keyId]: key.toString('base64url') }),
});
const contents = canonicalManifest(manifest);
let temporary: string | undefined;
try {
temporary = await writeTemporary(resolved, contents);
await fs.link(temporary, resolved);
await fs.unlink(temporary);
temporary = undefined;
await syncDirectory(path.dirname(resolved));
return summary(manifest);
} catch {
throw new LocalSecretUnavailableError();
} finally {
key.fill(0);
contents.fill(0);
if (temporary) await fs.unlink(temporary).catch(() => undefined);
}
}
export async function rotateLocalSecretKeyring(options: {
readonly filePath: string;
readonly expectedActiveKeyId: string;
}): Promise<LocalSecretKeyringSummary> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('Local Secret keyring rotation options are invalid');
}
assertKeyringPath(options.filePath);
try {
assertLocalSecretKeyId(options.expectedActiveKeyId);
} catch {
throw new TypeError('Local Secret expected active key is invalid');
}
const resolved = path.resolve(options.filePath);
await assertRealParent(resolved);
const lockPath = `${resolved}.lock`;
let lock: fs.FileHandle | undefined;
let temporary: string | undefined;
let key: Buffer | undefined;
let contents: Buffer | undefined;
try {
lock = await fs.open(
lockPath,
constants.O_CREAT |
constants.O_EXCL |
constants.O_WRONLY |
(constants.O_NOFOLLOW ?? 0),
0o600,
);
await lock.sync();
const current = await readManifest(resolved);
if (current.activeKeyId !== options.expectedActiveKeyId) {
throw new LocalSecretKeyringConflictError();
}
if (Object.keys(current.keys).length >= MAX_KEY_COUNT) {
throw new LocalSecretUnavailableError();
}
const keyId = newKeyId();
key = randomBytes(32);
const next: LocalSecretKeyringManifest = Object.freeze({
version: 1,
activeKeyId: keyId,
keys: Object.freeze({
...current.keys,
[keyId]: key.toString('base64url'),
}),
});
contents = canonicalManifest(next);
temporary = await writeTemporary(resolved, contents);
await fs.rename(temporary, resolved);
temporary = undefined;
await syncDirectory(path.dirname(resolved));
return summary(next);
} catch (error) {
if (error instanceof LocalSecretKeyringConflictError) throw error;
throw new LocalSecretUnavailableError();
} finally {
key?.fill(0);
contents?.fill(0);
if (temporary) await fs.unlink(temporary).catch(() => undefined);
await lock?.close().catch(() => undefined);
if (lock) await fs.unlink(lockPath).catch(() => undefined);
}
}
@@ -0,0 +1,321 @@
import { normalizeLocalDispatchCandidate } from '@qinglong/runtime-core/local-dispatch';
import {
LOCAL_SECRET_ALGORITHM,
MAX_LOCAL_SECRET_BATCH_SIZE,
LocalSecretMutationConflictError,
LocalSecretUnavailableError,
LocalSecretVersionConflictError,
assertLocalSecretExpectedVersion,
assertLocalSecretMutationId,
assertLocalSecretName,
assertLocalSecretPlaintext,
assertLocalSecretProjectId,
createLocalSecretRef,
parseLocalSecretRef,
type LocalSecretEnvelope,
type LocalSecretEnvelopeRepository,
type LocalSecretEnvironmentProvider,
type LocalSecretKeyProvider,
type PutEncryptedLocalSecretCommand,
type PutEncryptedLocalSecretResult,
} from '@qinglong/runtime-core/local-secret';
import {
decodeLocalSecretPlaintext,
decryptLocalSecretEnvelopeToBuffer,
encryptLocalSecretEnvelope,
type LocalSecretNonceFactory,
} from './crypto';
import {
localSecretPlaintextMatches,
ownedLocalSecretKeyMaterial,
} from './keyMaterial';
function exactKeys(value: object, expected: readonly string[]): boolean {
const actual = Object.keys(value).sort();
const sortedExpected = [...expected].sort();
return (
actual.length === sortedExpected.length &&
actual.every((key, index) => key === sortedExpected[index])
);
}
function assertPutCommand(command: PutEncryptedLocalSecretCommand): void {
if (
!command ||
typeof command !== 'object' ||
Array.isArray(command) ||
!exactKeys(command, [
'projectId',
'name',
'plaintext',
'mutationId',
'expectedCurrentVersion',
'createdAtMs',
])
) {
throw new TypeError('Local Secret write command is invalid');
}
assertLocalSecretProjectId(command.projectId);
assertLocalSecretName(command.name);
assertLocalSecretPlaintext(command.plaintext);
assertLocalSecretMutationId(command.mutationId);
assertLocalSecretExpectedVersion(command.expectedCurrentVersion);
if (!Number.isSafeInteger(command.createdAtMs) || command.createdAtMs < 0) {
throw new TypeError('Local Secret creation time is invalid');
}
}
export interface LocalProjectSecretMaterialRequest {
readonly projectId: string;
readonly secretRef: string;
readonly signal?: AbortSignal;
}
export interface LocalProjectSecretMaterial {
readonly secretRef: string;
readonly bytes: Uint8Array;
dispose(): void | Promise<void>;
}
export class EncryptedLocalSecretService
implements LocalSecretEnvironmentProvider
{
constructor(
private readonly envelopes: LocalSecretEnvelopeRepository,
private readonly keys: LocalSecretKeyProvider,
private readonly nonceFactory?: LocalSecretNonceFactory,
) {}
async put(
command: PutEncryptedLocalSecretCommand,
): Promise<PutEncryptedLocalSecretResult> {
assertPutCommand(command);
try {
return await this.putValidated(command);
} catch (error) {
if (
error instanceof LocalSecretVersionConflictError ||
error instanceof LocalSecretMutationConflictError ||
error instanceof LocalSecretUnavailableError
) {
throw error;
}
throw new LocalSecretUnavailableError();
}
}
private async putValidated(
command: PutEncryptedLocalSecretCommand,
): Promise<PutEncryptedLocalSecretResult> {
const existing = await this.envelopes.findLocalSecretEnvelopeByMutation(
command.projectId,
command.name,
command.mutationId,
);
if (existing) {
const material = ownedLocalSecretKeyMaterial(
await this.keys.resolve(existing.keyId),
existing.keyId,
);
try {
if (
existing.version !== command.expectedCurrentVersion + 1 ||
!localSecretPlaintextMatches(
existing,
material.key,
command.plaintext,
)
) {
throw new LocalSecretMutationConflictError();
}
} finally {
material.key.fill(0);
}
return this.result('existing', existing);
}
const material = ownedLocalSecretKeyMaterial(await this.keys.active());
try {
const envelope = encryptLocalSecretEnvelope(
{
projectId: command.projectId,
name: command.name,
version: command.expectedCurrentVersion + 1,
mutationId: command.mutationId,
keyId: material.keyId,
algorithm: LOCAL_SECRET_ALGORITHM,
createdAtMs: command.createdAtMs,
},
command.plaintext,
material.key,
this.nonceFactory,
);
const appended = await this.envelopes.appendLocalSecretEnvelope({
envelope,
expectedCurrentVersion: command.expectedCurrentVersion,
});
if (appended.status === 'existing') {
const existingMaterial =
appended.envelope.keyId === material.keyId
? material
: ownedLocalSecretKeyMaterial(
await this.keys.resolve(appended.envelope.keyId),
appended.envelope.keyId,
);
try {
if (
appended.envelope.version !== command.expectedCurrentVersion + 1 ||
!localSecretPlaintextMatches(
appended.envelope,
existingMaterial.key,
command.plaintext,
)
) {
throw new LocalSecretMutationConflictError();
}
} finally {
if (existingMaterial !== material) existingMaterial.key.fill(0);
}
}
return this.result(appended.status, appended.envelope);
} finally {
material.key.fill(0);
}
}
async resolveLocalSecretEnvironment(request: {
readonly candidate: Parameters<
LocalSecretEnvironmentProvider['resolveLocalSecretEnvironment']
>[0]['candidate'];
readonly secretRefs: readonly string[];
}): Promise<readonly string[] | null> {
const cachedKeys = new Map<string, Buffer>();
try {
if (!request || typeof request !== 'object' || Array.isArray(request)) {
throw new LocalSecretUnavailableError();
}
const candidate = normalizeLocalDispatchCandidate(request.candidate);
if (
!Array.isArray(request.secretRefs) ||
request.secretRefs.length > MAX_LOCAL_SECRET_BATCH_SIZE
) {
throw new LocalSecretUnavailableError();
}
const references = request.secretRefs.map(parseLocalSecretRef);
if (
references.some(
(reference) => reference.projectId !== candidate.projectId,
)
) {
throw new LocalSecretUnavailableError();
}
const envelopes = await this.envelopes.resolveLocalSecretEnvelopes(
references,
);
if (
envelopes.length !== references.length ||
envelopes.some((item) => item === null)
) {
return null;
}
const plaintext: string[] = [];
for (const envelope of envelopes as readonly LocalSecretEnvelope[]) {
let key = cachedKeys.get(envelope.keyId);
if (!key) {
const material = ownedLocalSecretKeyMaterial(
await this.keys.resolve(envelope.keyId),
envelope.keyId,
);
key = material.key;
cachedKeys.set(envelope.keyId, key);
}
const bytes = decryptLocalSecretEnvelopeToBuffer(envelope, key);
try {
plaintext.push(decodeLocalSecretPlaintext(bytes));
} finally {
bytes.fill(0);
}
}
return Object.freeze(plaintext);
} catch (error) {
if (error instanceof LocalSecretUnavailableError) throw error;
throw new LocalSecretUnavailableError();
} finally {
for (const key of cachedKeys.values()) key.fill(0);
cachedKeys.clear();
}
}
async resolveProjectSecretMaterial(
request: Readonly<LocalProjectSecretMaterialRequest>,
): Promise<Readonly<LocalProjectSecretMaterial> | null> {
let key: Buffer | undefined;
let plaintext: Buffer | undefined;
try {
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
!exactKeys(
request,
request.signal === undefined
? ['projectId', 'secretRef']
: ['projectId', 'secretRef', 'signal'],
) ||
(request.signal !== undefined &&
typeof request.signal.aborted !== 'boolean') ||
request.signal?.aborted
) {
throw new LocalSecretUnavailableError();
}
assertLocalSecretProjectId(request.projectId);
const reference = parseLocalSecretRef(request.secretRef);
if (reference.projectId !== request.projectId) {
throw new LocalSecretUnavailableError();
}
const [envelope] = await this.envelopes.resolveLocalSecretEnvelopes([
reference,
]);
if (!envelope) return null;
const material = ownedLocalSecretKeyMaterial(
await this.keys.resolve(envelope.keyId),
envelope.keyId,
);
key = material.key;
plaintext = decryptLocalSecretEnvelopeToBuffer(envelope, key);
const ownedPlaintext = plaintext;
plaintext = undefined;
let disposed = false;
return Object.freeze({
secretRef: request.secretRef,
bytes: ownedPlaintext,
dispose(): void {
if (disposed) return;
disposed = true;
ownedPlaintext.fill(0);
},
});
} catch (error) {
plaintext?.fill(0);
if (error instanceof LocalSecretUnavailableError) throw error;
throw new LocalSecretUnavailableError();
} finally {
key?.fill(0);
}
}
private result(
status: PutEncryptedLocalSecretResult['status'],
envelope: LocalSecretEnvelope,
): PutEncryptedLocalSecretResult {
return Object.freeze({
status,
version: envelope.version,
secretRef: createLocalSecretRef({
projectId: envelope.projectId,
name: envelope.name,
version: envelope.version,
}),
});
}
}
@@ -0,0 +1,365 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
LocalSecretMutationConflictError,
LocalSecretUnavailableError,
LocalSecretVersionConflictError,
createLocalSecretRef,
parseLocalSecretRef,
} = require('@qinglong/runtime-core/local-secret');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
openLocalSqliteRuntimeDatabase,
} = require('@qinglong/local-sqlite/runtime');
const {
EncryptedLocalSecretService,
LocalSecretKeyringConflictError,
LocalSecretKeyringFileProvider,
decryptLocalSecretEnvelopeToBuffer,
encryptLocalSecretEnvelope,
provisionLocalSecretKeyring,
rotateLocalSecretKeyring,
} = require('../dist');
const KEY = Buffer.alloc(32, 0x11);
function fixture(t, profile = 'edge') {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-secret-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return {
directory,
profile,
databasePath: path.join(directory, 'qinglong3.sqlite'),
keyringPath: path.join(directory, 'secret-keyring.json'),
};
}
function candidate(projectId = 'default') {
return {
runId: 'run-secret',
attemptId: 'attempt-secret',
projectId,
taskId: 'task-secret',
taskRevision: 'revision-secret',
attemptNumber: 1,
executorType: 'local_process',
priority: 0,
queuedAtMs: 1_760_000_000_000,
attemptCreatedAtMs: 1_760_000_000_000,
};
}
async function store(t, profile = 'edge') {
const value = fixture(t, profile);
await migrateLocalSqlitePath(value);
await provisionLocalSecretKeyring(value.keyringPath);
const runtime = await openLocalSqliteRuntimeDatabase(value);
t.after(() => runtime.close());
const keys = new LocalSecretKeyringFileProvider(value.keyringPath);
return {
...value,
runtime,
keys,
service: new EncryptedLocalSecretService(runtime.localSecrets, keys),
};
}
test('uses canonical Project-bound refs and metadata-authenticated AES-GCM', () => {
const ref = createLocalSecretRef({
projectId: 'default',
name: 'TOKEN',
version: 2,
});
assert.deepEqual(parseLocalSecretRef(ref), {
projectId: 'default',
name: 'TOKEN',
version: 2,
});
const unknown = Buffer.from(
JSON.stringify({ projectId: 'default', name: 'TOKEN', extra: true }),
).toString('base64url');
assert.throws(() => parseLocalSecretRef(`qlsecret:v1:${unknown}`));
const envelope = encryptLocalSecretEnvelope(
{
projectId: 'default',
name: 'TOKEN',
version: 1,
mutationId: 'create-token',
keyId: 'edge-key-1',
algorithm: 'aes-256-gcm',
createdAtMs: 100,
},
'fixed-secret',
KEY,
() => Buffer.alloc(12, 0x22),
);
assert.deepEqual(
{
nonce: envelope.nonce,
ciphertext: envelope.ciphertext,
authTag: envelope.authTag,
},
{
nonce: 'IiIiIiIiIiIiIiIi',
ciphertext: 'cZ5_LKTi7DqGTbtI',
authTag: '2af0_G7xHiOEBvMlsBSr0Q',
},
);
const plaintext = decryptLocalSecretEnvelopeToBuffer(envelope, KEY);
assert.equal(plaintext.toString('utf8'), 'fixed-secret');
plaintext.fill(0);
assert.throws(
() =>
decryptLocalSecretEnvelopeToBuffer(
{ ...envelope, projectId: 'another-project' },
KEY,
),
LocalSecretUnavailableError,
);
});
test('provisions once, reloads active rotation and keeps historical keys', async (t) => {
const value = fixture(t);
const first = await provisionLocalSecretKeyring(value.keyringPath);
assert.equal(first.keyIds.length, 1);
assert.equal(fs.statSync(value.keyringPath).mode & 0o777, 0o600);
await assert.rejects(
provisionLocalSecretKeyring(value.keyringPath),
LocalSecretUnavailableError,
);
const provider = new LocalSecretKeyringFileProvider(value.keyringPath);
const firstMaterial = await provider.active();
assert.equal(firstMaterial.keyId, first.activeKeyId);
firstMaterial.key.fill(0);
const second = await rotateLocalSecretKeyring({
filePath: value.keyringPath,
expectedActiveKeyId: first.activeKeyId,
});
assert.equal(second.keyIds.length, 2);
assert.notEqual(second.activeKeyId, first.activeKeyId);
const activeMaterial = await provider.active();
assert.equal(activeMaterial.keyId, second.activeKeyId);
activeMaterial.key.fill(0);
const historicalMaterial = await provider.resolve(first.activeKeyId);
assert.equal(historicalMaterial.key.length, 32);
historicalMaterial.key.fill(0);
await assert.rejects(
rotateLocalSecretKeyring({
filePath: value.keyringPath,
expectedActiveKeyId: first.activeKeyId,
}),
LocalSecretKeyringConflictError,
);
fs.chmodSync(value.keyringPath, 0o644);
await assert.rejects(provider.active(), LocalSecretUnavailableError);
});
test('rejects keyring symlinks and preserves an existing rotation lock', async (t) => {
const value = fixture(t);
const first = await provisionLocalSecretKeyring(value.keyringPath);
const symlinkPath = path.join(value.directory, 'secret-keyring-link.json');
fs.symlinkSync(value.keyringPath, symlinkPath);
const symlinkProvider = new LocalSecretKeyringFileProvider(symlinkPath);
await assert.rejects(symlinkProvider.active(), LocalSecretUnavailableError);
await assert.rejects(
rotateLocalSecretKeyring({
filePath: symlinkPath,
expectedActiveKeyId: first.activeKeyId,
}),
LocalSecretUnavailableError,
);
const lockPath = `${value.keyringPath}.lock`;
fs.writeFileSync(lockPath, 'external-manager\n', { mode: 0o600, flag: 'wx' });
await assert.rejects(
rotateLocalSecretKeyring({
filePath: value.keyringPath,
expectedActiveKeyId: first.activeKeyId,
}),
LocalSecretUnavailableError,
);
assert.equal(fs.readFileSync(lockPath, 'utf8'), 'external-manager\n');
assert.equal(
(await new LocalSecretKeyringFileProvider(value.keyringPath).inspect())
.activeKeyId,
first.activeKeyId,
);
});
test('creates and rotates append-only ciphertext through the Node 24 SQLite authority', async (t) => {
const value = await store(t);
const firstPlaintext = 'first-plaintext-never-persisted';
const secondPlaintext = 'second-plaintext-never-persisted';
const first = await value.service.put({
projectId: 'default',
name: 'TOKEN',
plaintext: firstPlaintext,
mutationId: 'create-token',
expectedCurrentVersion: 0,
createdAtMs: 100,
});
const rotatedKeyring = await rotateLocalSecretKeyring({
filePath: value.keyringPath,
expectedActiveKeyId: (await value.keys.inspect()).activeKeyId,
});
const second = await value.service.put({
projectId: 'default',
name: 'TOKEN',
plaintext: secondPlaintext,
mutationId: 'rotate-token',
expectedCurrentVersion: 1,
createdAtMs: 200,
});
assert.equal(first.version, 1);
assert.equal(second.version, 2);
assert.equal(
(await value.keys.inspect()).activeKeyId,
rotatedKeyring.activeKeyId,
);
assert.deepEqual(
await value.service.resolveLocalSecretEnvironment({
candidate: candidate(),
secretRefs: [
createLocalSecretRef({ projectId: 'default', name: 'TOKEN' }),
first.secretRef,
second.secretRef,
],
}),
[secondPlaintext, firstPlaintext, secondPlaintext],
);
const material = await value.service.resolveProjectSecretMaterial({
projectId: 'default',
secretRef: createLocalSecretRef({
projectId: 'default',
name: 'TOKEN',
}),
});
assert.ok(material);
assert.equal(material.secretRef.includes(secondPlaintext), false);
assert.equal(Buffer.from(material.bytes).toString('utf8'), secondPlaintext);
const ownedBytes = material.bytes;
await material.dispose();
assert.deepEqual([...ownedBytes], new Array(ownedBytes.length).fill(0));
await material.dispose();
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
const rows = database
.prepare(
`SELECT ciphertext, key_id FROM "QingLong3LocalSecretEnvelopes"
ORDER BY version`,
)
.all();
assert.equal(rows.length, 2);
assert.notEqual(rows[0].key_id, rows[1].key_id);
assert.equal(JSON.stringify(rows).includes(firstPlaintext), false);
assert.equal(JSON.stringify(rows).includes(secondPlaintext), false);
} finally {
database.close();
}
});
test('replays semantic mutations, fences stale versions and checks Project before storage', async (t) => {
const value = await store(t, 'standalone');
const command = {
projectId: 'default',
name: 'TOKEN',
plaintext: 'same-value',
mutationId: 'create-token',
expectedCurrentVersion: 0,
createdAtMs: 100,
};
assert.equal((await value.service.put(command)).status, 'inserted');
assert.equal(
(await value.service.put({ ...command, createdAtMs: 999 })).status,
'existing',
);
await assert.rejects(
value.service.put({ ...command, plaintext: 'different-value' }),
LocalSecretMutationConflictError,
);
await assert.rejects(
value.service.put({ ...command, mutationId: 'stale-create' }),
LocalSecretVersionConflictError,
);
await assert.rejects(
value.service.resolveLocalSecretEnvironment({
candidate: candidate('default'),
secretRefs: [
createLocalSecretRef({ projectId: 'another', name: 'TOKEN' }),
],
}),
LocalSecretUnavailableError,
);
await assert.rejects(
value.service.resolveProjectSecretMaterial({
projectId: 'default',
secretRef: createLocalSecretRef({
projectId: 'another',
name: 'TOKEN',
}),
}),
LocalSecretUnavailableError,
);
});
test('two SQLite authorities allow exactly one rotation for one expected version', async (t) => {
const value = fixture(t, 'standalone');
await migrateLocalSqlitePath(value);
await provisionLocalSecretKeyring(value.keyringPath);
const firstRuntime = await openLocalSqliteRuntimeDatabase(value);
const secondRuntime = await openLocalSqliteRuntimeDatabase(value);
t.after(() => Promise.all([firstRuntime.close(), secondRuntime.close()]));
const keys = new LocalSecretKeyringFileProvider(value.keyringPath);
const first = new EncryptedLocalSecretService(
firstRuntime.localSecrets,
keys,
);
const second = new EncryptedLocalSecretService(
secondRuntime.localSecrets,
keys,
);
await first.put({
projectId: 'default',
name: 'TOKEN',
plaintext: 'initial',
mutationId: 'initial',
expectedCurrentVersion: 0,
createdAtMs: 1,
});
const results = await Promise.allSettled([
first.put({
projectId: 'default',
name: 'TOKEN',
plaintext: 'winner-a',
mutationId: 'rotate-a',
expectedCurrentVersion: 1,
createdAtMs: 2,
}),
second.put({
projectId: 'default',
name: 'TOKEN',
plaintext: 'winner-b',
mutationId: 'rotate-b',
expectedCurrentVersion: 1,
createdAtMs: 2,
}),
]);
assert.equal(
results.filter(({ status }) => status === 'fulfilled').length,
1,
);
assert.equal(results.filter(({ status }) => status === 'rejected').length, 1);
assert.equal(
results.find(({ status }) => status === 'rejected').reason.constructor,
LocalSecretVersionConflictError,
);
});
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"types": ["node"],
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": false
},
"include": ["src/**/*.ts"]
}