<?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'
        ];
    }
}
 
  |