/** * Get the root hash of this Merkle tree. * * @param bool $raw - Do we want a raw string instead of a hex string? * * @return string */ public function getRoot(bool $raw = false) : string { if (!$this->rootCalculated) { $this->root = $this->calculateRoot(); } return $raw ? $this->root : \Sodium\bin2hex($this->root); }
public function getHash(bool $raw = false) : string { if ($raw) { return $this->lastHash; } return \Sodium\bin2hex($this->lastHash); }
/** * Calculate the Merkle root, taking care to distinguish between * leaves and branches (0x01 for the nodes, 0x00 for the branches) * to protect against second-preimage attacks * * @return string */ protected function calculateRoot() : string { $size = \count($this->nodes); $order = self::getSizeRoundedUp($size); $hash = []; $debug = []; // Population (Use self::MERKLE_LEAF as a prefix) for ($i = 0; $i < $order; ++$i) { if ($i >= $size) { $hash[$i] = self::MERKLE_LEAF . $this->nodes[$size - 1]->getHash(true); $debug[$i] = \Sodium\bin2hex($hash[$i]); } else { $hash[$i] = self::MERKLE_LEAF . $this->nodes[$i]->getHash(true); $debug[$i] = \Sodium\bin2hex($hash[$i]); } } // Calculation (Use self::MERKLE_BRANCH as a prefix) do { $tmp = []; $j = 0; for ($i = 0; $i < $order; $i += 2) { if (empty($hash[$i + 1])) { $tmp[$j] = \Sodium\crypto_generichash(self::MERKLE_BRANCH . $hash[$i] . $hash[$i]); } else { $tmp[$j] = \Sodium\crypto_generichash(self::MERKLE_BRANCH . $hash[$i] . $hash[$i + 1]); } ++$j; } $hash = $tmp; $order >>= 1; } while ($order > 1); // We should only have one value left:t return \array_shift($hash); }
/** * Fetch the provider key from verifyne server. * Verifies the key if libsodium is available. * After successful return the key can be read from the global class variables PROVIDER_KEY and PROVIDER_KEY_HEX. * * @return Returns TRUE if successful, or a WP_Error instance otherwise. */ static function get_provider_key() { if (NULL !== self::$PROVIDER_KEY) { return TRUE; } if (!extension_loaded('libsodium')) { return new WP_Error("verifyne", "libsodium not available"); } # Decode CA key self::$VERIFYNE_CA_KEY = base64_decode("i7dbWvlFdHwviUXav7N1Lwoi+DOJDG9SuFHNwP/AUjU=", true); if (FALSE === self::$VERIFYNE_CA_KEY) { return new WP_Error("verifyne", "Wrong CA key"); } # Invoke API $cont = @file_get_contents("https://api.verifyne.me/v1/provider-key"); if (FALSE === $cont) { return new WP_Error("verifyne", "Failed to query verifyne API"); } # Read JSON $resp = @json_decode($cont); if (NULL === $resp) { return new WP_Error("verifyne", "Invalid JSON received"); } # Decode signature $sig = base64_decode($resp->content->psig, $strict = true); if (FALSE === $sig) { return new WP_Error("verifyne", "Decoding signature failed"); } $pkey_b64 = $resp->content->pkey; # Verify signature if (TRUE !== @\Sodium\crypto_sign_verify_detached($sig, $pkey_b64, self::$VERIFYNE_CA_KEY)) { return new WP_Error("verifyne", "Failed to verify provider key"); } # Decode key (dec == 'ed25519:xxxxxxx...') $dec = base64_decode($pkey_b64, $strict = true); if (FALSE === $dec) { return new WP_Error("verifyne", "Decoding provider key description failed"); } # Decode raw provider key self::$PROVIDER_KEY = base64_decode(explode(":", $dec, 2)[1], $strict = true); if (FALSE === self::$PROVIDER_KEY) { return new WP_Error("verifyne", "Decoding provider key"); } self::$PROVIDER_KEY_HEX = @\Sodium\bin2hex(self::$PROVIDER_KEY); self::$VERIFYNE_CA_KEY_HEX = @\Sodium\bin2hex(self::$VERIFYNE_CA_KEY); return TRUE; }
/** * Encrypt a message using the Halite encryption protocol * (Encrypt then MAC -- Xsalsa20 then HMAC-SHA-512/256) * * @param string $plaintext * @param EncryptionKey $secretKey * @param boolean $raw Don't hex encode the output? * @return string */ public static function encrypt(string $plaintext, EncryptionKey $secretKey, bool $raw = false) : string { $config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'encrypt'); // Generate a nonce and HKDF salt: $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES); $salt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN); // Split our keys according to the HKDF salt: list($eKey, $aKey) = self::splitKeys($secretKey, $salt, $config); // Encrypt our message with the encryption key: $xored = \Sodium\crypto_stream_xor($plaintext, $nonce, $eKey); \Sodium\memzero($eKey); // Calculate an authentication tag: $auth = self::calculateMAC(Halite::HALITE_VERSION . $salt . $nonce . $xored, $aKey); \Sodium\memzero($aKey); if (!$raw) { return \Sodium\bin2hex(Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth); } return Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth; }
/** * Sign a message with our private key * * @param string $message Message to sign * @param Contract\CryptoKeyInterface $privatekey * @param boolean $raw Don't hex encode the output? * * @return string Signature (detached) */ public static function sign($message, Contract\CryptoKeyInterface $privatekey, $raw = false) { if (!$privatekey->isSigningKey()) { throw new CryptoAlert\InvalidKey('Expected a signing key'); } if (!$privatekey->isSecretKey()) { throw new CryptoAlert\InvalidKey('Expected a secret key'); } $signed = \Sodium\crypto_sign_detached($message, $privatekey->get()); if ($raw) { return $signed; } return \Sodium\bin2hex($signed); }
/** * We are revoking a key. * * @param array $args * @throws \Exception * @return mixed */ protected function handleKeyRevoke(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 revoke keys.', "\n"; echo 'Run this command: ', $this->c['yellow'], 'barge login', $this->c[''], "\n"; exit(255); } $masterKeys = []; $keyList = []; foreach ($this->config['suppliers'][$supplier]['signing_keys'] as $key) { if ($key['type'] === 'master') { if (!empty($key['salt'])) { $masterKeys[] = $key; } else { $keyList[] = $key; } } else { $keyList[] = $key; } } if (empty($masterKeys)) { echo 'No usable master keys found. Make sure the salt is loaded locally.', "\n"; exit(255); } if (empty($keyList)) { // If and only if you have nothing more to revoke, allow revoking the master key: $keyList = $masterKeys; } if (\count($masterKeys) === 1) { $masterKey = $masterKeys[0]; } else { $masterKey = $this->selectKeyFromList('Select your master key: ', $masterKeys); // Add other master keys to the list foreach ($masterKeys as $key) { if ($key['public_key'] !== $masterKey['public_key']) { $keyList[] = $key; } } } if (\count($keyList) === 1) { $revokingKey = $keyList[0]; } else { $revokingKey = $this->selectKeyFromList('Select which key to revoke: ', $keyList); } $confirm_revoke = null; while ($confirm_revoke === null) { $choice = $this->prompt('Are you sure you wish to revoke this key? (y/N): '); switch ($choice) { case 'YES': case 'yes': case 'Y': case 'y': $confirm_revoke = true; break; case 'N': case 'NO': case 'n': case 'no': case '': // Just pressing enter means "don't store it"! $confirm_revoke = false; break; default: echo "\n", $this->c['yellow'], 'Invalid response. Please enter yes or no.', $this->c[''], "\n"; } } // This is what get signed by our master key: $message = ['action' => 'REVOKE', 'date_revoked' => \date('Y-m-d\\TH:i:s'), 'public_key' => $revokingKey['public_key'], 'supplier' => $supplier]; $messageToSign = \json_encode($message); $iter = false; do { if ($iter) { echo 'Incorrect password.', "\n"; } $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, \Sodium\hex2bin($masterKey['salt']), false, KeyFactory::SENSITIVE); \Sodium\memzero($password); $masterPublicKeyString = \Sodium\bin2hex($masterKeyPair->getPublicKey()->getRawKeyMaterial()); $iter = true; } while (!\hash_equals($masterKey['public_key'], $masterPublicKeyString)); $signature = Asymmetric::sign($messageToSign, $masterKeyPair->getSecretKey()); $response = $this->sendRevocation($supplier, $message, $signature, $masterPublicKeyString); if ($response['status'] === 'OK') { foreach ($this->config['suppliers'][$supplier]['signing_keys'] as $i => $key) { if ($key['public_key'] === $message['public_key']) { unset($this->config['suppliers'][$supplier]['signing_keys'][$i]); } } } return $response; }
/** * Calculate the BLAKE2b checksum of an entire stream * * @param StreamInterface $fileStream * @param Key $key * @param bool $raw * @return string * @throws CryptoException\InvalidKey */ protected static function checksumData(StreamInterface $fileStream, Key $key = null, bool $raw = false) : string { $config = self::getConfig(Halite::HALITE_VERSION_FILE, 'checksum'); if ($key instanceof AuthenticationKey) { $state = \Sodium\crypto_generichash_init($key->getRawKeyMaterial(), $config->HASH_LEN); } elseif ($config->CHECKSUM_PUBKEY && $key instanceof SignaturePublicKey) { // In version 2, we use the public key as a hash key $state = \Sodium\crypto_generichash_init($key->getRawKeyMaterial(), $config->HASH_LEN); } elseif (isset($key)) { throw new CryptoException\InvalidKey('Argument 2: Expected an instance of AuthenticationKey'); } else { $state = \Sodium\crypto_generichash_init('', $config->HASH_LEN); } $size = $fileStream->getSize(); while ($fileStream->remainingBytes() > 0) { // Don't go past the file size even if $config->BUFFER is not an even multiple of it: if ($fileStream->getPos() + $config->BUFFER > $size) { $amount_to_read = $size - $fileStream->getPos(); } else { $amount_to_read = $config->BUFFER; } $read = $fileStream->readBytes($amount_to_read); \Sodium\crypto_generichash_update($state, $read); } // Do we want a raw checksum? if ($raw) { return \Sodium\crypto_generichash_final($state, $config->HASH_LEN); } return \Sodium\bin2hex(\Sodium\crypto_generichash_final($state, $config->HASH_LEN)); }
/** * Sign a message with our private key * * @param string $message Message to sign * @param SignatureSecretKey $privateKey * @param boolean $raw Don't hex encode the output? * @return string Signature (detached) */ public static function sign(string $message, SignatureSecretKey $privateKey, bool $raw = false) : string { $signed = \Sodium\crypto_sign_detached($message, $privateKey->getRawKeyMaterial()); if ($raw) { return $signed; } return \Sodium\bin2hex($signed); }
/** * 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; }
/** * Save a key to a file * * @param string $filePath * @param string $keyData * @return int|bool */ protected static function saveKeyFile($filePath, $keyData) { return \file_put_contents($filePath, \Sodium\bin2hex(Halite::HALITE_VERSION_KEYS . $keyData . \Sodium\crypto_generichash(Halite::HALITE_VERSION_KEYS . $keyData, null, \Sodium\CRYPTO_GENERICHASH_BYTES_MAX))); }
/** * Return the user's logout token. This is to prevent logout via CSRF. * * @return string */ function logout_token() : string { if (\array_key_exists('logout_token', $_SESSION)) { return $_SESSION['logout_token']; } $_SESSION['logout_token'] = \Sodium\bin2hex(\random_bytes(16)); return $_SESSION['logout_token']; }
/** * Wrapper around \Sodium\crypto_generichash() * * Expects a key (binary string). * Returns hexadecimal characters. * * @param string $input * @param string $key * @param int $length * @return string */ public static function keyed_hash(string $input, string $key, int $length = \Sodium\CRYPTO_GENERICHASH_BYTES) : string { return \Sodium\bin2hex(self::raw_keyed_hash($input, $key, $length)); }
/** * 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), '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"; } } }
/** * This method stores the necessary bits of data in this object. * * @param Channel $chan * @param array $updateData * @return void * @throws CouldNotUpdate * @throws NoSupplier */ protected function unpackMessageUpdate(Channel $chan, array $updateData) { // This is the JSON message from the tree node, stored as an array: $this->updateMessage = $updateData; if ($this->isPackageUpdate() || $this->isAirshipUpdate()) { // These aren't signed for updating the tree. return; } // We need a precise format: $dateGen = (new \DateTime($this->stored['date_generated']))->format(\AIRSHIP_DATE_FORMAT); $messageToSign = ['action' => $this->action, 'date_generated' => $dateGen, 'public_key' => $updateData['public_key'], 'supplier' => $updateData['supplier'], 'type' => $updateData['type']]; try { $this->supplier = $this->loadSupplier($chan, $updateData); } catch (NoSupplier $ex) { if (!$this->isNewSupplier) { throw $ex; } } // If this isn't a new supplier, we need to verify the key if ($this->isNewSupplier) { return; } if ($updateData['master'] === null) { throw new CouldNotUpdate(\__('The master data is NULL, but the supplier exists.')); } $master = \json_decode($updateData['master'], true); foreach ($this->supplier->getSigningKeys() as $supKey) { // Yes, this is (in fact) a SignaturePublicKey: if (IDE_HACKS) { $supKey['key'] = new SignaturePublicKey(); } if ($supKey['type'] !== 'master') { continue; } $pub = \Sodium\bin2hex($supKey['key']->getRawKeyMaterial()); // Is this the key we're looking for? if (\hash_equals($pub, $master['public_key'])) { // Store the public key $this->supplierMasterKeyUsed = $supKey['key']; break; } } if (empty($this->supplierMasterKeyUsed)) { throw new CouldNotUpdate(\__('The provided public key does not match any known master key.')); } $encoded = \json_encode($messageToSign); if (!Asymmetric::verify($encoded, $this->supplierMasterKeyUsed, $master['signature'])) { throw new CouldNotUpdate(\__('Invalid signature for this master key.')); } }
/** * Check if a password is known by the knownpassword.org API. * * @param string $password The password to check. * @param string $passwordFormat The format of the given password (Blake2b, Sha512, Cleartext) [Default: Blake2b]. * @return mixed Exception on error, true if the password is known and false if the password is unknown. * @access public */ public function checkPassword($password, $passwordFormat = "Blake2b") { $apiData = array(); switch ($passwordFormat) { case "Blake2b": $apiData = array("Blake2b" => $password); break; case "Sha512": $apiData = array("Sha512" => $password); break; case "Cleartext": $apiData = array("Cleartext" => $password); break; default: throw new \Exception("Unknown passwordFormat."); } $nonce = \Sodium\randombytes_buf(24); $signature = \Sodium\crypto_sign_detached($nonce, $this->_privatekey); $clearJson = json_encode($apiData); $encryptionNonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_BOX_NONCEBYTES); $encryptionKeyPair = \Sodium\crypto_box_keypair(); $encryptionSecretkey = \Sodium\crypto_box_secretkey($encryptionKeyPair); $encryptionPublickey = \Sodium\crypto_box_publickey($encryptionKeyPair); $encryptionKeyPair = \Sodium\crypto_box_keypair_from_secretkey_and_publickey($encryptionSecretkey, $this->_serverEncryptionPublicKey); $ciphertext = \Sodium\crypto_box($clearJson, $encryptionNonce, $encryptionKeyPair); $encryptedApiData = array("PublicKey" => \Sodium\bin2hex($encryptionPublickey), "Nonce" => \Sodium\bin2hex($encryptionNonce), "Ciphertext" => \Sodium\bin2hex($ciphertext)); $data_string = json_encode($encryptedApiData); $ch = curl_init($this->_apiurl . "/checkpassword"); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Content-Length: ' . strlen($data_string), 'User-Agent: ' . 'Laravel 5', 'X-Public: ' . \Sodium\bin2hex($this->_publickey), 'X-Nonce: ' . \Sodium\bin2hex($nonce), 'X-Signature: ' . \Sodium\bin2hex($signature))); if (!($result = curl_exec($ch))) { throw new \Exception("Request failed"); } $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header = substr($result, 0, $header_size); $headers = $this->get_headers_from_curl_response($header); if (array_key_exists("http_code", $headers[0]) && array_key_exists("X-Powered-By", $headers[0]) && array_key_exists("X-Signature", $headers[0])) { $httpCode = $headers[0]["http_code"]; $responsePowered = $headers[0]["X-Powered-By"]; $responseSignature = $headers[0]["X-Signature"]; $responseNonce = $headers[0]["X-Nonce"]; if ($httpCode === "HTTP/1.1 200 OK" || $httpCode === "HTTP/2.0 200 OK") { if ($responsePowered === "bitbeans") { // validate the response signature if (!\Sodium\crypto_sign_verify_detached(\Sodium\hex2bin($responseSignature), \Sodium\crypto_generichash(\Sodium\hex2bin($responseNonce), null, 64), $this->_serverSignaturePublicKey)) { throw new \Exception("Invalid signature"); } } else { throw new \Exception("Invalid server"); } } else { throw new \Exception("Invalid response code"); } } else { throw new \Exception("Invalid header"); } $result = substr($result, $header_size); curl_close($ch); $resultJson = json_decode($result); $decryptionKeyPair = \Sodium\crypto_box_keypair_from_secretkey_and_publickey($encryptionSecretkey, \Sodium\hex2bin($resultJson->{'publicKey'})); $plaintext = \Sodium\crypto_box_open(\Sodium\hex2bin($resultJson->{'ciphertext'}), \Sodium\hex2bin($resultJson->{'nonce'}), $decryptionKeyPair); if ($plaintext === FALSE) { throw new \Exception("Malformed message or invalid MAC"); } $plaintextJson = json_decode($plaintext); return !$plaintextJson->{'FoundPassword'}; }
/** * Calculate a BLAHE2b checksum of a file * * @param string $fileHandle The file you'd like to checksum * @param string $key An optional BLAKE2b key * @param bool $raw Set to true if you don't want hex * * @return string */ public static function checksumResource($fileHandle, \ParagonIE\Halite\Contract\CryptoKeyInterface $key = null, $raw = false) { // Input validation if (!\is_resource($fileHandle)) { throw new \ParagonIE\Halite\Alerts\InvalidType('Expected input handle to be a resource'); } $config = self::getConfig(Halite::HALITE_VERSION, 'checksum'); if ($key) { $state = \Sodium\crypto_generichash_init($key->get(), $config['HASH_LEN']); } else { $state = \Sodium\crypto_generichash_init(null, $config['HASH_LEN']); } while (!\feof($fileHandle)) { $read = \fread($fileHandle, $config['BUFFER']); if ($read === false) { throw new CryptoException\FileAccessDenied('Could not read from the file'); } \Sodium\crypto_generichash_update($state, $read); } if ($raw) { return \Sodium\crypto_generichash_final($state, $config['HASH_LEN']); } return \Sodium\bin2hex(\Sodium\crypto_generichash_final($state, $config['HASH_LEN'])); }
/** * Sign a message with our private key * * @param string $message Message to sign * @param SignatureSecretKey $privateKey * @param boolean $raw Don't hex encode the output? * * @return string Signature (detached) * * @throws CryptoException\InvalidKey */ public static function sign($message, Contract\KeyInterface $privateKey, $raw = false) { if (!$privateKey instanceof SignatureSecretKey) { throw new CryptoException\InvalidKey('Argument 2: Expected an instance of SignatureSecretKey'); } $signed = \Sodium\crypto_sign_detached($message, $privateKey->get()); if ($raw) { return $signed; } return \Sodium\bin2hex($signed); }
/** * Converts binary data to hexadecimal data. * * @param string $string String containing binary data to convert to hexadecimal. * @return string */ public static function bin2hex($string) { return \Sodium\bin2hex($string); }
/** * Save a key to a file * * @param string $filePath * @param string $keyData * @return int|bool */ protected static function saveKeyFile(string $filePath, string $keyData) : bool { return false !== \file_put_contents($filePath, \Sodium\bin2hex(Halite::HALITE_VERSION_KEYS . $keyData . \Sodium\crypto_generichash(Halite::HALITE_VERSION_KEYS . $keyData, '', \Sodium\CRYPTO_GENERICHASH_BYTES_MAX))); }
/** * This method is private to avoid it from being accessed outside of the * trusted methods (which handle validation). Don't change it. * * @param string $tmp_name * @return string "HH/HH/HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH" * @throws UploadError */ private function moveUploadedFile(string $tmp_name) : string { $dir1 = \Sodium\bin2hex(\random_bytes(1)); $dir2 = \Sodium\bin2hex(\random_bytes(1)); $base = AIRSHIP_UPLOADS . $dir1 . DIRECTORY_SEPARATOR . $dir2; if (!\file_exists($base)) { \mkdir($base, 0775, true); } do { $filename = \Sodium\bin2hex(\random_bytes(22)); } while (\file_exists($base . DIRECTORY_SEPARATOR . $filename)); if (!\move_uploaded_file($tmp_name, $base . DIRECTORY_SEPARATOR . $filename)) { throw new UploadError(\__('Could not move temporary file to its permanent home')); } return $dir1 . DIRECTORY_SEPARATOR . $dir2 . DIRECTORY_SEPARATOR . $filename; }
function crLoadEncKeys() { $this->webLog("Loading encryption keys.", __METHOD__); // if the user is not logged in we will only load the anonymous key if ($this->config->profile->group === 'public') { $this->config->crCryptoKeys = (object) array('anonymous' => false, 'cookie' => false); } // some internal pages allow unauthenticated users to run admin tools and only needs the system key if ($this->config->profile->internal === true) { $this->config->crCryptoKeys = (object) array('system' => false); } // loop through each expected key foreach ($this->config->crCryptoKeys as $keyName => $value) { // check if the key is loaded already or not if ($value === false) { $this->webLog("Found unloaded key with name '{$keyName}'", __METHOD__); // check whether or not a key exists in the db $this->config->crCryptoKeys->{$keyName} = (object) array('loaded' => false, 'keyid' => false, 'eckey' => false); $userid = 'systemwide'; if ($query = $this->mysql_query("SELECT `keyid`, `key` FROM `keys` WHERE `userid`='{$userid}' AND `name`='{$keyName}' ORDER BY `id` DESC LIMIT 1")) { $result = $this->mysql_returnResource($query); $data = $result->fetch_object(); if ($result->num_rows === 1) { // key is found, load it up $this->webLog("Located existing key for '{$keyName}' with keyid '{$data->keyid}'", __METHOD__, 'info'); $this->config->crCryptoKeys->{$keyName}->keyid = $data->keyid; $this->config->crCryptoKeys->{$keyName}->eckey = $data->key; $this->config->crCryptoKeys->{$keyName}->loaded = true; } } // check if no key was found if ($this->config->crCryptoKeys->{$keyName}->loaded === false) { $this->webLog("No key was located! Generating a new one.", __METHOD__); $this->config->crCryptoKeys->{$keyName}->keyid = $this->crGenerateKeyId($userid, $keyName); $this->config->crCryptoKeys->{$keyName}->eckey = \Sodium\bin2hex($this->crCryptoGenerateKey()); if ($this->mysql_query("INSERT INTO `keys` (`timestamp`, `userid`, `keyid`, `name`, `key`) VALUES \n\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\t'" . time() . "',\n\t\t\t\t\t\t\t\t'{$userid}',\n\t\t\t\t\t\t\t\t'{$this->config->crCryptoKeys->{$keyName}->keyid}',\n\t\t\t\t\t\t\t\t'{$keyName}',\n\t\t\t\t\t\t\t\t'{$this->config->crCryptoKeys->{$keyName}->eckey}'\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t")) { $this->webLog("Created new encryption key with keyid '{$this->config->crCryptoKeys->{$keyName}->keyid}'", __METHOD__); } else { $this->webLog("Failed to create new key with keyid '{$this->config->crCryptoKeys->{$keyName}->keyid}'", __METHOD__, "fatal"); $this->shutdown($this->crLanguage('generic', 'runtimeError')); } } // remove our tracking key unset($this->config->crCryptoKeys->{$keyName}->loaded); } else { // if it's not a string then we have a problem if (!is_object($value)) { $this->webLog($this->crLanguage('generic', 'invalidKey', array("config::crCryptoKeys::{$keyName}", "false or object")), __METHOD__, 'fatal'); $this->shutdown($this->crLanguage('generic', 'runtimeError')); } else { $this->webLog("Doesn't actually yet support providing the keys via configuration; all DB managed!", __METHOD__, "fatal"); $this->shutdown($this->crLanguage('generic', 'runtimeError')); } } } // pass the keys along to crCrypto $this->crCryptoSetup($this->config->crCryptoKeys); }
/** * Encrypt a message using the Halite encryption protocol * * @param string $plaintext * @param EncryptionKey $secretKey * @param boolean $raw Don't hex encode the output? * @return string */ public static function encrypt($plaintext, Contract\KeyInterface $secretKey, $raw = false) { if (!$secretKey instanceof EncryptionKey) { throw new CryptoException\InvalidKey('Expected an instance of EncryptionKey'); } $config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'encrypt'); $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES); $salt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN); list($eKey, $aKey) = self::splitKeys($secretKey, $salt, $config); $xored = \Sodium\crypto_stream_xor($plaintext, $nonce, $eKey); $auth = self::calculateMAC(Halite::HALITE_VERSION . $salt . $nonce . $xored, $aKey); \Sodium\memzero($eKey); \Sodium\memzero($aKey); if (!$raw) { return \Sodium\bin2hex(Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth); } return Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth; }
/** * * @param \ParagonIE\Halite\Contract\StreamInterface $fileStream * @param AuthenticationKey $key * @param type $raw * @return type */ public static function checksumStream(StreamInterface $fileStream, KeyInterface $key = null, $raw = false) { $config = self::getConfig(Halite::HALITE_VERSION_FILE, 'checksum'); if ($key) { if (!$key instanceof AuthenticationKey) { throw new \ParagonIE\Halite\Alerts\InvalidKey('Argument 2: Expected an instance of AuthenticationKey'); } $state = \Sodium\crypto_generichash_init($key->get(), $config->HASH_LEN); } else { $state = \Sodium\crypto_generichash_init(null, $config->HASH_LEN); } $size = $fileStream->getSize(); while ($fileStream->remainingBytes() > 0) { $read = $fileStream->readBytes($fileStream->getPos() + $config->BUFFER > $size ? $size - $fileStream->getPos() : $config->BUFFER); \Sodium\crypto_generichash_update($state, $read); } if ($raw) { return \Sodium\crypto_generichash_final($state, $config->HASH_LEN); } return \Sodium\bin2hex(\Sodium\crypto_generichash_final($state, $config->HASH_LEN)); }
function crCryptoProcess($dir, $input, $eckey, $cipher) { // sanity checks $allowedDir = array('encrypt', 'decrypt'); $inputTypes = array('string', 'ad'); $cipherList = array('aes256gcm', 'chacha'); if (!in_array($dir, $allowedDir)) { $this->webLog($this->crLanguage('generic', 'invalidKey', array('dir', implode('|', $allowedDir))), __METHOD__, 'error'); return false; } if (is_string($input)) { $input = (object) array('string' => $input, 'ad' => ''); } if (is_object($input)) { if (count((array) $input) === 2) { foreach ($input as $key => $value) { if (!in_array($key, $inputTypes)) { $this->webLog($this->crLanguage('generic', 'invalidKey', array('input', implode('|', $inputTypes))), __METHOD__, 'error'); return false; } } } else { $this->webLog($this->crLanguage('generic', 'tooManyKeys', array(count($inputTypes), count($input), "input::(" . implode('|', $inputTypes) . ")")), __METHOD__, 'error'); return false; } } /*if ( !is_string($eckey) ) { $this->webLog($this->crLanguage('generic', 'missingKeyType', 'eckey'), __METHOD__, 'error'); return false; }*/ if (!in_array($cipher, $cipherList)) { $this->webLog($this->crLanguage('generic', 'invalidKey', array('cipher', implode('|', $cipherList))), __METHOD__, 'error'); return false; } // if the eckey matches the name of a stored key we'll map it to the proper eckey $keyid = '00-'; if (isset($this->crCryptoKeys->{$eckey})) { // set our keyid $keyid = $this->crCryptoKeys->{$eckey}->keyid; // create our index $this->crCryptoKeys->index->{$keyid} = $eckey; // set our eckey to the hex value $eckey = $this->crCryptoKeys->{$eckey}->eckey; } // process the request $start = microtime(true); // if we're decrypting then the input should be nonce.ciphertext if ($dir === 'decrypt') { // grab our nonce bytes param if ($cipher === 'aes256gcm') { $bytes = \Sodium\CRYPTO_AEAD_AES256GCM_NPUBBYTES; } if ($cipher === 'chacha') { $bytes = \Sodium\CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES; } // grab our message data // accepts two input types: <keyid>$<ciphertext> or just <ciphertext> $data = explode('$', $input->string); if (count($data) === 2) { // located a keyid and ciphertext $keyid = $data[0]; $message = $data[1]; // no keyid was located, so treat it as pure ciphertext } else { $data = $input->string; } // parse the message contents $message = \Sodium\hex2bin($message); $nonce = mb_substr($message, 0, $bytes, '8bit'); $ciphertext = mb_substr($message, $bytes, null, '8bit'); // on decrypt only: check if a key has been provided yet if ($eckey === false) { // check if the keyid is indexed if (isset($this->crCryptoKeys->index->{$keyid})) { // found a match; set our eckey $eckey = $this->crCryptoKeys->{$this->crCryptoKeys->index->{$keyid}}->eckey; } else { // no match and this far along means we can't decrypt $this->webLog("Cannot decrypt ciphertext because no suitable eckey was located", __METHOD__, 'error'); return false; } } } // if eckey is false, we cannot proceed if ($eckey === false) { $this->webLog(); return false; } // set our eckey to the actual encryption key $eckey = \Sodium\hex2bin($eckey); // process AES-256-GCM methods if ($cipher === 'aes256gcm') { //$eckey = ( $eckey === false ) ? \Sodium\randombytes_buf(\Sodium\CRYPTO_AEAD_AES256GCM_KEYBYTES) : \Sodium\hex2bin($eckey); if ($dir === 'encrypt') { // create the ciphertext $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_AEAD_AES256GCM_NPUBBYTES); $resultString = \Sodium\crypto_aead_aes256gcm_encrypt($input->string, $input->ad, $nonce, $eckey); } else { // decrypt the ciphertext $resultString = \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $input->ad, $nonce, $eckey); } // process CHACHA20-POLY1305 methods } else { if ($cipher === 'chacha') { //$eckey = ( $eckey === false ) ? \Sodium\randombytes_buf(\Sodium\CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES) : \Sodium\hex2bin($eckey); if ($dir === 'encrypt') { // create the ciphertext $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES); $resultString = \Sodium\crypto_aead_chacha20poly1305_encrypt($input->string, $input->ad, $nonce, $eckey); } else { // decrypt the ciphertext $resultString = \Sodium\crypto_aead_chacha20poly1305_decrypt($ciphertext, $input->ad, $nonce, $eckey); } } } // finishing up $totalTime = number_format(microtime(true) - $start, $this->config->precison + 10); $this->webLog("Performed '{$dir}' on a string with {$cipher} in {$totalTime} seconds", __METHOD__); $this->crCryptoAnalytics($eckey, $nonce, $dir, $cipher, strlen($input->string), $totalTime); \Sodium\memzero($eckey); // if decrypt, just send the string if ($dir === 'decrypt') { \Sodium\memzero($nonce); if ($resultString === false) { $this->webLog("Decryption failed!", __METHOD__, 'warn'); } return $resultString; } // if encrypt, send back the data in case we generated it $ciphertext = \Sodium\bin2hex($nonce . $resultString); \Sodium\memzero($nonce); return $keyid . '$' . $ciphertext; }
/** * Converts a binary string to an hexdecimal string. * * This is the same as PHP's bin2hex() implementation, but it is resistant to * timing attacks. * * @link https://paragonie.com/book/pecl-libsodium/read/03-utilities-helpers.md#bin2hex * @param string $binaryString The binary string to convert * @return string */ public function bin2hex($binaryString) { /** @noinspection PhpUndefinedNamespaceInspection @noinspection PhpUndefinedFunctionInspection */ return \Sodium\bin2hex($binaryString); }
/** * Encrypt a message using the Halite encryption protocol * * @param string $plaintext * @param Key $secretKey * @param boolean $raw Don't hex encode the output? * @return string */ public static function encrypt($plaintext, Contract\CryptoKeyInterface $secretKey, $raw = false) { if ($secretKey->isAsymmetricKey()) { throw new CryptoAlert\InvalidKey('Expected a symmetric key, not an asymmetric key'); } if (!$secretKey->isEncryptionKey()) { throw new CryptoAlert\InvalidKey('Encryption key expected'); } $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES); $salt = \Sodium\randombytes_buf(Config::HKDF_SALT_LEN); list($eKey, $aKey) = self::splitKeys($secretKey, $salt); $xored = \Sodium\crypto_stream_xor($plaintext, $nonce, $eKey); $auth = self::calculateMAC(Config::HALITE_VERSION . $salt . $nonce . $xored, $aKey); \Sodium\memzero($eKey); \Sodium\memzero($aKey); if (!$raw) { return \Sodium\bin2hex(Config::HALITE_VERSION . $salt . $nonce . $xored . $auth); } return Config::HALITE_VERSION . $salt . $nonce . $xored . $auth; }
/** * Get a relative BLAKE2b hash of an input. Formatted as two lookup * directories followed by a cache entry. 'hh/hh/hhhhhhhh...' * * @param string $preHash The cache identifier (will be hashed) * @param bool $asString Return a string? * @return string|array * @throws InvalidType */ public static function getRelativeHash(string $preHash, bool $asString = false) { $state = State::instance(); $cacheKey = $state->keyring['cache.hash_key']; if (!$cacheKey instanceof Key) { throw new InvalidType(\trk('errors.type.wrong_class', '\\ParagonIE\\Halite\\Key')); } // We use a keyed hash, with a distinct key per Airship deployment to // make collisions unlikely, $hash = \Sodium\crypto_generichash($preHash, $cacheKey->getRawKeyMaterial(), self::HASH_SIZE); $relHash = [\Sodium\bin2hex($hash[0]), \Sodium\bin2hex($hash[1]), \Sodium\bin2hex(Util::subString($hash, 2))]; if ($asString) { return \implode(DIRECTORY_SEPARATOR, $relHash); } return $relHash; }