Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 26 additions & 42 deletions src/lib/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;
const KEY_ERROR_MESSAGE =
"ENCRYPTION_KEY env var must be a 32-byte hex string";
const ENCRYPTED_TOKEN_ERROR_MESSAGE =
"Encrypted token must be a hex string containing ciphertext and auth tag";
const IV_ERROR_MESSAGE = "Token IV must be a 12-byte hex string";

function getEncryptionKey(): Buffer {
const key = process.env.ENCRYPTION_KEY;
Expand Down Expand Up @@ -46,52 +49,33 @@ export function encryptToken(plaintext: string): {
};
}

export function decryptToken(encrypted: string, iv: string): string | null {
const key = getEncryptionKey();

export function decryptToken(
encrypted: string,
iv: string
): string | null {
try {
const key = getEncryptionKey();

if (!/^[0-9a-fA-F]*$/.test(encrypted) || encrypted.length % 2 !== 0) {
throw new Error("Invalid encrypted token format");
}

if (!/^[0-9a-fA-F]*$/.test(iv) || iv.length % 2 !== 0) {
throw new Error("Invalid IV format");
}

const encryptedBuffer = Buffer.from(encrypted, "hex");
const ivBuffer = Buffer.from(iv, "hex");

if (ivBuffer.length !== IV_LENGTH) {
throw new Error("Invalid IV length");
}

if (encryptedBuffer.length < AUTH_TAG_LENGTH + 1) {
throw new Error("Encrypted token too short");
}
if (!/^[0-9a-fA-F]+$/.test(encrypted) || encrypted.length <= AUTH_TAG_LENGTH * 2) {
return null;
}

const ciphertext = encryptedBuffer.subarray(
0,
encryptedBuffer.length - AUTH_TAG_LENGTH
);
if (!/^[0-9a-fA-F]{24}$/.test(iv)) {
return null;
}

const authTag = encryptedBuffer.subarray(
encryptedBuffer.length - AUTH_TAG_LENGTH
);
const encryptedBuffer = Buffer.from(encrypted, "hex");
const ivBuffer = Buffer.from(iv, "hex");

const decipher = createDecipheriv(ALGORITHM, key, ivBuffer);
const ciphertext = encryptedBuffer.subarray(
0,
encryptedBuffer.length - AUTH_TAG_LENGTH
);
const authTag = encryptedBuffer.subarray(
encryptedBuffer.length - AUTH_TAG_LENGTH
);

decipher.setAuthTag(authTag);
const decipher = createDecipheriv(ALGORITHM, key, ivBuffer);
decipher.setAuthTag(authTag);

return Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]).toString("utf8");
} catch (error) {
console.error("Token decryption failed:", error);
return null;
}
return Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]).toString("utf8");
}
25 changes: 6 additions & 19 deletions src/lib/github-accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function getLinkedTokens(userId: string): Promise<string[]> {

const rows = (data ?? []) as UserGitHubAccountRow[];

return rows
return rows
.map((row) =>
decryptToken(row.access_token_encrypted, row.access_token_iv)
)
Expand Down Expand Up @@ -113,24 +113,11 @@ export async function getLinkedAccounts(

const rows = (data ?? []) as UserGitHubAccountRow[];

return rows
.map((row) => {
const token = decryptToken(
row.access_token_encrypted,
row.access_token_iv
);

if (!token) {
return null;
}

return {
githubId: row.github_id ?? "",
githubLogin: row.github_login ?? "",
token,
};
})
.filter((account): account is LinkedAccount => account !== null);
return rows.map((row) => ({
githubId: row.github_id ?? "",
githubLogin: row.github_login ?? "",
token: decryptToken(row.access_token_encrypted, row.access_token_iv),
})).filter((account): account is LinkedAccount => account.token !== null);
}

export async function getAllAccounts(
Expand Down
Loading