Skip to content
Merged
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
18 changes: 13 additions & 5 deletions src/Commands/SecretsSetup.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,15 @@ protected function configure(): void
With an argument, stores the provided key on this node.
Example: protocol secrets:setup "your-hex-key-here"

The storage location is determined by the environment set via
config:env. Production environments store the key globally in
protocol's .node/key. All other environments (local, dev, staging)
store the key per-project in the current working directory's
.protocol/key.

The key is also read automatically from the PROTOCOL_ENCRYPTION_KEY
environment variable, so CI/CD workflows can pass it from GitHub secrets.

The key is stored at ~/.protocol/.node/key with 0600 permissions.

HELP)
;
$this
Expand All @@ -86,8 +90,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}

$keyPath = Secrets::keyPath();
$scope = Secrets::isGlobal() ? 'global' : 'project';
$output->writeln("<comment>Environment scope: {$scope}</comment>");

if (Secrets::hasKey() && !$providedKey) {
$output->writeln('<comment>Encryption key already exists at: ' . Secrets::keyPath() . '</comment>');
$output->writeln('<comment>Encryption key already exists at: ' . $keyPath . '</comment>');
$output->writeln('To replace it, delete the existing key first.');
return Command::SUCCESS;
}
Expand All @@ -101,7 +109,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

if (Secrets::storeKey($providedKey)) {
$output->writeln('<info>Encryption key stored at: ' . Secrets::keyPath() . '</info>');
$output->writeln('<info>Encryption key stored at: ' . $keyPath . '</info>');
} else {
$output->writeln('<error>Failed to store encryption key.</error>');
return Command::FAILURE;
Expand All @@ -117,7 +125,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output->writeln('<comment>Copy this key to other nodes:</comment>');
$output->writeln(" protocol secrets:setup \"{$hexKey}\"");
$output->writeln('');
$output->writeln('Stored at: ' . Secrets::keyPath());
$output->writeln('Stored at: ' . $keyPath);
} else {
$output->writeln('<error>Failed to generate encryption key.</error>');
return Command::FAILURE;
Expand Down
59 changes: 55 additions & 4 deletions src/Helpers/Secrets.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,33 @@ class Secrets
const NONCE_LENGTH = 12;
const TAG_LENGTH = 16;

/**
* Determine whether secrets should be stored globally (production)
* or per-project (all other environments).
*/
public static function isGlobal(): bool
{
return !IncidentDetector::isDev();
}

/**
* Path to the project-local .protocol directory.
*/
public static function projectDataDir(): string
{
return WORKING_DIR . '.protocol' . DIRECTORY_SEPARATOR;
}

/**
* Path to the encryption key file.
* Production stores globally in NODE_DATA_DIR, all other envs store per-project.
*/
public static function keyPath(): string
{
return NODE_DATA_DIR . 'key';
if (self::isGlobal()) {
return NODE_DATA_DIR . 'key';
}
return self::projectDataDir() . 'key';
}

/**
Expand All @@ -37,17 +58,24 @@ public static function generateKey(): string

/**
* Store the encryption key to disk.
* Location is determined by the current environment (config:env).
*/
public static function storeKey(string $hexKey): bool
{
$dir = dirname(self::keyPath());
$path = self::keyPath();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0700, true);
}
$written = file_put_contents(self::keyPath(), $hexKey);
$written = file_put_contents($path, $hexKey);
if ($written !== false) {
chmod(self::keyPath(), 0600);
chmod($path, 0600);
}

if (!self::isGlobal()) {
self::ensureGitignore();
}

return $written !== false;
}

Expand Down Expand Up @@ -167,4 +195,27 @@ public static function decryptToTempFile(string $inputPath): ?string
chmod($tmpFile, 0600);
return $tmpFile;
}

/**
* Ensure .protocol/ is listed in the project's .gitignore.
*/
public static function ensureGitignore(): void
{
$gitignorePath = WORKING_DIR . '.gitignore';
$entry = '.protocol/';

if (is_file($gitignorePath)) {
$contents = file_get_contents($gitignorePath);
// Check if already present (exact line match)
$lines = array_map('trim', explode("\n", $contents));
if (in_array($entry, $lines)) {
return;
}
// Append with a newline if file doesn't end with one
$append = (substr($contents, -1) === "\n" ? '' : "\n") . $entry . "\n";
file_put_contents($gitignorePath, $contents . $append);
} else {
file_put_contents($gitignorePath, $entry . "\n");
}
}
}
Loading