/** * 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; }
public function testLegacyDerive() { $key = KeyFactory::deriveEncryptionKey('apple', "\t\n\v\f\r" . "", true); $this->assertEquals($key->getRawKeyMaterial(), "6�¹je\r��~^X��" . "63�u��7�B�TX-", true); $salt = \Sodium\hex2bin('762ce4cabd543065172236de1027536ad52ec4c9133ced3766ff319f10301888'); // Issue #10 $enc_secret = KeyFactory::deriveEncryptionKey('correct horse battery staple', $salt, Key::ENCRYPTION | Key::SECRET_KEY); $this->assertTrue($enc_secret->isEncryptionKey()); }
/** * Get the configuration for this version of halite * * @param string $stored A stored password hash * @return SymmetricConfig * @throws InvalidMessage */ protected static function getConfig(string $stored) : SymmetricConfig { $length = Util::safeStrlen($stored); // This doesn't even have a header. if ($length < 8) { throw new InvalidMessage('Encrypted password hash is way too short.'); } if (\hash_equals(Util::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { return SymmetricConfig::getConfig(Base64UrlSafe::decode($stored), 'encrypt'); } $v = \Sodium\hex2bin(Util::safeSubstr($stored, 0, 8)); return SymmetricConfig::getConfig($v, 'encrypt'); }
/** * Reload the signing keys * * @param array $data * @return Supplier */ public function reloadSigningKeys(array $data = []) : self { if (empty($data)) { $data = \Airship\loadJSON(ROOT . '/config/supplier_keys/' . $this->name . '.json'); } if (isset($data['signing_keys'])) { $keys = []; foreach ($data['signing_keys'] as $sk) { $keys[] = ['type' => $sk['type'], 'key' => new SignaturePublicKey(\Sodium\hex2bin($sk['public_key']))]; } $this->signing_keys = $keys; } return $this; }
/** * $secret should each be a 128-character hexademical value. * * This will be broken into 2 64-character parts: crypto secret and auth secret. * * While in memory these are stored as OpaqueProperty, to obscure from debug code or stacktraces. * * @param string $secret * * @throws CryptoException */ public function __construct($secret) { if (!extension_loaded(self::LIBSODIUM_EXT)) { throw new CryptoException(self::ERR_LIBSODIUM); } if (!function_exists(self::FQDN_LIBSODIUM_VERSION)) { throw new CryptoException(self::ERR_LIBSODIUM); } if (!function_exists(self::FQDN_RANDOMBYTES)) { throw new CryptoException(self::ERR_CSPRNG); } if (1 !== preg_match(sprintf('#%s#', self::REGEX_FULL_SECRET), $secret)) { throw new CryptoException(self::ERR_INVALID_SECRET); } $this->cryptoSecret = new OpaqueProperty(\Sodium\hex2bin(ByteString::substr($secret, 0, 64))); $this->authSecret = new OpaqueProperty(\Sodium\hex2bin(ByteString::substr($secret, 64))); }
/** * Channel constructor. * * @param object $parent (Continuum or Keyggdrasil) * @param string $name * @param array $config * @throws \TypeError */ public function __construct($parent, string $name, array $config = []) { if ($parent instanceof Keyggdrasil || $parent instanceof Continuum) { $this->parent = $parent; } if (!\is1DArray($config['urls'])) { throw new \TypeError(\trk('errors.type.expected_1d_array')); } // The channel should be signing responses at the application layer: $this->publicKey = new SignaturePublicKey(\Sodium\hex2bin($config['publickey'])); $this->name = $name; foreach (\array_values($config['urls']) as $index => $url) { if (!\is_string($url)) { throw new \TypeError(\trk('errors.type.expected_string_array', \gettype($url), $index)); } $this->urls[] = $url; } }
/** * Converts hexadecimal data to binary data. * * @param string $string String containing hexadecimal data to convert to binary. * @param string $ignore String characters to ignore in binary conversion. * @return string */ public static function hex2bin($string, $ignore = '') { return \Sodium\hex2bin($string, $ignore); }
$db->beginTransaction(); foreach ($updates as $update) { $db->update('airship_package_cache', ['skyport_metadata' => \json_encode($update['metadata'])], ['packagetype' => $update['package']['type'], 'supplier' => $update['package']['supplier'], 'name' => $update['package']['name']]); } return $db->commit(); } $channels = \Airship\loadJSON(ROOT . '/config/channels.json'); $state = State::instance(); $lastScan = \file_get_contents(ROOT . '/tmp/last_metadata_update.txt'); if ($lastScan === false) { $lastScan = '1970-01-01\\T00:00:00'; } $db = \Airship\get_database(); if ($state->hail instanceof Hail) { foreach ($channels as $identifier => $channel) { $publicKey = new SignaturePublicKey(\Sodium\hex2bin($channel['public_key'])); foreach ($channel['urls'] as $url) { try { $updates = $state->hail->postSignedJSON($url . '/packageMeta', $publicKey, ['since' => $lastScan]); if ($updates['status'] === 'success') { if (\processUpdates($db, ['packageMetadata'])) { file_put_contents(ROOT . '/tmp/last_metadata_update.txt', (new \DateTime())->format(\AIRSHIP_DATE_FORMAT)); exit(0); } } } catch (NoAPIResponse $ex) { } } } } exit(255);
/** * Get the new public key as a SignaturePublicKey object * * @return SignaturePublicKey */ public function getPublicKeyObject() : SignaturePublicKey { return new SignaturePublicKey(\Sodium\hex2bin($this->newPublicKey)); }
/** * 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']]; }
/** * 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'}; }
/** * Verify a MAC, given a MAC key * * @param string $message * @param AuthenticationKey $secretKey * @param string $mac * @param boolean $raw * @return boolean */ public static function verify($message, Contract\KeyInterface $secretKey, $mac, $raw = false) { if (!$secretKey instanceof AuthenticationKey) { throw new CryptoException\InvalidKey('Expected an instance of AuthenticationKey'); } if (!$raw) { $mac = \Sodium\hex2bin($mac); } return self::verifyMAC($mac, $message, $secretKey->get()); }
/** * Verify that this key update was signed by the master key for this supplier. * * @param array $supplierData * @param array $keyData * @param array $nodeData * @return bool */ protected function verifyMasterSignature(array $supplierData, array $keyData, array $nodeData) : bool { $masterData = \json_decode($nodeData['master'], true); if ($masterData === false) { return false; } foreach ($supplierData['signing_keys'] as $key) { if ($key['type'] !== 'master') { continue; } if (\hash_equals($keyData['public_key'], $masterData['public_key'])) { $publicKey = new SignaturePublicKey(\Sodium\hex2bin($masterData['public_key'])); $message = \json_encode($keyData); // If the signature is valid, we return true. return AsymmetricCrypto::verify($message, $publicKey, $masterData['signature']); } } // Fail closed. return false; }
/** * Read a key from a file, verify its checksum * * @param string $filePath * @return string * @throws Alerts\CannotPerformOperation */ protected static function loadKeyFile(string $filePath) : string { $filedata = \file_get_contents($filePath); if ($filedata === false) { throw new Alerts\CannotPerformOperation('Cannot load key from file: ' . $filePath); } $data = \Sodium\hex2bin($filedata); \Sodium\memzero($filedata); return self::getKeyDataFromString($data); }
/** * Verify a signed message with the correct public key * * @param string $message Message to verify * @param SignaturePublicKey $publicKey * @param string $signature * @param boolean $raw Don't hex decode the input? * * @return boolean * * @throws CryptoException\InvalidKey * @throws CryptoException\CannotPerformOperation */ public static function verify($message, Contract\KeyInterface $publicKey, $signature, $raw = false) { if (!$publicKey instanceof SignaturePublicKey) { throw new CryptoException\InvalidKey('Argument 2: Expected an instance of SignaturePublicKey'); } if (!$raw) { $signature = \Sodium\hex2bin($signature); } return \Sodium\crypto_sign_verify_detached($signature, $message, $publicKey->get()); }
/** * Get the signature * * @param bool $hex * @return string */ public function getSignature(bool $hex = false) : string { $signature = $this->releaseInfo['signature']; if (!$hex) { return \Sodium\hex2bin($signature); } return $signature; }
/** * Verify a MAC, given a MAC key * * @param string $message * @param AuthenticationKey $secretKey * @param string $mac * @param boolean $raw * @return boolean */ public static function verify(string $message, AuthenticationKey $secretKey, string $mac, bool $raw = false) : bool { if (!$raw) { $mac = \Sodium\hex2bin($mac); } return self::verifyMAC($mac, $message, $secretKey->getRawKeyMaterial()); }
/** * @return array * @throws \Exception */ protected final function getSkyport() : array { $sp = $this->config['skyports']; if (empty($sp)) { throw new \Exception("No skyports configured"); } if (\count($sp) === 1) { $ret = \array_shift($sp); return [$ret['url'], new SignaturePublicKey(\Sodium\hex2bin($ret['public_key']))]; } $k = \array_keys($sp); $i = $k[\random_int(0, \count($sp) - 1)]; $ret = $sp[$i]; return [$ret['url'], new SignaturePublicKey(\Sodium\hex2bin($ret['public_key']))]; }
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; }
/** * Verify a signed message with the correct public key * * @param string $message Message to verify * @param SignaturePublicKey $publicKey * @param string $signature * @param boolean $raw Don't hex decode the input? * @return bool * @throws CryptoException\InvalidSignature */ public static function verify(string $message, SignaturePublicKey $publicKey, string $signature, bool $raw = false) : bool { if (!$raw) { $signature = \Sodium\hex2bin($signature); } if (CryptoUtil::safeStrlen($signature) !== \Sodium\CRYPTO_SIGN_BYTES) { throw new CryptoException\InvalidSignature('Signature is not the correct length; is it encoded?'); } return \Sodium\crypto_sign_verify_detached($signature, $message, $publicKey->getRawKeyMaterial()); }
/** * Verify a signed message with the correct public key * * @param string $message Message to verify * @param Key $publickey * @param string $signature * @param boolean $raw Don't hex decode the input? * * @return boolean */ public static function verify($message, Contract\CryptoKeyInterface $publickey, $signature, $raw = false) { if (!$publickey->isSigningKey()) { throw new CryptoAlert\InvalidKey('Expected a signing key'); } if (!$publickey->isPublicKey()) { throw new CryptoAlert\InvalidKey('Expected a public key'); } if (!$raw) { $signature = \Sodium\hex2bin($signature); } return \Sodium\crypto_sign_verify_detached($signature, $message, $publickey->get()); }
/** * 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; }
/** * Authenticate a user by a long-term authentication token (e.g. a cookie). * * @param string $token * @return mixed int * @throws LongTermAuthAlert */ public function loginByToken(string $token = '') : int { $table = $this->db->escapeIdentifier($this->tableConfig['table']['longterm']); $f = ['selector' => $this->db->escapeIdentifier($this->tableConfig['fields']['longterm']['selector']), 'userid' => $this->tableConfig['fields']['longterm']['userid'], 'validator' => $this->tableConfig['fields']['longterm']['validator']]; try { $decoded = Base64::decode($token); } catch (\RangeException $ex) { throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token')); } if ($decoded === false) { throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token')); } elseif (Binary::safeStrlen($decoded) !== self::LONG_TERM_AUTH_BYTES) { throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token')); } \Sodium\memzero($token); $sel = Binary::safeSubstr($decoded, 0, self::SELECTOR_BYTES); $val = CryptoUtil::raw_hash(Binary::safeSubstr($decoded, self::SELECTOR_BYTES)); \Sodium\memzero($decoded); $record = $this->db->row('SELECT * FROM ' . $table . ' WHERE ' . $f['selector'] . ' = ?', Base64::encode($sel)); if (empty($record)) { \Sodium\memzero($val); throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token')); } $stored = \Sodium\hex2bin($record[$f['validator']]); \Sodium\memzero($record[$f['validator']]); if (!\hash_equals($stored, $val)) { \Sodium\memzero($val); \Sodium\memzero($stored); throw new LongTermAuthAlert(\trk('errors.security.invalid_persistent_token')); } \Sodium\memzero($stored); \Sodium\memzero($val); $userID = (int) $record[$f['userid']]; $_SESSION['session_canary'] = $this->db->cell('SELECT session_canary FROM airship_users WHERE userid = ?', $userID); return $userID; }
/** * Check that the signature is valid for a given Phar * * @param string $path * @param array $manifest * @return bool */ protected function zipSignatureCheck(string $path, array $manifest = []) : bool { $supplier_name = $manifest['supplier']; $zipName = $supplier_name . '.' . $manifest['name'] . '.zip'; $signature = \file_get_contents($path . '/dist/' . $zipName . '.ed25519.sig'); $supplier =& $this->config['suppliers'][$supplier_name]; $numKeys = \count($supplier['signing_keys']); $verified = false; for ($i = 0; $i < $numKeys; ++$i) { // signing key $publicKey = new SignaturePublicKey(\Sodium\hex2bin($supplier['signing_keys'][$i]['public_key']), true); if (File::verify($path . '/dist/' . $zipName, $publicKey, $signature)) { $verified = true; } } return $verified; }
/** * 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; }
/** * Get the updated metadata for a particular package. * * @param string $type * @param string $supplier * @param string $pkg * @return array */ protected function getPackageMetadata(string $type, string $supplier, string $pkg) : array { $state = State::instance(); if (IDE_HACKS) { $state->hail = new Hail(new Client()); } $channels = \Airship\loadJSON(ROOT . "/config/channels.json"); $ch = $state->universal['airship']['trusted-supplier'] ?? 'paragonie'; if (empty($channels[$ch])) { return []; } $publicKey = new SignaturePublicKey(\Sodium\hex2bin($channels[$ch]['publickey'])); foreach ($channels[$ch]['urls'] as $url) { try { $response = $state->hail->postSignedJSON($url, $publicKey, ['type' => $type, 'supplier' => $supplier, 'name' => $pkg]); } catch (NoAPIResponse $ex) { // Continue } } if (empty($response)) { return []; } if ($response['status'] !== 'success') { return []; } return $response['packageMetadata']; }
/** * Converts an hexdecimal string to a binary string. * * This is the same as PHP's hex2bin() implementation, but it is resistant to * timing attacks. * * @link https://paragonie.com/book/pecl-libsodium/read/03-utilities-helpers.md#hex2bin * @param string $hexString The hex string to convert * @param string|null $ignore (optional) Characters to ignore * @throws \Threema\Core\Exception * @return string */ public function hex2bin($hexString, $ignore = null) { /** @noinspection PhpUndefinedNamespaceInspection @noinspection PhpUndefinedFunctionInspection */ return \Sodium\hex2bin($hexString, $ignore); }
/** * Verify a MAC, given a MAC key * * @param string $message * @param \ParagonIE\Halite\Contract\CryptoKeyInterface $secretKey * @param string $mac * @param boolean $raw * @return boolean */ public static function verify($message, Contract\CryptoKeyInterface $secretKey, $mac, $raw = false) { if (!$raw) { $mac = \Sodium\hex2bin($mac); } return self::verifyMAC($mac, $message, $secretKey->get()); }
/** * Get the EncryptionPublicKey used for encrypting password guesses * to give admins insight into the type of attack being launched. * * @param string $publicKey Hex-encoded public key * @return EncryptionPublicKey * @throws SecurityAlert */ public function getLogPublicKey(string $publicKey = '') : EncryptionPublicKey { if (!$publicKey) { $publicKey = $this->config['log-public-key'] ?? null; if (!$publicKey) { throw new SecurityAlert(\__('Encryption public key not configured.')); } } return new EncryptionPublicKey(\Sodium\hex2bin($publicKey)); }