mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-30 03:28:06 +03:00
Added Encryptions to All stored token and passwords
This commit is contained in:
169
src/lib/utils/encryption.ts
Normal file
169
src/lib/utils/encryption.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user