@sirmrmarty/n8n-nodes-tmux-orchestrator
Version:
n8n nodes for orchestrating Claude AI agents through tmux sessions
802 lines (700 loc) • 23.4 kB
text/typescript
import { randomBytes, createHash, timingSafeEqual } from 'crypto';
import * as ed25519 from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha512';
import { promisify } from 'util';
import { stateManager } from './threadSafeState';
// Configure Ed25519 to use noble-hashes
ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
/**
* Secure memory utilities for cryptographic operations
*/
class SecureMemory {
/**
* Securely zero out a Uint8Array
*/
static zeroize(buffer: Uint8Array): void {
if (buffer && buffer.length > 0) {
for (let i = 0; i < buffer.length; i++) {
buffer[i] = 0;
}
// Additional overwrite with random data for paranoid security
const random = randomBytes(buffer.length);
for (let i = 0; i < buffer.length; i++) {
buffer[i] = random[i];
}
// Final zero pass
for (let i = 0; i < buffer.length; i++) {
buffer[i] = 0;
}
}
}
/**
* Create a secure copy of a Uint8Array
*/
static secureCopy(source: Uint8Array): Uint8Array {
const copy = new Uint8Array(source.length);
copy.set(source);
return copy;
}
}
/**
* Secure private key wrapper that automatically manages key lifecycle
*/
class SecurePrivateKey {
private keyData: Uint8Array | null = null;
private isDestroyed: boolean = false;
private readonly createdAt: number = Date.now();
private readonly keyId: string;
constructor(privateKey: Uint8Array) {
if (!privateKey || privateKey.length !== 32) {
throw new Error('Invalid private key: must be 32 bytes');
}
this.keyData = SecureMemory.secureCopy(privateKey);
this.keyId = randomBytes(8).toString('hex');
// Auto-destroy after 5 minutes to limit exposure
setTimeout(() => {
if (!this.isDestroyed) {
this.destroy();
}
}, 5 * 60 * 1000);
}
/**
* Use the private key in a secure callback pattern
* Key is only accessible within the callback and immediately zeroized after
*/
use<T>(callback: (key: Uint8Array) => T | Promise<T>): T | Promise<T> {
if (this.isDestroyed || !this.keyData) {
throw new Error('SecurePrivateKey has been destroyed');
}
try {
// Create temporary copy for use
const tempKey = SecureMemory.secureCopy(this.keyData);
try {
return callback(tempKey);
} finally {
// Always zeroize the temporary copy
SecureMemory.zeroize(tempKey);
}
} catch (error) {
// Ensure no key material in error messages
throw new Error(`Cryptographic operation failed: ${error.message}`);
}
}
/**
* Use the private key asynchronously in a secure callback pattern
*/
async useAsync<T>(callback: (key: Uint8Array) => Promise<T>): Promise<T> {
if (this.isDestroyed || !this.keyData) {
throw new Error('SecurePrivateKey has been destroyed');
}
try {
// Create temporary copy for use
const tempKey = SecureMemory.secureCopy(this.keyData);
try {
return await callback(tempKey);
} finally {
// Always zeroize the temporary copy
SecureMemory.zeroize(tempKey);
}
} catch (error) {
// Ensure no key material in error messages
throw new Error(`Cryptographic operation failed: ${error.message}`);
}
}
/**
* Get the corresponding public key
*/
async getPublicKey(): Promise<Uint8Array> {
return this.useAsync(async (privateKey) => {
return await ed25519.getPublicKey(privateKey);
});
}
/**
* Get the public key as hex string
*/
async getPublicKeyHex(): Promise<string> {
const publicKey = await this.getPublicKey();
return Buffer.from(publicKey).toString('hex');
}
/**
* Sign data with this private key
*/
async sign(data: Uint8Array): Promise<Uint8Array> {
return this.useAsync(async (privateKey) => {
return await ed25519.sign(data, privateKey);
});
}
/**
* Check if the key has been destroyed
*/
isValid(): boolean {
return !this.isDestroyed && this.keyData !== null;
}
/**
* Get key metadata (safe to log)
*/
getMetadata(): { keyId: string; createdAt: number; isValid: boolean } {
return {
keyId: this.keyId,
createdAt: this.createdAt,
isValid: this.isValid()
};
}
/**
* Securely destroy the private key
*/
destroy(): void {
if (this.keyData) {
SecureMemory.zeroize(this.keyData);
this.keyData = null;
}
this.isDestroyed = true;
}
}
/**
* Secure key manager for Ed25519 cryptographic operations
*/
export class SecureKeyManager {
private static activeKeys = stateManager.getMap<string, SecurePrivateKey>('activeKeys');
private static keyRotationInterval = 24 * 60 * 60 * 1000; // 24 hours
/**
* Create a secure private key from various sources
*/
static async createSecureKey(source: Uint8Array | Buffer | string): Promise<SecurePrivateKey> {
let keyBytes: Uint8Array;
if (typeof source === 'string') {
// Handle hex string input (legacy support during migration)
if (!/^[a-fA-F0-9]{64}$/.test(source)) {
throw new Error('Invalid hex private key format');
}
keyBytes = new Uint8Array(Buffer.from(source, 'hex'));
} else if (source instanceof Buffer) {
keyBytes = new Uint8Array(source);
} else {
keyBytes = source;
}
const secureKey = new SecurePrivateKey(keyBytes);
try {
await this.activeKeys.safeSet(secureKey.getMetadata().keyId, secureKey);
} catch (error) {
console.warn('Failed to track secure key:', error.message);
}
// Clean up destroyed keys periodically
setTimeout(async () => {
await this.cleanupDestroyedKeys();
}, 60000); // 1 minute
return secureKey;
}
/**
* Generate a new secure key pair
*/
static async generateSecureKeyPair(): Promise<{ privateKey: SecurePrivateKey; publicKeyHex: string }> {
const privateKeyBytes = randomBytes(32);
const privateKey = await this.createSecureKey(privateKeyBytes);
// Zeroize the original bytes
SecureMemory.zeroize(privateKeyBytes);
const publicKeyHex = await privateKey.getPublicKeyHex();
return {
privateKey,
publicKeyHex
};
}
/**
* Clean up destroyed keys from active tracking
*/
private static async cleanupDestroyedKeys(): Promise<void> {
try {
const entries = this.activeKeys.safeEntries();
for (const [keyId, key] of entries) {
if (!key.isValid()) {
try {
await this.activeKeys.safeDelete(keyId);
} catch (error) {
console.warn(`Failed to cleanup key ${keyId}:`, error.message);
}
}
}
} catch (error) {
console.error('Failed to cleanup destroyed keys:', error.message);
}
}
/**
* Get statistics about active keys (for monitoring)
*/
static async getKeyStats(): Promise<{ activeKeys: number; oldestKeyAge: number }> {
await this.cleanupDestroyedKeys();
let oldestAge = 0;
const now = Date.now();
for (const key of this.activeKeys.safeValues()) {
if (key.isValid()) {
const age = now - key.getMetadata().createdAt;
oldestAge = Math.max(oldestAge, age);
}
}
return {
activeKeys: this.activeKeys.safeSize(),
oldestKeyAge: oldestAge
};
}
}
export interface QAApprovalData {
projectName: string;
commitHash: string;
commitMessage: string;
testResults: QATestResults;
qaEngineerID: string;
approvalTimestamp: number;
expirationTimestamp: number;
approvalNonce: string;
}
export interface QATestResults {
unit: boolean;
integration: boolean;
security: boolean;
performance: boolean;
coverage: number;
passedTests: number;
totalTests: number;
criticalIssues: string[];
}
export interface CryptoQAApproval {
data: QAApprovalData;
signature: string;
publicKey: string;
}
export interface QAKeyPair {
privateKey: SecurePrivateKey;
publicKey: Uint8Array;
publicKeyHex: string;
}
export class CryptographicQASystem {
private static readonly APPROVAL_VALIDITY_DURATION = 30 * 60 * 1000; // 30 minutes
private static readonly MIN_COVERAGE_THRESHOLD = 80; // 80% code coverage minimum
private static readonly MAX_CRITICAL_ISSUES = 0; // No critical issues allowed
// Cache for verified public keys to prevent key substitution attacks
private static readonly verifiedPublicKeys = stateManager.getMap<string, { publicKey: string; verifiedAt: number; qaEngineerID: string }>('verifiedPublicKeys');
/**
* Generate a new Ed25519 key pair for QA Engineer with secure key management
*/
static async generateQAKeyPair(): Promise<QAKeyPair> {
try {
const keyPair = await SecureKeyManager.generateSecureKeyPair();
const publicKey = await keyPair.privateKey.getPublicKey();
return {
privateKey: keyPair.privateKey,
publicKey,
publicKeyHex: keyPair.publicKeyHex
};
} catch (error) {
throw new Error(`Failed to generate secure key pair: ${error.message}`);
}
}
/**
* Register and verify a QA Engineer's public key
*/
static async registerQAEngineer(qaEngineerID: string, publicKey: string, masterAuthSignature?: string): Promise<boolean> {
try {
// Validate inputs
if (!qaEngineerID || typeof qaEngineerID !== 'string') {
throw new Error('Invalid QA Engineer ID');
}
if (!/^[a-fA-F0-9]{64}$/.test(publicKey)) {
throw new Error('Invalid public key format (must be 64-char hex)');
}
// Verify the public key is valid
const publicKeyBytes = Buffer.from(publicKey, 'hex');
if (publicKeyBytes.length !== 32) {
throw new Error('Invalid public key length');
}
// Store verified public key
try {
await this.verifiedPublicKeys.safeSet(qaEngineerID, {
publicKey,
verifiedAt: Date.now(),
qaEngineerID
});
} catch (error) {
console.error('Failed to store verified public key:', error.message);
throw new Error('Failed to register QA Engineer');
}
return true;
} catch (error) {
throw new Error(`Failed to register QA Engineer: ${error.message}`);
}
}
/**
* Create cryptographically signed QA approval with secure key management
*/
static async createQAApproval(
approvalData: QAApprovalData,
securePrivateKey: SecurePrivateKey
): Promise<CryptoQAApproval> {
try {
// Validate approval data
this.validateApprovalData(approvalData);
// Validate secure key
if (!securePrivateKey.isValid()) {
throw new Error('Invalid or destroyed private key');
}
// Generate cryptographic nonce
approvalData.approvalNonce = randomBytes(16).toString('hex');
approvalData.approvalTimestamp = Date.now();
approvalData.expirationTimestamp = Date.now() + this.APPROVAL_VALIDITY_DURATION;
// Create deterministic serialization for signing
const dataToSign = this.serializeApprovalData(approvalData);
const dataHash = createHash('sha256').update(dataToSign).digest();
// Sign the hash with secure Ed25519 operations
const signature = await securePrivateKey.sign(dataHash);
const publicKeyHex = await securePrivateKey.getPublicKeyHex();
const approval: CryptoQAApproval = {
data: approvalData,
signature: Buffer.from(signature).toString('hex'),
publicKey: publicKeyHex
};
return approval;
} catch (error) {
// Ensure no key material in error messages
const safeError = error.message.replace(/[a-fA-F0-9]{64}/g, '[REDACTED_KEY]');
throw new Error(`Failed to create QA approval: ${safeError}`);
}
}
/**
* Verify cryptographic QA approval
*/
static async verifyQAApproval(approval: CryptoQAApproval): Promise<{ valid: boolean; reason?: string }> {
try {
// Basic structure validation
if (!approval || !approval.data || !approval.signature || !approval.publicKey) {
return { valid: false, reason: 'Invalid approval structure' };
}
// Check if approval has expired
if (Date.now() > approval.data.expirationTimestamp) {
return { valid: false, reason: 'Approval has expired' };
}
// Verify QA Engineer is registered
const registeredKey = this.verifiedPublicKeys.safeGet(approval.data.qaEngineerID);
if (!registeredKey) {
return { valid: false, reason: 'QA Engineer not registered' };
}
// Verify public key matches registered key (prevent key substitution)
if (!timingSafeEqual(Buffer.from(registeredKey.publicKey, 'hex'), Buffer.from(approval.publicKey, 'hex'))) {
return { valid: false, reason: 'Public key does not match registered key' };
}
// Verify quality requirements are met
const qualityCheck = this.verifyQualityRequirements(approval.data.testResults);
if (!qualityCheck.valid) {
return { valid: false, reason: `Quality requirements not met: ${qualityCheck.reason}` };
}
// Verify cryptographic signature
const dataToSign = this.serializeApprovalData(approval.data);
const dataHash = createHash('sha256').update(dataToSign).digest();
const signature = Buffer.from(approval.signature, 'hex');
const publicKey = Buffer.from(approval.publicKey, 'hex');
const signatureValid = await ed25519.verify(signature, dataHash, publicKey);
if (!signatureValid) {
return { valid: false, reason: 'Invalid cryptographic signature' };
}
// All checks passed
return { valid: true };
} catch (error) {
return { valid: false, reason: `Verification error: ${error.message}` };
}
}
/**
* Validate approval data structure and content
*/
private static validateApprovalData(data: QAApprovalData): void {
if (!data.projectName || typeof data.projectName !== 'string') {
throw new Error('Invalid project name');
}
if (!data.commitHash || !/^[a-fA-F0-9]{40}$/.test(data.commitHash)) {
throw new Error('Invalid commit hash');
}
if (!data.commitMessage || typeof data.commitMessage !== 'string') {
throw new Error('Invalid commit message');
}
if (!data.qaEngineerID || typeof data.qaEngineerID !== 'string') {
throw new Error('Invalid QA Engineer ID');
}
if (!data.testResults || typeof data.testResults !== 'object') {
throw new Error('Invalid test results');
}
// Validate test results
const { testResults } = data;
if (typeof testResults.unit !== 'boolean' ||
typeof testResults.integration !== 'boolean' ||
typeof testResults.security !== 'boolean' ||
typeof testResults.performance !== 'boolean') {
throw new Error('Invalid test result format');
}
if (!Number.isInteger(testResults.coverage) || testResults.coverage < 0 || testResults.coverage > 100) {
throw new Error('Invalid coverage percentage');
}
if (!Number.isInteger(testResults.passedTests) || testResults.passedTests < 0) {
throw new Error('Invalid passed tests count');
}
if (!Number.isInteger(testResults.totalTests) || testResults.totalTests < 0) {
throw new Error('Invalid total tests count');
}
if (!Array.isArray(testResults.criticalIssues)) {
throw new Error('Critical issues must be an array');
}
}
/**
* Verify quality requirements are met
*/
private static verifyQualityRequirements(testResults: QATestResults): { valid: boolean; reason?: string } {
// All core tests must pass
if (!testResults.unit) {
return { valid: false, reason: 'Unit tests failed' };
}
if (!testResults.integration) {
return { valid: false, reason: 'Integration tests failed' };
}
if (!testResults.security) {
return { valid: false, reason: 'Security tests failed' };
}
if (!testResults.performance) {
return { valid: false, reason: 'Performance tests failed' };
}
// Coverage requirements
if (testResults.coverage < this.MIN_COVERAGE_THRESHOLD) {
return { valid: false, reason: `Code coverage ${testResults.coverage}% below minimum ${this.MIN_COVERAGE_THRESHOLD}%` };
}
// No critical issues allowed
if (testResults.criticalIssues.length > this.MAX_CRITICAL_ISSUES) {
return { valid: false, reason: `${testResults.criticalIssues.length} critical issues found (max ${this.MAX_CRITICAL_ISSUES} allowed)` };
}
// Test success rate
const successRate = testResults.totalTests > 0 ? (testResults.passedTests / testResults.totalTests) * 100 : 0;
if (successRate < 100) {
return { valid: false, reason: `Test success rate ${successRate.toFixed(1)}% (100% required)` };
}
return { valid: true };
}
/**
* Create deterministic serialization for signing
*/
private static serializeApprovalData(data: QAApprovalData): string {
// Create deterministic JSON serialization
const serializable = {
projectName: data.projectName,
commitHash: data.commitHash,
commitMessage: data.commitMessage,
qaEngineerID: data.qaEngineerID,
approvalTimestamp: data.approvalTimestamp,
expirationTimestamp: data.expirationTimestamp,
approvalNonce: data.approvalNonce,
testResults: {
unit: data.testResults.unit,
integration: data.testResults.integration,
security: data.testResults.security,
performance: data.testResults.performance,
coverage: data.testResults.coverage,
passedTests: data.testResults.passedTests,
totalTests: data.testResults.totalTests,
criticalIssues: data.testResults.criticalIssues.slice().sort() // Sort for determinism
}
};
return JSON.stringify(serializable, Object.keys(serializable).sort());
}
/**
* Create a QA block (rejection) with secure signature
*/
static async createQABlock(
projectName: string,
commitHash: string,
commitMessage: string,
qaEngineerID: string,
blockReason: string,
testResults: QATestResults,
securePrivateKey: SecurePrivateKey
): Promise<{ blockData: any; signature: string; publicKey: string }> {
try {
// Validate secure key
if (!securePrivateKey.isValid()) {
throw new Error('Invalid or destroyed private key');
}
const blockData = {
projectName,
commitHash,
commitMessage,
qaEngineerID,
blockReason,
testResults,
blockTimestamp: Date.now(),
blockNonce: randomBytes(16).toString('hex')
};
// Sign the block with secure operations
const dataToSign = JSON.stringify(blockData, Object.keys(blockData).sort());
const dataHash = createHash('sha256').update(dataToSign).digest();
const signature = await securePrivateKey.sign(dataHash);
const publicKeyHex = await securePrivateKey.getPublicKeyHex();
return {
blockData,
signature: Buffer.from(signature).toString('hex'),
publicKey: publicKeyHex
};
} catch (error) {
// Ensure no key material in error messages
const safeError = error.message.replace(/[a-fA-F0-9]{64}/g, '[REDACTED_KEY]');
throw new Error(`Failed to create QA block: ${safeError}`);
}
}
/**
* Generate audit trail hash for compliance
*/
static generateAuditHash(approval: CryptoQAApproval): string {
const auditData = {
projectName: approval.data.projectName,
commitHash: approval.data.commitHash,
qaEngineerID: approval.data.qaEngineerID,
timestamp: approval.data.approvalTimestamp,
signature: approval.signature
};
return createHash('sha256')
.update(JSON.stringify(auditData, Object.keys(auditData).sort()))
.digest('hex');
}
/**
* Generate key rotation recommendation based on key age and usage
*/
static shouldRotateKey(keyCreatedAt: number, signingOperations: number = 0): {
should: boolean;
reason: string;
urgency: 'low' | 'medium' | 'high' | 'critical';
} {
const now = Date.now();
const keyAge = now - keyCreatedAt;
const daysSinceCreation = keyAge / (24 * 60 * 60 * 1000);
// Critical: Key older than 90 days
if (daysSinceCreation > 90) {
return {
should: true,
reason: `Key is ${Math.floor(daysSinceCreation)} days old (>90 days)`,
urgency: 'critical'
};
}
// High: Key older than 60 days or excessive usage
if (daysSinceCreation > 60 || signingOperations > 10000) {
return {
should: true,
reason: daysSinceCreation > 60
? `Key is ${Math.floor(daysSinceCreation)} days old (>60 days)`
: `Key has been used ${signingOperations} times (>10000 operations)`,
urgency: 'high'
};
}
// Medium: Key older than 30 days or high usage
if (daysSinceCreation > 30 || signingOperations > 5000) {
return {
should: true,
reason: daysSinceCreation > 30
? `Key is ${Math.floor(daysSinceCreation)} days old (>30 days)`
: `Key has been used ${signingOperations} times (>5000 operations)`,
urgency: 'medium'
};
}
// Low: Key older than 14 days
if (daysSinceCreation > 14) {
return {
should: true,
reason: `Key is ${Math.floor(daysSinceCreation)} days old (>14 days)`,
urgency: 'low'
};
}
return {
should: false,
reason: `Key is ${Math.floor(daysSinceCreation)} days old and has ${signingOperations} operations`,
urgency: 'low'
};
}
/**
* Perform security audit of the cryptographic system
*/
static async performSecurityAudit(): Promise<{
securityScore: number;
findings: Array<{ severity: string; issue: string; recommendation: string }>;
keyStats: { activeKeys: number; oldestKeyAge: number };
}> {
const findings: Array<{ severity: string; issue: string; recommendation: string }> = [];
let securityScore = 100;
// Check key management statistics
const keyStats = await SecureKeyManager.getKeyStats();
// Check for old keys
if (keyStats.oldestKeyAge > 90 * 24 * 60 * 60 * 1000) {
findings.push({
severity: 'CRITICAL',
issue: `Keys older than 90 days detected (${Math.floor(keyStats.oldestKeyAge / (24 * 60 * 60 * 1000))} days)`,
recommendation: 'Immediately rotate all keys older than 90 days'
});
securityScore -= 30;
} else if (keyStats.oldestKeyAge > 60 * 24 * 60 * 60 * 1000) {
findings.push({
severity: 'HIGH',
issue: `Keys older than 60 days detected (${Math.floor(keyStats.oldestKeyAge / (24 * 60 * 60 * 1000))} days)`,
recommendation: 'Schedule key rotation within 7 days'
});
securityScore -= 15;
}
// Check for excessive active keys
if (keyStats.activeKeys > 10) {
findings.push({
severity: 'MEDIUM',
issue: `High number of active keys detected (${keyStats.activeKeys})`,
recommendation: 'Review and cleanup unused keys to reduce attack surface'
});
securityScore -= 10;
}
// Check for proper key destruction
const publicKeyCount = this.verifiedPublicKeys.safeSize();
if (publicKeyCount > keyStats.activeKeys * 2) {
findings.push({
severity: 'LOW',
issue: 'Potential key cleanup needed - registered keys exceed active keys significantly',
recommendation: 'Review registered public keys and remove obsolete entries'
});
securityScore -= 5;
}
// Positive findings
if (findings.length === 0) {
findings.push({
severity: 'INFO',
issue: 'No security issues detected',
recommendation: 'Continue monitoring key lifecycle and maintain security practices'
});
}
return {
securityScore: Math.max(0, securityScore),
findings,
keyStats
};
}
/**
* Create secure audit log entry (no sensitive data)
*/
static createSecureAuditLog(operation: string, qaEngineerID: string, details: any = {}): {
timestamp: number;
operation: string;
qaEngineerID: string;
details: any;
auditHash: string;
} {
// Sanitize details to remove any potential key material
const sanitizedDetails = JSON.parse(
JSON.stringify(details).replace(/[a-fA-F0-9]{64}/g, '[REDACTED_KEY]')
);
const auditEntry = {
timestamp: Date.now(),
operation,
qaEngineerID,
details: sanitizedDetails,
auditHash: ''
};
// Generate audit hash
auditEntry.auditHash = createHash('sha256')
.update(JSON.stringify(auditEntry, Object.keys(auditEntry).sort()))
.digest('hex');
return auditEntry;
}
}