Skip to content

[Enhancement]: Migrate to AES-256-GCM #6473

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
1 task done
rubentalstra opened this issue Mar 22, 2025 · 2 comments
Open
1 task done

[Enhancement]: Migrate to AES-256-GCM #6473

rubentalstra opened this issue Mar 22, 2025 · 2 comments
Assignees
Labels
✨ enhancement New feature or request

Comments

@rubentalstra
Copy link
Collaborator

What features would you like to see added?

We’d like to add a new AES-256-GCM encryption (v4) method, because GCM provides both encryption and tamper protection.
This will help secure stored secrets (API keys, backup codes) while ensuring that any unauthorized modifications can be detected.

More details

  • Keep existing encryption functions (v1, v2, v3) for backward compatibility.
  • Add a new encryptV4 and decryptV4 using AES-256-GCM.
  • Use a random 16-byte IV each time and handle the GCM authTag properly.
  • Update relevant documentation and tests to show how to migrate to v4 encryption.
  • Potentially deprecate older methods in the future once v4 is widely adopted.
require('dotenv').config();
const crypto = require('node:crypto');
const { webcrypto } = crypto;

// Legacy keys for v1, v2, v3 (for demonstration only!)
const key = Buffer.from(process.env.CREDS_KEY, 'hex');
const iv = Buffer.from(process.env.CREDS_IV, 'hex');  // used for v1

// AES-CBC (v1/v2) and AES-256-CTR (v3) code ...
// -- existing code left as-is --

// --- v4: AES-256-GCM using Node's crypto functions ---
const algorithm_v4 = 'aes-256-gcm';

/**
 * Encrypts a value using AES-256-GCM (v4).
 *
 * @param {string} plaintext - The string to encrypt.
 * @returns {string} The encrypted string containing the IV, AuthTag, and ciphertext, joined by “:”, prefixed with “v4:”.
 */
function encryptV4(plaintext) {
  // Ensure key is 32 bytes for AES-256
  if (key.length !== 32) {
    throw new Error(`Invalid key length: expected 32 bytes, got ${key.length} bytes`);
  }
  
  // Generate random 16-byte IV
  const iv_v4 = crypto.randomBytes(16);

  // Create cipher
  const cipher = crypto.createCipheriv(algorithm_v4, key, iv_v4);

  // Encrypt
  const encrypted = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final()
  ]);

  // Retrieve the auth tag generated by GCM
  const authTag = cipher.getAuthTag();

  // Format: v4:iv:tag:ciphertext
  return `v4:${iv_v4.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
}

/**
 * Decrypts a value produced by `encryptV4`.
 *
 * @param {string} encryptedValue - String in the format “v4:ivHex:tagHex:ciphertextHex”.
 * @returns {string} Decrypted plaintext.
 */
function decryptV4(encryptedValue) {
  const parts = encryptedValue.split(':');
  if (parts[0] !== 'v4') {
    throw new Error('Not a v4 encrypted value');
  }

  const iv_v4 = Buffer.from(parts[1], 'hex');
  const authTag = Buffer.from(parts[2], 'hex');
  const ciphertext = Buffer.from(parts[3], 'hex');

  // Create decipher
  const decipher = crypto.createDecipheriv(algorithm_v4, key, iv_v4);

  // Set the auth tag *before* finalizing
  decipher.setAuthTag(authTag);

  // Decrypt
  const decrypted = Buffer.concat([
    decipher.update(ciphertext),
    decipher.final()
  ]);

  return decrypted.toString('utf8');
}

/**
 * Hash a given string with SHA-256.
 *
 * @param {string} str
 * @returns {Promise<string>} Returns a hex-encoded hash of the string.
 */
async function hashToken(str) {
  const data = new TextEncoder().encode(str);
  const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
  return Buffer.from(hashBuffer).toString('hex');
}

/**
 * Computes SHA-256 hash for a backup code (example usage).
 *
 * @param {string} input
 * @returns {Promise<string>}
 */
async function hashBackupCode(input) {
  const encoder = new TextEncoder();
  const data = encoder.encode(input);
  const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}

/**
 * Returns a string of random values (hex-encoded).
 *
 * @param {number} length
 * @returns {Promise<string>}
 */
async function getRandomValues(length) {
  if (!Number.isInteger(length) || length <= 0) {
    throw new Error('Length must be a positive integer');
  }
  const randomValues = new Uint8Array(length);
  webcrypto.getRandomValues(randomValues);
  return Buffer.from(randomValues).toString('hex');
}

// Exports: keep v1, v2, v3 for backward compatibility, add v4
module.exports = {
  // --- v1/v2 code below here (AES-CBC) ---
  encrypt,
  decrypt,
  encryptV2,
  decryptV2,

  // --- v3: AES-256-CTR ---
  encryptV3,
  decryptV3,

  // --- v4: AES-256-GCM ---
  encryptV4,
  decryptV4,

  // Hash & random
  hashToken,
  hashBackupCode,
  getRandomValues,
};

Which components are impacted by your request?

General, Other

Pictures

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct
@rubentalstra rubentalstra added the ✨ enhancement New feature or request label Mar 22, 2025
@rubentalstra rubentalstra self-assigned this Mar 22, 2025
@rubentalstra
Copy link
Collaborator Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants
@rubentalstra and others