PHP Classes

File: src/Commands/Keygen.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   Airship barge   src/Commands/Keygen.php   Download  
File: src/Commands/Keygen.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Airship barge
Build extensions for the Airship CMS
Author: By
Last change:
Date: 5 years ago
Size: 14,041 bytes
 

Contents

Class file image Download
<?php declare(strict_types=1); namespace Airship\Barge\Commands; use \Airship\Barge as Base; use \ParagonIE\ConstantTime\Base64UrlSafe; use \ZxcvbnPhp\Zxcvbn; use \ParagonIE\Halite\KeyFactory; use \ParagonIE\Halite\Asymmetric\Crypto as Asymmetric; class Keygen extends Base\Command { public $essential = false; public $name = 'Key Generator'; public $description = 'Generate a new signing key.'; public $display = 4; /** * Execute the keygen command * * @param array $args - CLI arguments * @echo * @return null * @throws \Error */ public function fire(array $args = []) { if (count($this->config['suppliers']) === 1) { $supplier = \count($args) > 0 ? $args[0] : \array_keys($this->config['suppliers'])[0]; } else { $supplier = \count($args) > 0 ? $args[0] : $this->prompt("Please enter the name of the supplier: "); } if (!\array_key_exists($supplier, $this->config['suppliers'])) { echo 'Please authenticate before attempting to generate a key.', "\n"; echo 'Run this command: ', $this->c['yellow'], 'barge login', $this->c[''], "\n"; exit(255); } if (\count($this->config['suppliers'][$supplier]['signing_keys']) === 0) { // Your first key is a master key; always. $has_master = false; $key_type = 'master'; } else { $has_master = true; echo 'Please enter the key type you would like to generate (master, signing).', "\n"; do { $key_type = $this->prompt('Key type: '); switch ($key_type) { case 'm': case 'main': case 'master': case 'primary': $key_type = 'master'; break; case 's': case 'secondary': case 'sub': case 'subkey': case 'signing': $key_type = 'signing'; break; default: echo 'Acceptable key types: master, signing', "\n"; $key_type = null; } } while (empty($key_type)); } // Each key gets its own unique Argon2 salt echo 'Generating a unique salt...', "\n"; $salt = \random_bytes(\Sodium\CRYPTO_PWHASH_SALTBYTES); $store_in_cloud = null; // This is optional and not recommended, but some people prefer convenience. // We really hope this is adequate information to make an informed choice // based on personal risk tolerance: echo 'Do you wish to store the salt for generating your signing key in the Skyport?', "\n"; echo 'This is a security-convenience trade-off. The default is NO.', "\n\n"; echo $this->c['green'], 'Pro:', $this->c[''], ' It\'s there if you need it, and the salt alone is not enough for us to', "\n", ' reproduce your signing key.', "\n"; echo $this->c['red'], 'Con:', $this->c[''], ' If your salt is stored online, the security of your signing key depends', "\n", ' entirely on your password.', "\n\n"; // Iterate until we get a valid response while ($store_in_cloud === null) { $choice = $this->prompt('Store salt in the Skyport? (y/N): '); switch ($choice) { case 'YES': case 'yes': case 'Y': case 'y': $store_in_cloud = true; break; case 'N': case 'NO': case 'n': case 'no': case '': // Just pressing enter means "don't store it"! $store_in_cloud = false; break; default: echo "\n", $this->c['yellow'], 'Invalid response. Please enter yes or no.', $this->c[''], "\n"; } } $zxcvbn = new Zxcvbn(); $userInput = $this->getZxcvbnKeywords($supplier); // If we're storing in the cloud, our standards should be much higher. $min_score = $store_in_cloud ? 3 : 2; do { // Next, let's get a password. echo 'Please enter a strong passphrase to use for your signing key.', "\n"; $password = $this->silentPrompt("Passphrase:"); $password2 = $this->silentPrompt("Confirm passphrase:"); if (!\hash_equals($password, $password2)) { unset($password); echo $this->c['red'], 'Passwords did not match!', $this->c[''], "\n"; continue; } // Use zxcvbn to assess password strength $strength = $zxcvbn->passwordStrength($password, $userInput); if ($strength['score'] < $min_score) { echo $this->c['yellow'], 'Sorry, that password is not strong enough. Try making ', 'your password longer and use a wider variety of characters.', $this->c[''], "\n"; $password = false; } } while (empty($password)); echo 'Generating signing key...'; if ($key_type === 'master') { // Master keys are treated as sensitive. $sign_level = KeyFactory::SENSITIVE; } else { // Signing keys (day-to-day) are still moderately sensitive. // We're using a KDF locally so we don't have DDoS concerns // (which usually calls for INTERACTIVE). $sign_level = KeyFactory::MODERATE; } $keyPair = KeyFactory::deriveSignatureKeyPair( $password, $salt, false, $sign_level ); $sign_public = $keyPair->getPublicKey(); echo 'DONE!', "\n"; // Wipe the password from memory \Sodium\memzero($password); // Store this in the configuration $new_key = [ 'date_generated' => \date('Y-m-d\TH:i:s'), 'store_in_cloud' => $store_in_cloud, 'salt' => \Sodium\bin2hex($salt), // See sendToSkyport(); the salt isn't sent unless you explicitly // opt for it to be sent. 'public_key' => \Sodium\bin2hex( $sign_public->getRawKeyMaterial() ), 'type' => $key_type ]; // This is the message we are signing. $message = \json_encode( [ 'action' => 'CREATE', 'date_generated' => $new_key['date_generated'], 'public_key' => $new_key['public_key'], 'supplier' => $supplier, 'type' => $new_key['type'] ] ); if ($has_master) { list($masterSig, $masterPubKey) = $this->signNewKeyWithMasterKey($supplier, $message); } else { // This is our first key, so we don't need it. $masterSig = ''; $masterPubKey = ''; } // Save the configuration $this->config['suppliers'][$supplier]['signing_keys'][] = $new_key; // Send the public kay (and, maybe, the salt) to the Skyport. $response = $this->sendToSkyport($supplier, $new_key, $message, $masterSig, $masterPubKey); if (!empty($response['status'])) { if ($response['status'] === 'ERROR') { echo "Error message returned!\n"; throw new \Error($response['message']); } $pk = Base64UrlSafe::encode( \Sodium\hex2bin($new_key['public_key']) ); if ($new_key['type'] === 'master') { echo 'New master key: ', $this->c['red'], $pk, $this->c[''], "\n"; } else { echo 'New signing key: ', $this->c['yellow'], $pk, $this->c[''], "\n"; } } } /** * Sign the new key with our current master key * * @param string $supplier * @param string $messageToSign * @return string[] * @throws \Exception */ protected function signNewKeyWithMasterKey(string $supplier, string $messageToSign): array { $master_keys = []; foreach ($this->config['suppliers'][$supplier]['signing_keys'] as $key) { if ($key['type'] === 'master' && !empty($key['salt'])) { $master_keys [] = $key; } } // This shouldn't happen, but just in case: if (empty($master_keys)) { throw new \Exception( 'You cannot generate another key unless you already have a master key with the salt loaded locally.' ); } // Select the correct master key. if (\count($master_keys) === 1) { $signingKey = $master_keys[0]; } else { echo 'Select which master key to use:'; do { foreach ($master_keys as $index => $key) { $pk = Base64UrlSafe::encode( \Sodium\hex2bin($key['public_key']) ); echo ($index + 1), "\t", $pk, "\n"; } $keyIndex = $this->prompt('Enter a number: '); if (empty($keyIndex)) { // Okay, let's cancel. throw new \Exception('Aborted.'); } if ($keyIndex < 1 || $keyIndex > \count($master_keys)) { $keyIndex = 0; echo 'Please enter a number between 1 and ', \count($master_keys), ".\n"; } } while ($keyIndex < 1); $signingKey = $master_keys[--$keyIndex]; } $signature = ''; $masterSalt = \Sodium\hex2bin($signingKey['salt']); do { $password = $this->silentPrompt('Enter the password for your master key: '); if (empty($password)) { // Okay, let's cancel. throw new \Exception('Aborted.'); } $masterKeyPair = KeyFactory::deriveSignatureKeyPair( $password, $masterSalt, false, KeyFactory::SENSITIVE ); // We must verify the public key matches: $masterPublicKey = $masterKeyPair->getPublicKey(); if (\hash_equals( $masterPublicKey->getRawKeyMaterial(), \Sodium\hex2bin($signingKey['public_key']) )) { $masterSecretKey = $masterKeyPair->getSecretKey(); // Setting $signature exits the loop $signature = Asymmetric::sign( $messageToSign, $masterSecretKey ); } else { echo 'Incorrect master key passphrase!', "\n"; } } while (!$signature); // We are returning two strings: return [ $signature, $signingKey['public_key'] ]; } /** * Send information about the new key to our Skyport * * @param string $supplier * @param array $data * @param string $message * @param string $masterSignature * @param string $masterPublicKey * @return array */ protected function sendToSkyport( string $supplier, array $data = [], string $message, string $masterSignature, string $masterPublicKey ): array { list ($skyport, $publicKey) = $this->getSkyport(); $postData = [ 'token' => $this->getToken($supplier), 'date_generated' => $data['date_generated'], 'message' => $message, 'publickey' => $data['public_key'], 'type' => $data['type'] ]; // The user must opt in for this to be invoked: if ($data['store_in_cloud']) { $postData['stored_salt'] = $data['salt']; } // If this isn't our first key, we should be signing it with our master key. if ($masterPublicKey && $masterSignature) { // The skyport MUST make validate the master public key before checking // the signature. $postData['master'] = [ // Only used for "which key?", don't trust this input 'public_key' => $masterPublicKey, // Should validate date_generated and publickey 'signature' => $masterSignature ]; } return Base\HTTP::postSignedJSON( $skyport . 'key/add', $publicKey, $postData ); } /** * Get a list of keywords (including supplier name) for Zxcvbn. This includes * the supplier name and some keywords relevant to Airship to demote obvious * password choices. * * @param string $supplier_name * @return array */ protected function getZxcvbnKeywords( string $supplier_name ): array { return [ $supplier_name, 'airship', 'barge', 'flotilla', 'php 7', 'libsodium', 'sodium', 'NaCl', 'crypto', 'cryptography', 'Halite', 'scrypt', 'argon2', 'argon2i', 'kdf', 'paragon', 'Paragon Initiative Enterprises' ]; } }