mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-17 03:43:46 +03:00
Added Encryptions to All stored token and passwords
This commit is contained in:
@@ -11,6 +11,7 @@ import { httpPost, httpGet } from "./http-client";
|
||||
import { createMirrorJob } from "./helpers";
|
||||
import { db, organizations, repositories } from "./db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { decryptConfigTokens } from "./utils/config-encryption";
|
||||
|
||||
/**
|
||||
* Helper function to get organization configuration including destination override
|
||||
@@ -183,12 +184,15 @@ export const isRepoPresentInGitea = async ({
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
// Decrypt config tokens for API usage
|
||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||
|
||||
// Check if the repository exists at the specified owner location
|
||||
const response = await fetch(
|
||||
`${config.giteaConfig.url}/api/v1/repos/${owner}/${repoName}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -371,7 +375,7 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
service: "git",
|
||||
},
|
||||
{
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -480,7 +484,7 @@ export async function getOrCreateGiteaOrg({
|
||||
`${config.giteaConfig.url}/api/v1/orgs/${orgName}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
@@ -533,7 +537,7 @@ export async function getOrCreateGiteaOrg({
|
||||
const createRes = await fetch(`${config.giteaConfig.url}/api/v1/orgs`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -720,7 +724,7 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
private: repository.isPrivate,
|
||||
},
|
||||
{
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1074,6 +1078,9 @@ export const syncGiteaRepo = async ({
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
// Decrypt config tokens for API usage
|
||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||
|
||||
console.log(`Syncing repository ${repository.name}`);
|
||||
|
||||
// Mark repo as "syncing" in DB
|
||||
@@ -1200,6 +1207,9 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
||||
throw new Error("Missing GitHub or Gitea configuration.");
|
||||
}
|
||||
|
||||
// Decrypt config tokens for API usage
|
||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||
|
||||
const [owner, repo] = repository.fullName.split("/");
|
||||
|
||||
// Fetch GitHub issues
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createGitHubClient } from './github';
|
||||
import { processWithResilience } from './utils/concurrency';
|
||||
import { repositoryVisibilityEnum, repoStatusEnum } from '@/types/Repository';
|
||||
import type { Repository } from './db/schema';
|
||||
import { getDecryptedGitHubToken } from './utils/config-encryption';
|
||||
|
||||
// Recovery state tracking
|
||||
let recoveryInProgress = false;
|
||||
@@ -262,7 +263,8 @@ async function recoverMirrorJob(job: any, remainingItemIds: string[]) {
|
||||
// Create GitHub client with error handling
|
||||
let octokit;
|
||||
try {
|
||||
octokit = createGitHubClient(config.githubConfig.token);
|
||||
const decryptedToken = getDecryptedGitHubToken(config);
|
||||
octokit = createGitHubClient(decryptedToken);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create GitHub client: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
52
src/lib/utils/config-encryption.ts
Normal file
52
src/lib/utils/config-encryption.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { decrypt } from "./encryption";
|
||||
import type { Config } from "@/types/config";
|
||||
|
||||
/**
|
||||
* Decrypts tokens in a config object for use in API calls
|
||||
* @param config The config object with potentially encrypted tokens
|
||||
* @returns Config object with decrypted tokens
|
||||
*/
|
||||
export function decryptConfigTokens(config: Config): Config {
|
||||
const decryptedConfig = { ...config };
|
||||
|
||||
// Deep clone the config objects
|
||||
if (config.githubConfig) {
|
||||
decryptedConfig.githubConfig = { ...config.githubConfig };
|
||||
if (config.githubConfig.token) {
|
||||
decryptedConfig.githubConfig.token = decrypt(config.githubConfig.token);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.giteaConfig) {
|
||||
decryptedConfig.giteaConfig = { ...config.giteaConfig };
|
||||
if (config.giteaConfig.token) {
|
||||
decryptedConfig.giteaConfig.token = decrypt(config.giteaConfig.token);
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a decrypted GitHub token from config
|
||||
* @param config The config object
|
||||
* @returns Decrypted GitHub token
|
||||
*/
|
||||
export function getDecryptedGitHubToken(config: Config): string {
|
||||
if (!config.githubConfig?.token) {
|
||||
throw new Error("GitHub token not found in config");
|
||||
}
|
||||
return decrypt(config.githubConfig.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a decrypted Gitea token from config
|
||||
* @param config The config object
|
||||
* @returns Decrypted Gitea token
|
||||
*/
|
||||
export function getDecryptedGiteaToken(config: Config): string {
|
||||
if (!config.giteaConfig?.token) {
|
||||
throw new Error("Gitea token not found in config");
|
||||
}
|
||||
return decrypt(config.giteaConfig.token);
|
||||
}
|
||||
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