mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-27 01:58:07 +03:00
169 lines
4.7 KiB
TypeScript
169 lines
4.7 KiB
TypeScript
import * as crypto from "crypto";
|
|
|
|
// Encryption configuration
|
|
const ALGORITHM = "aes-256-gcm";
|
|
const IV_LENGTH = 16; // 128 bits
|
|
const SALT_LENGTH = 32; // 256 bits
|
|
const TAG_LENGTH = 16; // 128 bits
|
|
const KEY_LENGTH = 32; // 256 bits
|
|
const ITERATIONS = 100000; // PBKDF2 iterations
|
|
|
|
// Get or generate encryption key
|
|
function getEncryptionKey(): Buffer {
|
|
const secret = process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET;
|
|
|
|
if (!secret) {
|
|
throw new Error("No encryption secret found. Please set ENCRYPTION_SECRET environment variable.");
|
|
}
|
|
|
|
// Use a static salt derived from the secret for consistent key generation
|
|
// This ensures the same key is generated across application restarts
|
|
const salt = crypto.createHash('sha256').update('gitea-mirror-salt' + secret).digest();
|
|
|
|
return crypto.pbkdf2Sync(secret, salt, ITERATIONS, KEY_LENGTH, 'sha256');
|
|
}
|
|
|
|
export interface EncryptedData {
|
|
encrypted: string;
|
|
iv: string;
|
|
salt: string;
|
|
tag: string;
|
|
version: number;
|
|
}
|
|
|
|
/**
|
|
* Encrypts sensitive data like API tokens
|
|
* @param plaintext The data to encrypt
|
|
* @returns Encrypted data with metadata
|
|
*/
|
|
export function encrypt(plaintext: string): string {
|
|
if (!plaintext) {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
const key = getEncryptionKey();
|
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
|
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
|
|
const encrypted = Buffer.concat([
|
|
cipher.update(plaintext, 'utf8'),
|
|
cipher.final()
|
|
]);
|
|
|
|
const tag = cipher.getAuthTag();
|
|
|
|
const encryptedData: EncryptedData = {
|
|
encrypted: encrypted.toString('base64'),
|
|
iv: iv.toString('base64'),
|
|
salt: salt.toString('base64'),
|
|
tag: tag.toString('base64'),
|
|
version: 1
|
|
};
|
|
|
|
// Return as base64 encoded JSON for easy storage
|
|
return Buffer.from(JSON.stringify(encryptedData)).toString('base64');
|
|
} catch (error) {
|
|
console.error('Encryption error:', error);
|
|
throw new Error('Failed to encrypt data');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypts encrypted data
|
|
* @param encryptedString The encrypted data string
|
|
* @returns Decrypted plaintext
|
|
*/
|
|
export function decrypt(encryptedString: string): string {
|
|
if (!encryptedString) {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
// Check if it's already plaintext (for backward compatibility during migration)
|
|
if (!isEncrypted(encryptedString)) {
|
|
return encryptedString;
|
|
}
|
|
|
|
const encryptedData: EncryptedData = JSON.parse(
|
|
Buffer.from(encryptedString, 'base64').toString('utf8')
|
|
);
|
|
|
|
const key = getEncryptionKey();
|
|
const iv = Buffer.from(encryptedData.iv, 'base64');
|
|
const tag = Buffer.from(encryptedData.tag, 'base64');
|
|
const encrypted = Buffer.from(encryptedData.encrypted, 'base64');
|
|
|
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
decipher.setAuthTag(tag);
|
|
|
|
const decrypted = Buffer.concat([
|
|
decipher.update(encrypted),
|
|
decipher.final()
|
|
]);
|
|
|
|
return decrypted.toString('utf8');
|
|
} catch (error) {
|
|
// If decryption fails, check if it's plaintext (backward compatibility)
|
|
try {
|
|
JSON.parse(Buffer.from(encryptedString, 'base64').toString('utf8'));
|
|
throw error; // It was encrypted but failed to decrypt
|
|
} catch {
|
|
// Not encrypted, return as-is for backward compatibility
|
|
console.warn('Token appears to be unencrypted, returning as-is for backward compatibility');
|
|
return encryptedString;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a string is encrypted
|
|
* @param value The string to check
|
|
* @returns true if encrypted, false otherwise
|
|
*/
|
|
export function isEncrypted(value: string): boolean {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const decoded = Buffer.from(value, 'base64').toString('utf8');
|
|
const data = JSON.parse(decoded);
|
|
return data.version === 1 && data.encrypted && data.iv && data.tag;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrates unencrypted tokens to encrypted format
|
|
* @param token The token to migrate
|
|
* @returns Encrypted token if it wasn't already encrypted
|
|
*/
|
|
export function migrateToken(token: string): string {
|
|
if (!token || isEncrypted(token)) {
|
|
return token;
|
|
}
|
|
|
|
return encrypt(token);
|
|
}
|
|
|
|
/**
|
|
* Generates a secure random token
|
|
* @param length Token length in bytes (default: 32)
|
|
* @returns Hex encoded random token
|
|
*/
|
|
export function generateSecureToken(length: number = 32): string {
|
|
return crypto.randomBytes(length).toString('hex');
|
|
}
|
|
|
|
/**
|
|
* Hashes a value using SHA-256 (for non-reversible values like API keys for comparison)
|
|
* @param value The value to hash
|
|
* @returns Hex encoded hash
|
|
*/
|
|
export function hashValue(value: string): string {
|
|
return crypto.createHash('sha256').update(value).digest('hex');
|
|
} |