/** * Seal a message, using only the recipient's public key. * * @param string $message * @param EncryptionPublicKey $publicKey * @return string */ public function sealAsymmetric($plaintext, EncryptionPublicKey $publicKey, array $options = []) : string { /** * 1. Generate a random DH private key. */ list($eph_secret, $eph_public) = KeyFactory::generateEncryptionKeyPair(Common::DRIVER_OPENSSL); /** * 2. Calculate the shared secret. */ $sharedSecret = $this->getSharedSecret($eph_secret, $publicKey); /** * 3. Encrypt with symmetric-key crypto. */ // Build our header: // [VV][VV]: $message = \chr(Common::VERSION_MAJOR); $message .= \chr(Common::VERSION_MINOR); // [DD]: Make sure leftmost bit is 1 because we're sealing $message .= \chr(0x80 | self::DRIVER_ID & 0x7f); // [CC]: $message .= \chr(Common::VERSION_MAJOR ^ Common::VERSION_MINOR ^ (0x80 | self::DRIVER_ID & 0x7f)); $message .= $eph_public->getRawBytes(); // Salt: $salt = \random_bytes(Common::safeStrlen(\hash($this->config['hash'], '', true))); // Split keys: list($encKey, $authKey) = $this->splitSymmetricKey($sharedSecret, $salt); $message .= $salt; // HKDF salt // Nonce: $nonce = \random_bytes(\openssl_cipher_iv_length($this->config['cipher'])); $message .= $nonce; // Nonce for the stream cipher // Encrypt: $message .= \openssl_encrypt($plaintext, $this->config['cipher'] . '-ctr', $encKey->getRawBytes(), OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $nonce); unset($encKey); /** * 4. Authenticate: */ $message .= \hash_hmac($this->config['hash'], $message, $authKey->getRawBytes(), true); unset($authKey); // Return: return $message; }
/** * Create a JSON Web Token string * * @param string $keyName name of the key to use to sign the token * @param array|Traversable $payload traversable set of claims, claim => value * @param int $expirationOffset seconds from now that token will expire. If not specified, * an "exp" claim will not be added or updated * * @return string a token string returned from JsonWebToken::create() * * @throws \DomainException; * @throws \InvalidArgumentException; * @throws \UnexpectedValueException; */ public static function build($keyName, $payload, $expirationOffset = 0) { $token = new JsonWebToken(KeyFactory::build($keyName)); return $token->create($payload, $expirationOffset); }
/** * Seal the contents of a file. * * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionPublicKey $publicKey * @return int */ protected static function sealData(ReadOnlyFile $input, MutableFile $output, EncryptionPublicKey $publicKey) : int { // Generate a new keypair for this encryption $ephemeralKeyPair = KeyFactory::generateEncryptionKeyPair(); $ephSecret = $ephemeralKeyPair->getSecretKey(); $ephPublic = $ephemeralKeyPair->getPublicKey(); unset($ephemeralKeyPair); // Calculate the shared secret key $sharedSecretKey = AsymmetricCrypto::getSharedSecret($ephSecret, $publicKey, true); // Destroy the secret key after we have the shared secret unset($ephSecret); // Load the configuration $config = self::getConfig(Halite::HALITE_VERSION_FILE, 'seal'); // Generate a nonce as per crypto_box_seal $nonce = \Sodium\crypto_generichash($ephPublic->getRawKeyMaterial() . $publicKey->getRawKeyMaterial(), '', \Sodium\CRYPTO_STREAM_NONCEBYTES); // Generate a random HKDF salt $hkdfSalt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN); // Split the keys list($encKey, $authKey) = self::splitKeys($sharedSecretKey, $hkdfSalt, $config); // We no longer need the original key after we split it unset($key); // Write the header: $output->writeBytes(Halite::HALITE_VERSION_FILE, Halite::VERSION_TAG_LEN); $output->writeBytes($ephPublic->getRawKeyMaterial(), \Sodium\CRYPTO_BOX_PUBLICKEYBYTES); $output->writeBytes($hkdfSalt, $config->HKDF_SALT_LEN); // VERSION 2+ $mac = \Sodium\crypto_generichash_init($authKey); // We no longer need $authKey after we set up the hash context \Sodium\memzero($authKey); \Sodium\crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); \Sodium\crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); \Sodium\crypto_generichash_update($mac, $hkdfSalt); unset($ephPublic); \Sodium\memzero($hkdfSalt); return self::streamEncrypt($input, $output, new EncryptionKey(new HiddenString($encKey)), $nonce, $mac, $config); }
/** * Execute the start command, which will start a new hangar session. * * @param array $args * @return bool * @throws \Error */ public function fire(array $args = []) : bool { $file = $this->selectFile($args[0] ?? ''); if (!isset($this->config['salt']) && \count($args) < 2) { throw new \Error('No salt configured or passed'); } if (\count($args) > 2) { switch (\strtolower($args[2])) { case 'fast': case 'i': case 'interactive': case 'weak': $level = KeyFactory::INTERACTIVE; break; case 'm': case 'signing': case 'moderate': $level = KeyFactory::MODERATE; break; default: $level = KeyFactory::SENSITIVE; break; } } elseif (isset($this->config['keytype'])) { switch ($this->config['keytype']) { case 'fast': case 'i': case 'interactive': case 'weak': $level = KeyFactory::INTERACTIVE; break; case 'm': case 'signing': case 'moderate': $level = KeyFactory::MODERATE; break; default: $level = KeyFactory::SENSITIVE; break; } } else { $level = KeyFactory::SENSITIVE; } $salt = \Sodium\hex2bin($args[1] ?? $this->config['salt']); echo 'Generating a signature for: ', $file, "\n"; $password = $this->silentPrompt('Enter password: '******'false' in version 2.0.0 (with Halite 3) $sign_kp = KeyFactory::deriveSignatureKeyPair($password, $salt, false, $level); if (!$sign_kp instanceof SignatureKeyPair) { throw new \Error('Error during key derivation'); } $signature = File::sign($file, $sign_kp->getSecretKey()); if (isset($this->history)) { $this->config['build_history']['signed'] = true; } \file_put_contents($file . '.sig', $signature); echo 'File signed: ' . $file . '.sig', "\n"; echo 'Public key: ' . \Sodium\bin2hex($sign_kp->getPublicKey()->getRawKeyMaterial()), "\n"; return true; }
/** * Common signing process. User selects key, provides password. * * @param array $manifest * @return SignatureSecretKey * @throws \Exception */ protected function signPreamble(array $manifest) : SignatureSecretKey { $HTAB = \str_repeat(' ', \intdiv(self::TAB_SIZE, 2)); $supplier_name = $manifest['supplier']; // Sanity checks: if (!\array_key_exists('suppliers', $this->config)) { echo 'You are not authenticated for any suppliers.', "\n"; exit(255); } if (!\array_key_exists($supplier_name, $this->config['suppliers'])) { echo 'Check the supplier in the JSON file (', $supplier_name, ') for correctness.', 'Otherwise, you might need to log in.', "\n"; exit(255); } $supplier = $this->config['suppliers'][$supplier_name]; $numKeys = 0; if ($this->signWithMasterKeys) { $good_keys = []; // This should really not be used: $numKeys = \count($supplier['signing_keys']); foreach ($supplier['signing_keys'] as $k) { if (!empty($k['salt'])) { $good_keys[] = $k; ++$numKeys; } } } else { // This should be used instead: $good_keys = []; foreach ($supplier['signing_keys'] as $k) { if ($k['type'] === 'signing' && !empty($k['salt'])) { $good_keys[] = $k; ++$numKeys; } } } if ($numKeys > 1) { echo 'You have more than one signing key available.', "\n"; $n = 1; $size = (int) \floor(\log($numKeys, 10)); $key_associations = $HTAB . "ID\t Public Key " . \str_repeat(' ', 33) . "\t Type\n"; foreach ($supplier['signing_keys'] as $sign_key) { if (!$this->signWithMasterKeys && $sign_key['type'] === 'master') { continue; } $_n = \str_pad($n, $size, ' ', STR_PAD_LEFT); // Short format: $pk = Base64UrlSafe::encode(\Sodium\hex2bin($sign_key['public_key'])); $key_associations .= $HTAB . $_n . $HTAB . $pk . $HTAB . $sign_key['type'] . "\n"; ++$n; } // Let's ascertain the user's key selection do { echo $key_associations; $choice = (int) $this->prompt('Enter the ID for the key you wish to use: '); if ($choice < 1 || $choice > $numKeys) { $choice = null; } } while (empty($choice)); $supplierKey = $good_keys[$choice - 1]; echo "\n"; } else { $supplierKey = $good_keys[0]; } // The above !empty($k['salt']) check should have rendered this check redundant: if (empty($supplierKey['salt'])) { echo 'Salt not found for this key. It is not possible to reproduce it.', "\n"; exit(255); } // Short format: $pk = Base64UrlSafe::encode(\Sodium\hex2bin($supplierKey['public_key'])); // Color coded: Master keys are red, since they take longer. // We don't support signing packages with a master key, but // this decision could be undone in the future. $c = $supplierKey['type'] === 'master' ? $this->c['red'] : $this->c['yellow']; echo 'Selected ', $supplierKey['type'], ' key: ', $c, $pk, $this->c[''], "\n"; $password = $this->silentPrompt('Enter Password for Signing Key:'); // Derive and split the SignatureKeyPair from your password and salt $salt = \Sodium\hex2bin($supplierKey['salt']); switch ($supplierKey['type']) { case 'signing': $type = KeyFactory::MODERATE; echo 'Verifying (this may take a second or two)...'; break; case 'master': $type = KeyFactory::SENSITIVE; echo 'Verifying (this may take a few seconds)...'; break; default: $type = KeyFactory::INTERACTIVE; echo 'Verifying...'; } $keyPair = KeyFactory::deriveSignatureKeyPair($password, $salt, false, $type); $sign_secret = $keyPair->getSecretKey(); $sign_public = $keyPair->getPublicKey(); echo ' Done.', "\n"; // We don't need this anymore. \Sodium\memzero($password); // Check that the public key we derived from the password matches the one on file $pubKey = \Sodium\bin2hex($sign_public->getRawKeyMaterial()); if (!\hash_equals($supplierKey['public_key'], $pubKey)) { // Zero the memory ASAP unset($sign_secret); unset($sign_public); echo 'Invalid password for selected key', "\n"; exit(255); } // Zero the memory ASAP unset($sign_public); return $sign_secret; }