mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 18:33:15 +00:00
132 lines
4 KiB
TypeScript
132 lines
4 KiB
TypeScript
import * as crypto from "node:crypto";
|
|
|
|
/**
|
|
* Encryption utility for GitHub tokens using AES-256-GCM
|
|
*
|
|
* This module provides secure encryption and decryption of GitHub access tokens
|
|
* using AES-256-GCM encryption with authenticated encryption.
|
|
*/
|
|
|
|
const ALGORITHM = "aes-256-gcm";
|
|
const IV_LENGTH = 12; // 96 bits (Standard for GCM)
|
|
const TAG_LENGTH = 16; // 128 bits
|
|
|
|
/**
|
|
* Derives a 256-bit key from the provided encryption key string
|
|
* Uses SHA-256 to ensure consistent key length
|
|
*/
|
|
function deriveKey(encryptionKey: string): Buffer {
|
|
return crypto.createHash("sha256").update(encryptionKey).digest();
|
|
}
|
|
|
|
/**
|
|
* Encrypts a GitHub token using AES-256-GCM
|
|
*
|
|
* @param token - The GitHub access token to encrypt
|
|
* @param encryptionKey - The encryption key (will be hashed to 256 bits)
|
|
* @returns Base64 encoded encrypted data containing IV, encrypted token, and auth tag
|
|
* @throws Error if encryption fails or inputs are invalid
|
|
*/
|
|
export function encryptGitHubToken(
|
|
token: string,
|
|
encryptionKey: string,
|
|
): string {
|
|
if (!token || typeof token !== "string") {
|
|
throw new Error("Token must be a non-empty string");
|
|
}
|
|
|
|
if (!encryptionKey || typeof encryptionKey !== "string") {
|
|
throw new Error("Encryption key must be a non-empty string");
|
|
}
|
|
|
|
try {
|
|
// Generate a random IV for each encryption (12 bytes for GCM)
|
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
|
|
// Derive the encryption key
|
|
const key = deriveKey(encryptionKey);
|
|
|
|
// Create cipher
|
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
|
|
// Encrypt the token
|
|
const encryptedBuffer = Buffer.concat([
|
|
cipher.update(token, "utf8"),
|
|
cipher.final(),
|
|
]);
|
|
|
|
// Get the authentication tag
|
|
const tag = cipher.getAuthTag();
|
|
|
|
// Combine IV, encrypted data, and tag into a single base64 string
|
|
// Format: IV (12 bytes) + EncryptedData + AuthTag (16 bytes)
|
|
const combined = Buffer.concat([iv, encryptedBuffer, tag]);
|
|
return combined.toString("base64");
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Failed to encrypt token: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypts a GitHub token using AES-256-GCM
|
|
*
|
|
* @param encryptedToken - Base64 encoded encrypted data from encryptGitHubToken
|
|
* @param encryptionKey - The encryption key used for encryption
|
|
* @returns The decrypted GitHub access token
|
|
* @throws Error if decryption fails or inputs are invalid
|
|
*/
|
|
export function decryptGitHubToken(
|
|
encryptedToken: string,
|
|
encryptionKey: string,
|
|
): string {
|
|
if (!encryptedToken || typeof encryptedToken !== "string") {
|
|
throw new Error("Encrypted token must be a non-empty string");
|
|
}
|
|
|
|
if (!encryptionKey || typeof encryptionKey !== "string") {
|
|
throw new Error("Encryption key must be a non-empty string");
|
|
}
|
|
|
|
try {
|
|
// Decode the combined data
|
|
const combined = Buffer.from(encryptedToken, "base64");
|
|
|
|
// Minimum length: IV_LENGTH + TAG_LENGTH + 1 byte for data
|
|
if (combined.length < IV_LENGTH + TAG_LENGTH + 1) {
|
|
throw new Error("Invalid encrypted token format: too short or malformed");
|
|
}
|
|
|
|
// Extract IV, encrypted data, and tag
|
|
// IV is first IV_LENGTH bytes
|
|
// AuthTag is last TAG_LENGTH bytes
|
|
// Encrypted data is in between
|
|
const iv = combined.subarray(0, IV_LENGTH);
|
|
const tag = combined.subarray(combined.length - TAG_LENGTH);
|
|
const encrypted = combined.subarray(
|
|
IV_LENGTH,
|
|
combined.length - TAG_LENGTH,
|
|
);
|
|
|
|
// Derive the encryption key
|
|
const key = deriveKey(encryptionKey);
|
|
|
|
// Create decipher
|
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
decipher.setAuthTag(tag);
|
|
|
|
// Decrypt the token
|
|
// 'encrypted' is a Buffer, so no input encoding is specified for update()
|
|
const decryptedBuffer = Buffer.concat([
|
|
decipher.update(encrypted),
|
|
decipher.final(),
|
|
]);
|
|
|
|
return decryptedBuffer.toString("utf8");
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Failed to decrypt token: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
}
|