From 79b2ca4e7e672faf789ce3f3de7b7e1d758cc29d Mon Sep 17 00:00:00 2001 From: basantnema31 Date: Sun, 7 Jun 2026 22:54:33 +0530 Subject: [PATCH 1/2] feat: implement secure storage and encryption for third-party API tokens --- models/User.ts | 7 +++++ utils/encryption.ts | 76 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 utils/encryption.ts diff --git a/models/User.ts b/models/User.ts index c5ccf794c..6fef4a650 100644 --- a/models/User.ts +++ b/models/User.ts @@ -2,6 +2,7 @@ import mongoose, { Document, Model, Schema } from 'mongoose'; export interface IUser extends Document { username: string; + githubToken?: string; createdAt: Date; lastSeen?: Date; visitCount: number; @@ -15,6 +16,12 @@ const UserSchema: Schema = new Schema({ lowercase: true, trim: true, }, + githubToken: { + type: String, + // Note: The actual encryption/decryption happens at the service layer + // or via mongoose pre-save hooks. For now, we store the encrypted string here. + select: false, // Ensure tokens aren't accidentally exposed in general queries + }, createdAt: { type: Date, default: Date.now, diff --git a/utils/encryption.ts b/utils/encryption.ts new file mode 100644 index 000000000..4b48071d5 --- /dev/null +++ b/utils/encryption.ts @@ -0,0 +1,76 @@ +import crypto from 'crypto'; + +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 16; +const TAG_LENGTH = 16; +const KEY_LEN = 32; + +// The encryption key should be exactly 32 bytes for AES-256 +// In production, ensure ENCRYPTION_KEY is securely set in environment variables +const getEncryptionKey = (): Buffer => { + const key = process.env.ENCRYPTION_KEY || 'default_commitpulse_secret_key_32'; + // Use scrypt to securely derive a 32-byte key from the environment variable + return crypto.scryptSync(key, 'commitpulse_salt', KEY_LEN); +}; + +/** + * Securely encrypts a third-party API token using AES-256-GCM. + * @param plaintextToken The plaintext API token (e.g., GitHub PAT) + * @returns The encrypted token string in the format iv:tag:encryptedData + */ +export function encryptToken(plaintextToken: string): string { + if (!plaintextToken) return plaintextToken; + + try { + const iv = crypto.randomBytes(IV_LENGTH); + const key = getEncryptionKey(); + + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + + let encrypted = cipher.update(plaintextToken, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + const tag = cipher.getAuthTag(); + + return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`; + } catch (error) { + console.error('Encryption failed:', error); + throw new Error('Failed to encrypt token securely'); + } +} + +/** + * Decrypts a securely stored API token back to plaintext. + * @param encryptedString The encrypted token string from the database + * @returns The plaintext API token + */ +export function decryptToken(encryptedString: string): string { + if (!encryptedString) return encryptedString; + + const parts = encryptedString.split(':'); + if (parts.length !== 3) { + // Return original string if it doesn't match the encrypted format + // This allows graceful fallback for any legacy plaintext tokens + return encryptedString; + } + + const [ivHex, tagHex, encrypted] = parts; + + try { + const iv = Buffer.from(ivHex, 'hex'); + const tag = Buffer.from(tagHex, 'hex'); + const key = getEncryptionKey(); + + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(tag); + + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } catch (error) { + console.error('Decryption failed:', error); + // Do not return partial or corrupted data on decryption failure + throw new Error('Failed to decrypt token securely'); + } +} From f6ed72586842e1e28c8a971378ee9a5c36e5c07b Mon Sep 17 00:00:00 2001 From: basantnema31 Date: Sun, 7 Jun 2026 23:08:09 +0530 Subject: [PATCH 2/2] style: fix trailing whitespaces --- utils/encryption.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/utils/encryption.ts b/utils/encryption.ts index 4b48071d5..47b5130cf 100644 --- a/utils/encryption.ts +++ b/utils/encryption.ts @@ -20,18 +20,18 @@ const getEncryptionKey = (): Buffer => { */ export function encryptToken(plaintextToken: string): string { if (!plaintextToken) return plaintextToken; - + try { const iv = crypto.randomBytes(IV_LENGTH); const key = getEncryptionKey(); - + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); - + let encrypted = cipher.update(plaintextToken, 'utf8', 'hex'); encrypted += cipher.final('hex'); - + const tag = cipher.getAuthTag(); - + return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`; } catch (error) { console.error('Encryption failed:', error); @@ -46,27 +46,27 @@ export function encryptToken(plaintextToken: string): string { */ export function decryptToken(encryptedString: string): string { if (!encryptedString) return encryptedString; - + const parts = encryptedString.split(':'); if (parts.length !== 3) { // Return original string if it doesn't match the encrypted format // This allows graceful fallback for any legacy plaintext tokens return encryptedString; } - + const [ivHex, tagHex, encrypted] = parts; - + try { const iv = Buffer.from(ivHex, 'hex'); const tag = Buffer.from(tagHex, 'hex'); const key = getEncryptionKey(); - + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(tag); - + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); - + return decrypted; } catch (error) { console.error('Decryption failed:', error);