Skip to content

Commit beedbaf

Browse files
committed
Added Encryptions to All stored token and passwords
1 parent 7cc4aa8 commit beedbaf

File tree

14 files changed

+475
-24
lines changed

14 files changed

+475
-24
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ DATABASE_URL=sqlite://data/gitea-mirror.db
1212
# Security
1313
BETTER_AUTH_SECRET=change-this-to-a-secure-random-string-in-production
1414
BETTER_AUTH_URL=http://localhost:4321
15+
# ENCRYPTION_SECRET=optional-encryption-key-for-token-encryption # Generate with: openssl rand -base64 48
1516

1617
# Optional GitHub/Gitea Mirror Configuration (for docker-compose, can also be set via web UI)
1718
# Uncomment and set as needed. These are passed as environment variables to the container.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ certs/*.crt
3131
certs/*.pem
3232
certs/*.cer
3333
!certs/README.md
34+
35+
# Hosted version documentation (local only)
36+
docs/HOSTED_VERSION.md

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,25 @@ bun run build
199199
- **APIs**: GitHub (Octokit), Gitea REST API
200200
- **Auth**: JWT tokens with bcryptjs password hashing
201201

202+
## Security
203+
204+
### Token Encryption
205+
- All GitHub and Gitea API tokens are encrypted at rest using AES-256-GCM
206+
- Encryption is automatic and transparent to users
207+
- Set `ENCRYPTION_SECRET` environment variable for production deployments
208+
- Falls back to `BETTER_AUTH_SECRET` or `JWT_SECRET` if not set
209+
210+
### Password Security
211+
- User passwords are hashed using bcrypt (via Better Auth)
212+
- Never stored in plaintext
213+
- Secure session management with JWT tokens
214+
215+
### Migration
216+
If upgrading from a version without token encryption:
217+
```bash
218+
bun run migrate:encrypt-tokens
219+
```
220+
202221
## Contributing
203222

204223
Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"db:check": "bun drizzle-kit check",
2424
"db:studio": "bun drizzle-kit studio",
2525
"migrate:better-auth": "bun scripts/migrate-to-better-auth.ts",
26+
"migrate:encrypt-tokens": "bun scripts/migrate-tokens-encryption.ts",
2627
"startup-recovery": "bun scripts/startup-recovery.ts",
2728
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
2829
"test-recovery": "bun scripts/test-recovery.ts",
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Migration script to encrypt existing GitHub and Gitea tokens in the database
4+
* Run with: bun run scripts/migrate-tokens-encryption.ts
5+
*/
6+
7+
import { db, configs } from "../src/lib/db";
8+
import { eq } from "drizzle-orm";
9+
import { encrypt, isEncrypted, migrateToken } from "../src/lib/utils/encryption";
10+
11+
async function migrateTokens() {
12+
console.log("Starting token encryption migration...");
13+
14+
try {
15+
// Fetch all configs
16+
const allConfigs = await db.select().from(configs);
17+
18+
console.log(`Found ${allConfigs.length} configurations to check`);
19+
20+
let migratedCount = 0;
21+
let skippedCount = 0;
22+
let errorCount = 0;
23+
24+
for (const config of allConfigs) {
25+
try {
26+
let githubUpdated = false;
27+
let giteaUpdated = false;
28+
29+
// Parse configs
30+
const githubConfig = typeof config.githubConfig === "string"
31+
? JSON.parse(config.githubConfig)
32+
: config.githubConfig;
33+
34+
const giteaConfig = typeof config.giteaConfig === "string"
35+
? JSON.parse(config.giteaConfig)
36+
: config.giteaConfig;
37+
38+
// Check and migrate GitHub token
39+
if (githubConfig.token) {
40+
if (!isEncrypted(githubConfig.token)) {
41+
console.log(`Encrypting GitHub token for config ${config.id} (user: ${config.userId})`);
42+
githubConfig.token = encrypt(githubConfig.token);
43+
githubUpdated = true;
44+
} else {
45+
console.log(`GitHub token already encrypted for config ${config.id}`);
46+
}
47+
}
48+
49+
// Check and migrate Gitea token
50+
if (giteaConfig.token) {
51+
if (!isEncrypted(giteaConfig.token)) {
52+
console.log(`Encrypting Gitea token for config ${config.id} (user: ${config.userId})`);
53+
giteaConfig.token = encrypt(giteaConfig.token);
54+
giteaUpdated = true;
55+
} else {
56+
console.log(`Gitea token already encrypted for config ${config.id}`);
57+
}
58+
}
59+
60+
// Update config if any tokens were migrated
61+
if (githubUpdated || giteaUpdated) {
62+
await db
63+
.update(configs)
64+
.set({
65+
githubConfig,
66+
giteaConfig,
67+
updatedAt: new Date(),
68+
})
69+
.where(eq(configs.id, config.id));
70+
71+
migratedCount++;
72+
console.log(`✓ Config ${config.id} updated successfully`);
73+
} else {
74+
skippedCount++;
75+
}
76+
77+
} catch (error) {
78+
errorCount++;
79+
console.error(`✗ Error processing config ${config.id}:`, error);
80+
}
81+
}
82+
83+
console.log("\n=== Migration Summary ===");
84+
console.log(`Total configs: ${allConfigs.length}`);
85+
console.log(`Migrated: ${migratedCount}`);
86+
console.log(`Skipped (already encrypted): ${skippedCount}`);
87+
console.log(`Errors: ${errorCount}`);
88+
89+
if (errorCount > 0) {
90+
console.error("\n⚠️ Some configs failed to migrate. Please check the errors above.");
91+
process.exit(1);
92+
} else {
93+
console.log("\n✅ Token encryption migration completed successfully!");
94+
}
95+
96+
} catch (error) {
97+
console.error("Fatal error during migration:", error);
98+
process.exit(1);
99+
}
100+
}
101+
102+
// Verify environment setup
103+
function verifyEnvironment() {
104+
const requiredEnvVars = ["ENCRYPTION_SECRET", "JWT_SECRET", "BETTER_AUTH_SECRET"];
105+
const availableSecrets = requiredEnvVars.filter(varName => process.env[varName]);
106+
107+
if (availableSecrets.length === 0) {
108+
console.error("❌ No encryption secret found!");
109+
console.error("Please set one of the following environment variables:");
110+
console.error(" - ENCRYPTION_SECRET (recommended)");
111+
console.error(" - JWT_SECRET");
112+
console.error(" - BETTER_AUTH_SECRET");
113+
process.exit(1);
114+
}
115+
116+
console.log(`Using encryption secret from: ${availableSecrets[0]}`);
117+
}
118+
119+
// Main execution
120+
async function main() {
121+
console.log("=== Gitea Mirror Token Encryption Migration ===\n");
122+
123+
// Verify environment
124+
verifyEnvironment();
125+
126+
// Run migration
127+
await migrateTokens();
128+
129+
process.exit(0);
130+
}
131+
132+
main().catch((error) => {
133+
console.error("Unexpected error:", error);
134+
process.exit(1);
135+
});

src/lib/gitea.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { httpPost, httpGet } from "./http-client";
1111
import { createMirrorJob } from "./helpers";
1212
import { db, organizations, repositories } from "./db";
1313
import { eq, and } from "drizzle-orm";
14+
import { decryptConfigTokens } from "./utils/config-encryption";
1415

1516
/**
1617
* Helper function to get organization configuration including destination override
@@ -183,12 +184,15 @@ export const isRepoPresentInGitea = async ({
183184
throw new Error("Gitea config is required.");
184185
}
185186

187+
// Decrypt config tokens for API usage
188+
const decryptedConfig = decryptConfigTokens(config as Config);
189+
186190
// Check if the repository exists at the specified owner location
187191
const response = await fetch(
188192
`${config.giteaConfig.url}/api/v1/repos/${owner}/${repoName}`,
189193
{
190194
headers: {
191-
Authorization: `token ${config.giteaConfig.token}`,
195+
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
192196
},
193197
}
194198
);
@@ -371,7 +375,7 @@ export const mirrorGithubRepoToGitea = async ({
371375
service: "git",
372376
},
373377
{
374-
Authorization: `token ${config.giteaConfig.token}`,
378+
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
375379
}
376380
);
377381

@@ -480,7 +484,7 @@ export async function getOrCreateGiteaOrg({
480484
`${config.giteaConfig.url}/api/v1/orgs/${orgName}`,
481485
{
482486
headers: {
483-
Authorization: `token ${config.giteaConfig.token}`,
487+
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
484488
"Content-Type": "application/json",
485489
},
486490
}
@@ -533,7 +537,7 @@ export async function getOrCreateGiteaOrg({
533537
const createRes = await fetch(`${config.giteaConfig.url}/api/v1/orgs`, {
534538
method: "POST",
535539
headers: {
536-
Authorization: `token ${config.giteaConfig.token}`,
540+
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
537541
"Content-Type": "application/json",
538542
},
539543
body: JSON.stringify({
@@ -720,7 +724,7 @@ export async function mirrorGitHubRepoToGiteaOrg({
720724
private: repository.isPrivate,
721725
},
722726
{
723-
Authorization: `token ${config.giteaConfig.token}`,
727+
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
724728
}
725729
);
726730

@@ -1074,6 +1078,9 @@ export const syncGiteaRepo = async ({
10741078
throw new Error("Gitea config is required.");
10751079
}
10761080

1081+
// Decrypt config tokens for API usage
1082+
const decryptedConfig = decryptConfigTokens(config as Config);
1083+
10771084
console.log(`Syncing repository ${repository.name}`);
10781085

10791086
// Mark repo as "syncing" in DB
@@ -1200,6 +1207,9 @@ export const mirrorGitRepoIssuesToGitea = async ({
12001207
throw new Error("Missing GitHub or Gitea configuration.");
12011208
}
12021209

1210+
// Decrypt config tokens for API usage
1211+
const decryptedConfig = decryptConfigTokens(config as Config);
1212+
12031213
const [owner, repo] = repository.fullName.split("/");
12041214

12051215
// Fetch GitHub issues

src/lib/recovery.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { createGitHubClient } from './github';
1111
import { processWithResilience } from './utils/concurrency';
1212
import { repositoryVisibilityEnum, repoStatusEnum } from '@/types/Repository';
1313
import type { Repository } from './db/schema';
14+
import { getDecryptedGitHubToken } from './utils/config-encryption';
1415

1516
// Recovery state tracking
1617
let recoveryInProgress = false;
@@ -262,7 +263,8 @@ async function recoverMirrorJob(job: any, remainingItemIds: string[]) {
262263
// Create GitHub client with error handling
263264
let octokit;
264265
try {
265-
octokit = createGitHubClient(config.githubConfig.token);
266+
const decryptedToken = getDecryptedGitHubToken(config);
267+
octokit = createGitHubClient(decryptedToken);
266268
} catch (error) {
267269
throw new Error(`Failed to create GitHub client: ${error instanceof Error ? error.message : String(error)}`);
268270
}

src/lib/utils/config-encryption.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { decrypt } from "./encryption";
2+
import type { Config } from "@/types/config";
3+
4+
/**
5+
* Decrypts tokens in a config object for use in API calls
6+
* @param config The config object with potentially encrypted tokens
7+
* @returns Config object with decrypted tokens
8+
*/
9+
export function decryptConfigTokens(config: Config): Config {
10+
const decryptedConfig = { ...config };
11+
12+
// Deep clone the config objects
13+
if (config.githubConfig) {
14+
decryptedConfig.githubConfig = { ...config.githubConfig };
15+
if (config.githubConfig.token) {
16+
decryptedConfig.githubConfig.token = decrypt(config.githubConfig.token);
17+
}
18+
}
19+
20+
if (config.giteaConfig) {
21+
decryptedConfig.giteaConfig = { ...config.giteaConfig };
22+
if (config.giteaConfig.token) {
23+
decryptedConfig.giteaConfig.token = decrypt(config.giteaConfig.token);
24+
}
25+
}
26+
27+
return decryptedConfig;
28+
}
29+
30+
/**
31+
* Gets a decrypted GitHub token from config
32+
* @param config The config object
33+
* @returns Decrypted GitHub token
34+
*/
35+
export function getDecryptedGitHubToken(config: Config): string {
36+
if (!config.githubConfig?.token) {
37+
throw new Error("GitHub token not found in config");
38+
}
39+
return decrypt(config.githubConfig.token);
40+
}
41+
42+
/**
43+
* Gets a decrypted Gitea token from config
44+
* @param config The config object
45+
* @returns Decrypted Gitea token
46+
*/
47+
export function getDecryptedGiteaToken(config: Config): string {
48+
if (!config.giteaConfig?.token) {
49+
throw new Error("Gitea token not found in config");
50+
}
51+
return decrypt(config.giteaConfig.token);
52+
}

0 commit comments

Comments
 (0)