/** * @param string $keyMaterial - The actual key data * @param bool $signing - Is this a signing key? */ public function __construct(string $keyMaterial = '', ...$args) { if (CryptoUtil::safeStrlen($keyMaterial) !== \Sodium\CRYPTO_BOX_PUBLICKEYBYTES) { throw new InvalidKey('Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES bytes long'); } parent::__construct($keyMaterial, false); }
/** * Write to a stream; prevent partial writes * * @param resource $stream * @param string $buf * @param int $num (number of bytes) * @throws FileAlert\AccessDenied */ public function writeBytes($buf, $num = null) { $bufSize = Util::safeStrlen($buf); if ($num === null || $num > $bufSize) { $num = $bufSize; } if ($num < 0) { throw new \Exception('num < 0'); } $remaining = $num; do { if ($remaining <= 0) { break; } $written = \fwrite($this->fp, $buf, $remaining); if ($written === false) { throw new CryptoException\FileAccessDenied('Could not write to the file'); } $buf = Util::safeSubstr($buf, $written, null); $this->pos += $written; $this->stat = \fstat($this->fp); $remaining -= $written; } while ($remaining > 0); return $num; }
/** * @param string $keyMaterial - The actual key data * @param bool $signing - Is this a signing key? */ public function __construct(string $keyMaterial = '', ...$args) { if (CryptoUtil::safeStrlen($keyMaterial) !== \Sodium\CRYPTO_SIGN_SECRETKEYBYTES) { throw new InvalidKey('Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long'); } parent::__construct($keyMaterial, true); }
/** * @param string $keyMaterial - The actual key data */ public function __construct(string $keyMaterial = '', ...$args) { if (CryptoUtil::safeStrlen($keyMaterial) !== \Sodium\CRYPTO_AUTH_KEYBYTES) { throw new InvalidKey('Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long'); } parent::__construct($keyMaterial, true); }
/** * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey */ public function __construct(HiddenString $keyMaterial) { if (CryptoUtil::safeStrlen($keyMaterial->getString()) !== \Sodium\CRYPTO_BOX_SECRETKEYBYTES) { throw new InvalidKey('Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES bytes long'); } parent::__construct($keyMaterial); }
/** * Read from a stream; prevent partial reads (also uses run-time testing to * prevent partial reads -- you can turn this off if you need performance * and aren't concerned about race condition attacks, but this isn't a * decision to make lightly!) * * @param int $num * @param boolean $skipTests Only set this to TRUE if you're absolutely sure * that you don't want to defend against TOCTOU / * race condition attacks on the filesystem! * @return string * @throws FileAlert\AccessDenied */ public function readBytes($num, $skipTests = false) { if ($num <= 0) { throw new \Exception('num < 0'); } if ($this->pos + $num > $this->stat['size']) { throw new \Exception('Out-of-bounds read'); } $buf = ''; $remaining = $num; if (!$skipTests) { $this->toctouTest(); } do { if ($remaining <= 0) { break; } $read = \fread($this->fp, $remaining); if ($read === false) { throw new CryptoException\FileAccessDenied('Could not read from the file'); } $buf .= $read; $readSize = Util::safeStrlen($read); $this->pos += $readSize; $remaining -= $readSize; } while ($remaining > 0); return $buf; }
/** * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey */ public function __construct(HiddenString $keyMaterial) { if (CryptoUtil::safeStrlen($keyMaterial->getString()) !== \Sodium\CRYPTO_AUTH_KEYBYTES) { throw new InvalidKey('Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long'); } parent::__construct($keyMaterial); $this->isSigningKey = true; }
/** * test safeStrLen() with illegal parameter. We expect to see an exception * @return void * @throws CannotPerformOperation */ public function testSafeStrlen() { $this->setExpectedException('\\ParagonIE\\Halite\\Alerts\\HaliteAlert'); $teststring = []; // is not a string, will provoke a warning //suppress php warning Util::safeStrlen($teststring); }
/** * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey */ public function __construct(HiddenString $keyMaterial) { if (CryptoUtil::safeStrlen($keyMaterial->getString()) !== \Sodium\CRYPTO_SIGN_SECRETKEYBYTES) { throw new InvalidKey('Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long'); } parent::__construct($keyMaterial); $this->isSigningKey = true; }
/** * 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'); }
/** * Decrypt a message using the Halite encryption protocol * * @param string $ciphertext * @param Key $secretKey * @param boolean $raw Don't hex decode the input? */ public static function decrypt($ciphertext, 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'); } if (!$raw) { // We were given hex data: $ciphertext = \Sodium\hex2bin($ciphertext); } $length = CryptoUtil::safeStrlen($ciphertext); // The first 4 bytes are reserved for the version size $version = CryptoUtil::safeSubstr($ciphertext, 0, Config::VERSION_TAG_LEN); // The HKDF is used for key splitting $salt = CryptoUtil::safeSubstr($ciphertext, Config::VERSION_TAG_LEN, Config::HKDF_SALT_LEN); // This is the nonce (we authenticated it): $nonce = CryptoUtil::safeSubstr($ciphertext, Config::VERSION_TAG_LEN + Config::HKDF_SALT_LEN, \Sodium\CRYPTO_STREAM_NONCEBYTES); // This is the crypto_stream_xor()ed ciphertext $xored = CryptoUtil::safeSubstr($ciphertext, Config::VERSION_TAG_LEN + Config::HKDF_SALT_LEN + \Sodium\CRYPTO_STREAM_NONCEBYTES, $length - (Config::VERSION_TAG_LEN + Config::HKDF_SALT_LEN + \Sodium\CRYPTO_STREAM_NONCEBYTES + \Sodium\CRYPTO_AUTH_BYTES)); // $auth is the last 32 bytes $auth = CryptoUtil::safeSubstr($ciphertext, $length - \Sodium\CRYPTO_AUTH_BYTES); // Split our keys list($eKey, $aKey) = self::splitKeys($secretKey, $salt); // Check the MAC first if (!self::verifyMAC($auth, $version . $salt . $nonce . $xored, $aKey)) { throw new CryptoAlert\InvalidMessage('Invalid message authenticaiton code'); } // Down the road, do whatever logic around $version here, in case we // need to upgrade our protocol. // Add version logic above $plaintext = \Sodium\crypto_stream_xor($xored, $nonce, $eKey); if ($plaintext === false) { throw new CryptoAlert\InvalidMessage('Invalid message authenticaiton code'); } return $plaintext; }
/** * Read from a stream; prevent partial reads * * @param resource $stream * @param int $num * @throws FileAlert\AccessDenied */ private static final function readBytes($stream, $num) { if ($num <= 0) { throw new \Exception('num < 0'); } $buf = ''; $remaining = $num; do { if ($remaining <= 0) { break; } $read = \fread($stream, $remaining); if ($read === false) { throw new FileAlert\AccessDenied('Could not read from the file'); } $buf .= $read; $remaining = $num - CryptoUtil::safeStrlen($buf); } while ($remaining > 0); return $buf; }
/** * Decrypt then verify a password * * @param HiddenString $password The user's password * @param string $stored The encrypted password hash * @param EncryptionKey $secretKey The master key for all passwords * @return bool Is this password valid? * @throws InvalidMessage */ public static function verify(HiddenString $password, string $stored, EncryptionKey $secretKey) : bool { $config = self::getConfig($stored); // Base64-urlsafe encoded, so 4/3 the size of raw binary if (Util::safeStrlen($stored) < $config->SHORTEST_CIPHERTEXT_LENGTH * 4 / 3) { throw new InvalidMessage('Encrypted password hash is too short.'); } // First let's decrypt the hash $hash_str = Crypto::decrypt($stored, $secretKey, $config->ENCODING); // Upon successful decryption, verify the password is correct return \Sodium\crypto_pwhash_str_verify($hash_str->getString(), $password->getString()); }
/** * @covers Symmetric::unpackMessageForDecryption() */ public function testUnpack() { $key = new EncryptionKey(new HiddenString(\str_repeat('A', 32))); // Randomly sized plaintext $size = \Sodium\randombytes_uniform(1023) + 1; $plaintext = \Sodium\randombytes_buf($size); $message = Symmetric::encrypt(new HiddenString($plaintext), $key, true); // Let's unpack our message $unpacked = Symmetric::unpackMessageForDecryption($message); // Now to test our expected results! $this->assertSame(Util::safeStrlen($unpacked[0]), Halite::VERSION_TAG_LEN); $this->assertTrue($unpacked[1] instanceof \ParagonIE\Halite\Symmetric\Config); $config = $unpacked[1]; if ($config instanceof \ParagonIE\Halite\Symmetric\Config) { $this->assertSame(Util::safeStrlen($unpacked[2]), $config->HKDF_SALT_LEN); $this->assertSame(Util::safeStrlen($unpacked[3]), \Sodium\CRYPTO_STREAM_NONCEBYTES); $this->assertSame(Util::safeStrlen($unpacked[4]), Util::safeStrlen($message) - (Halite::VERSION_TAG_LEN + $config->HKDF_SALT_LEN + \Sodium\CRYPTO_STREAM_NONCEBYTES + $config->MAC_SIZE)); $this->assertSame(Util::safeStrlen($unpacked[5]), $config->MAC_SIZE); } else { $this->fail('Cannot continue'); } }
/** * Unpack a message string into an array. * * @param string $ciphertext * @return array */ public static function unpackMessageForDecryption($ciphertext) { $length = CryptoUtil::safeStrlen($ciphertext); // The first 4 bytes are reserved for the version size $version = CryptoUtil::safeSubstr($ciphertext, 0, Halite::VERSION_TAG_LEN); $config = SymmetricConfig::getConfig($version, 'encrypt'); // The HKDF is used for key splitting $salt = CryptoUtil::safeSubstr($ciphertext, Halite::VERSION_TAG_LEN, $config->HKDF_SALT_LEN); // This is the nonce (we authenticated it): $nonce = CryptoUtil::safeSubstr($ciphertext, Halite::VERSION_TAG_LEN + $config->HKDF_SALT_LEN, \Sodium\CRYPTO_STREAM_NONCEBYTES); // This is the crypto_stream_xor()ed ciphertext $xored = CryptoUtil::safeSubstr($ciphertext, Halite::VERSION_TAG_LEN + $config->HKDF_SALT_LEN + \Sodium\CRYPTO_STREAM_NONCEBYTES, $length - (Halite::VERSION_TAG_LEN + $config->HKDF_SALT_LEN + \Sodium\CRYPTO_STREAM_NONCEBYTES + \Sodium\CRYPTO_AUTH_BYTES)); // $auth is the last 32 bytes $auth = CryptoUtil::safeSubstr($ciphertext, $length - \Sodium\CRYPTO_AUTH_BYTES); // We don't need this anymore. \Sodium\memzero($ciphertext); return [$version, $config, $salt, $nonce, $xored, $auth]; }
/** * Derive a key pair for public key signatures from a password and salt * * @param HiddenString $password * @param string $salt * @param string $level Security level for KDF * * @return SignatureKeyPair * @throws CryptoException\InvalidSalt */ public static function deriveSignatureKeyPair(HiddenString $password, string $salt, string $level = self::INTERACTIVE) : SignatureKeyPair { $kdfLimits = self::getSecurityLevels($level); // VERSION 2+ (argon2) if (Util::safeStrlen($salt) !== \Sodium\CRYPTO_PWHASH_SALTBYTES) { throw new CryptoException\InvalidSalt('Expected ' . \Sodium\CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Util::safeStrlen($salt)); } // Digital signature keypair $seed = \Sodium\crypto_pwhash(\Sodium\CRYPTO_SIGN_SEEDBYTES, $password->getString(), $salt, $kdfLimits[0], $kdfLimits[1]); $keyPair = \Sodium\crypto_sign_seed_keypair($seed); $secretKey = \Sodium\crypto_sign_secretkey($keyPair); // Let's wipe our $kp variable \Sodium\memzero($keyPair); return new SignatureKeyPair(new SignatureSecretKey(new HiddenString($secretKey))); }