feat: Implement platform service providers for MinIO and MongoDB

- Added base interface and abstract class for platform service providers.
- Created MinIOProvider class for S3-compatible storage with deployment, provisioning, and deprovisioning functionalities.
- Implemented MongoDBProvider class for MongoDB service with similar capabilities.
- Introduced error handling utilities for better error management.
- Developed TokensComponent for managing registry tokens in the UI, including creation, deletion, and display of tokens.
This commit is contained in:
2025-11-25 04:20:19 +00:00
parent 9aa6906ca5
commit 8ebd677478
28 changed files with 3462 additions and 490 deletions

203
ts/classes/encryption.ts Normal file
View File

@@ -0,0 +1,203 @@
/**
* AES-256-GCM encryption for credential storage
*/
import { logger } from '../logging.ts';
export class CredentialEncryption {
private key: CryptoKey | null = null;
private readonly algorithm = 'AES-GCM';
private readonly keyLength = 256;
private readonly ivLength = 12; // 96 bits for GCM
/**
* Initialize encryption with a key from environment or generate machine-specific key
*/
async init(): Promise<void> {
const envKey = Deno.env.get('ONEBOX_ENCRYPTION_KEY');
if (envKey) {
// Use provided key (should be 32 bytes base64 encoded)
const keyBytes = this.base64ToBytes(envKey);
if (keyBytes.length !== 32) {
throw new Error('ONEBOX_ENCRYPTION_KEY must be 32 bytes (256 bits) base64 encoded');
}
this.key = await crypto.subtle.importKey(
'raw',
keyBytes,
{ name: this.algorithm },
false,
['encrypt', 'decrypt']
);
logger.log('info', 'Encryption key loaded from environment', 'CredentialEncryption');
} else {
// Derive key from machine-specific data
this.key = await this.deriveKeyFromMachine();
logger.log('info', 'Encryption key derived from machine identity', 'CredentialEncryption');
}
}
/**
* Derive a key from machine-specific information
* This ensures the key is consistent across restarts on the same machine
*/
private async deriveKeyFromMachine(): Promise<CryptoKey> {
// Collect machine-specific data
const machineData: string[] = [];
// Hostname
try {
machineData.push(Deno.hostname());
} catch {
machineData.push('unknown-host');
}
// Machine ID from /etc/machine-id (Linux) or generate consistent fallback
try {
const machineId = await Deno.readTextFile('/etc/machine-id');
machineData.push(machineId.trim());
} catch {
// Fallback: use a combination of other identifiers
machineData.push('onebox-default-machine-id');
}
// Add a salt
machineData.push('onebox-credential-encryption-v1');
// Create seed from machine data
const seed = machineData.join(':');
const encoder = new TextEncoder();
const seedBytes = encoder.encode(seed);
// Use PBKDF2 to derive key
const baseKey = await crypto.subtle.importKey(
'raw',
seedBytes,
'PBKDF2',
false,
['deriveKey']
);
return await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode('onebox-salt-v1'),
iterations: 100000,
hash: 'SHA-256',
},
baseKey,
{ name: this.algorithm, length: this.keyLength },
false,
['encrypt', 'decrypt']
);
}
/**
* Encrypt a credentials object to a base64 string
*/
async encrypt(data: Record<string, string>): Promise<string> {
if (!this.key) {
throw new Error('Encryption not initialized. Call init() first.');
}
const iv = crypto.getRandomValues(new Uint8Array(this.ivLength));
const encoded = new TextEncoder().encode(JSON.stringify(data));
const ciphertext = await crypto.subtle.encrypt(
{ name: this.algorithm, iv },
this.key,
encoded
);
// Combine IV + ciphertext and encode as base64
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertext), iv.length);
return this.bytesToBase64(combined);
}
/**
* Decrypt a base64 string back to credentials object
*/
async decrypt(encrypted: string): Promise<Record<string, string>> {
if (!this.key) {
throw new Error('Encryption not initialized. Call init() first.');
}
const combined = this.base64ToBytes(encrypted);
// Extract IV and ciphertext
const iv = combined.slice(0, this.ivLength);
const ciphertext = combined.slice(this.ivLength);
const decrypted = await crypto.subtle.decrypt(
{ name: this.algorithm, iv },
this.key,
ciphertext
);
const decoded = new TextDecoder().decode(decrypted);
return JSON.parse(decoded);
}
/**
* Generate a secure random password
*/
generatePassword(length: number = 32): string {
// Exclude ambiguous characters (0, O, l, 1, I)
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
const randomBytes = crypto.getRandomValues(new Uint8Array(length));
let result = '';
for (const byte of randomBytes) {
result += chars[byte % chars.length];
}
return result;
}
/**
* Generate an access key (alphanumeric, uppercase)
*/
generateAccessKey(length: number = 20): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const randomBytes = crypto.getRandomValues(new Uint8Array(length));
let result = '';
for (const byte of randomBytes) {
result += chars[byte % chars.length];
}
return result;
}
/**
* Generate a secret key (alphanumeric, mixed case)
*/
generateSecretKey(length: number = 40): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const randomBytes = crypto.getRandomValues(new Uint8Array(length));
let result = '';
for (const byte of randomBytes) {
result += chars[byte % chars.length];
}
return result;
}
private bytesToBase64(bytes: Uint8Array): string {
let binary = '';
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
}
private base64ToBytes(base64: string): Uint8Array {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
}
// Singleton instance
export const credentialEncryption = new CredentialEncryption();