bws-secure
Version:
Secure environment management with Bitwarden Secrets Manager
256 lines (225 loc) • 11.4 kB
JavaScript
import { execSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import dotenv from 'dotenv';
import logger from './logger.js';
// Get current script directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function ensureBwsInstalled() {
const bwsPath = './node_modules/.bin/bws';
const bwsExePath = './node_modules/.bin/bws.exe';
// Check if bws binary exists, install it if not
if (!fs.existsSync(bwsPath) && !fs.existsSync(bwsExePath)) {
logger.info('bws binary not found. Running bws-installer.sh...');
try {
// Use the correct path to bws-installer.sh
const installerPath = path.join(__dirname, 'bws-installer.sh');
// This works for both Unix systems and Git Bash on Windows
execSync(`sh ${installerPath}`, {
stdio: 'inherit',
shell: true
});
} catch (error) {
logger.error(`Failed to install bws: ${error.message}`);
throw error;
}
}
}
function encryptContent(plaintext, encryptionKey) {
const nonce = crypto.randomBytes(12); // 12 bytes is optimal for GCM
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(encryptionKey, 'hex'), nonce);
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag();
return `${nonce.toString('base64')}:${authTag.toString('base64')}:${encrypted}`;
}
function decryptContent(encrypted, encryptionKey) {
const [nonceBase64, authTagBase64, data] = encrypted.split(':');
const nonce = Buffer.from(nonceBase64, 'base64');
const authTag = Buffer.from(authTagBase64, 'base64');
const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(encryptionKey, 'hex'), nonce);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(data, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
function loadBwsSecrets(encryptionKey) {
// Always load from .env file first
const environmentConfig = dotenv.config();
const environmentToken = environmentConfig.parsed?.BWS_ACCESS_TOKEN;
// Override any existing token with the one from .env
if (environmentToken) {
process.env.BWS_ACCESS_TOKEN = environmentToken;
}
if (!process.env.BWS_ACCESS_TOKEN) {
// prettier-ignore
{
console.warn('\u001B[33m╔════════════════════════════════════════════════════════╗\u001B[0m');
console.warn('\u001B[33m║ ║\u001B[0m');
console.warn('\u001B[33m║ WARNING: BWS TOKEN MISSING ║\u001B[0m');
console.warn('\u001B[33m║ ║\u001B[0m');
console.warn('\u001B[33m║ To use BWS features: ║\u001B[0m');
console.warn('\u001B[33m║ 1. Log in to vault.bitwarden.com ║\u001B[0m');
console.warn('\u001B[33m║ 2. Go to Secrets Manager > Machine Accounts ║\u001B[0m');
console.warn('\u001B[33m║ 3. Create or copy your machine access token ║\u001B[0m');
console.warn('\u001B[33m║ 4. Add to .env: BWS_ACCESS_TOKEN=your_token ║\u001B[0m');
console.warn('\u001B[33m║ ║\u001B[0m');
console.warn('\u001B[33m║ For now, continuing with only .env values... ║\u001B[0m');
console.warn('\u001B[33m║ ║\u001B[0m');
console.warn('\u001B[33m╚════════════════════════════════════════════════════════╝\u001B[0m');
console.warn(
'\nVisit the link below to create your token: \n' +
'\nhttps://vault.bitwarden.com/#/sm/22479128-f194-460a-884b-b24a015686c6/machine-accounts\n'
);
}
return '';
}
try {
ensureBwsInstalled();
const mergedVariables = {};
// First, try to load global secrets (auth tokens)
try {
console.log('Debug: Loading global secrets...');
const output = execSync(
`./node_modules/.bin/bws secret list -t ${process.env.BWS_ACCESS_TOKEN} -o env`,
{
encoding: 'utf-8',
env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' }
}
);
// These are data processing operations, not command executions
const cleanOutput = output.replaceAll(/\u001B\[\d+m/g, '').trim();
const globalSecrets = cleanOutput.split('\n').reduce((accumulator, line) => {
const [key, value] = line.split('=');
if (key && value) {
accumulator[key] = value;
}
return accumulator;
}, {});
// Also just data processing
for (const { key, value } of globalSecrets) {
if (key === 'NETLIFY_AUTH_TOKEN' || key === 'VERCEL_AUTH_TOKEN') {
mergedVariables[key] = value;
console.log('Debug: Found auth token:', key);
}
}
} catch (globalError) {
console.warn('Warning: Failed to load global secrets:', globalError.message);
}
// Then, if we have a project ID, load project-specific secrets
if (process.env.BWS_PROJECT_ID) {
try {
console.log('Debug: Loading project secrets for:', process.env.BWS_PROJECT_ID);
// NOSONAR: BWS CLI execution with system-controlled variables - no user input
/* sonar-disable-next-line sonar:S4721 */
const projectOutput = execSync(
`./node_modules/.bin/bws secret list ${process.env.BWS_PROJECT_ID} -t ${process.env.BWS_ACCESS_TOKEN} -o env`,
{
encoding: 'utf-8',
env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' }
}
);
const projectSecrets = projectOutput.split('\n').reduce((accumulator, line) => {
const [key, value] = line.split('=');
if (key && value) {
accumulator[key] = value;
}
return accumulator;
}, {});
// More data processing
for (const { key, value } of projectSecrets) {
if (key && value) {
mergedVariables[key] = value;
}
}
console.log('Debug: Loaded project secrets:', Object.keys(mergedVariables).length);
} catch (projectError) {
console.warn('Warning: Failed to load project secrets:', projectError.message);
}
}
const environmentContent = Object.entries(mergedVariables)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
// Only create .env.secure if we have content
if (encryptionKey && environmentContent) {
try {
const cipherText = encryptContent(environmentContent, encryptionKey);
fs.writeFileSync('.env.secure', cipherText, { encoding: 'utf-8' });
console.log('Debug: Created .env.secure file');
// Add decryption output if debug and show_decrypted are enabled
if (process.env.DEBUG === 'true' && process.env.SHOW_DECRYPTED === 'true') {
console.log('\nDecrypted contents:');
console.log('-------------------------------------');
console.log(environmentContent);
console.log('-------------------------------------\n');
}
} catch (error) {
console.warn(`Failed to encrypt content: ${error.message}`);
}
}
return environmentContent;
} catch {
if (process.env.BWS_ACCESS_TOKEN) {
// prettier-ignore
{
console.error('\u001B[31m╔════════════════════════════════════════════════════════╗\u001B[0m');
console.error('\u001B[31m║ ║\u001B[0m');
console.error('\u001B[31m║ CRITICAL BWS TOKEN ERROR ║\u001B[0m');
console.error('\u001B[31m║ ║\u001B[0m');
console.error('\u001B[31m║ Your BWS_ACCESS_TOKEN appears to be invalid: ║\u001B[0m');
console.error('\u001B[31m║ 1. Check if token has expired ║\u001B[0m');
console.error('\u001B[31m║ 2. Verify token permissions in vault.bitwarden.com ║\u001B[0m');
console.error('\u001B[31m║ 3. Generate new token if needed ║\u001B[0m');
console.error('\u001B[31m║ 4. Ensure token has read access to required projects ║\u001B[0m');
console.error('\u001B[31m║ ║\u001B[0m');
console.error('\u001B[31m║ For now, continuing with only .env values... ║\u001B[0m');
console.error('\u001B[31m║ ║\u001B[0m');
console.error('\u001B[31m╚════════════════════════════════════════════════════════╝\u001B[0m');
console.error(
'\nVisit the link below to check or regenerate your token: \n' +
'\nhttps://vault.bitwarden.com/#/sm/22479128-f194-460a-884b-b24a015686c6/machine-accounts\n'
);
}
}
return '';
}
}
async function loadEnvironmentSecrets(environment, projectId) {
try {
// Create secure file path using project ID
const secureFile = path.join(process.cwd(), `.env.secure.${projectId}`);
console.log(`Loading secrets for project ID: ${projectId}...`);
// Set BWS_PROJECT_ID for this environment
process.env.BWS_PROJECT_ID = projectId;
// Load secrets from BWS
const environmentContent = loadBwsSecrets(process.env.BWS_EPHEMERAL_KEY);
if (environmentContent) {
// Write encrypted content to file
const cipherText = encryptContent(environmentContent, process.env.BWS_EPHEMERAL_KEY);
fs.writeFileSync(secureFile, cipherText);
// Show decrypted contents if debug and show_decrypted are enabled
if (process.env.DEBUG === 'true' && process.env.SHOW_DECRYPTED === 'true') {
console.log(`\nDecrypted contents of ${secureFile}:`);
console.log('-------------------------------------');
console.log(environmentContent);
console.log('-------------------------------------\n');
}
// Track created files for later use
const createdFiles = JSON.parse(process.env.BWS_CREATED_FILES || '[]');
if (!createdFiles.includes(projectId)) {
createdFiles.push(projectId);
process.env.BWS_CREATED_FILES = JSON.stringify(createdFiles);
}
console.log(`Created secure file: ${secureFile}`);
return true;
}
} catch (error) {
console.warn(`Warning: Failed to load secrets for project ID ${projectId}: ${error.message}`);
}
return false;
}
// Export the function rather than auto-running it
export { loadBwsSecrets, loadEnvironmentSecrets, ensureBwsInstalled };