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:
203
ts/classes/encryption.ts
Normal file
203
ts/classes/encryption.ts
Normal 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();
|
||||
Reference in New Issue
Block a user