From 494d20efc89b01992cdc7687c4aadc9ef490c533 Mon Sep 17 00:00:00 2001 From: terrafrost Date: Mon, 18 Mar 2019 06:59:00 -0500 Subject: [PATCH] add Salsa20 / ChaCha20 stream ciphers --- phpseclib/Crypt/ChaCha20.php | 783 ++++++++++++++++++++++++ phpseclib/Crypt/Common/SymmetricKey.php | 222 +++++-- phpseclib/Crypt/RC4.php | 3 + phpseclib/Crypt/Salsa20.php | 541 ++++++++++++++++ phpseclib/Net/SSH2.php | 251 ++++++-- tests/Unit/Crypt/ChaCha20.php | 218 +++++++ tests/Unit/Crypt/Salsa20.php | 160 +++++ 7 files changed, 2076 insertions(+), 102 deletions(-) create mode 100644 phpseclib/Crypt/ChaCha20.php create mode 100644 phpseclib/Crypt/Salsa20.php create mode 100644 tests/Unit/Crypt/ChaCha20.php create mode 100644 tests/Unit/Crypt/Salsa20.php diff --git a/phpseclib/Crypt/ChaCha20.php b/phpseclib/Crypt/ChaCha20.php new file mode 100644 index 00000000..0f9e90c1 --- /dev/null +++ b/phpseclib/Crypt/ChaCha20.php @@ -0,0 +1,783 @@ + + * @copyright 2019 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +use phpseclib\Crypt\Common\StreamCipher; +use phpseclib\Exception\InsufficientSetupException; +use phpseclib\Exception\BadDecryptionException; + +/** + * Pure-PHP implementation of ChaCha20. + * + * @package ChaCha20 + * @author Jim Wigginton + * @access public + */ +class ChaCha20 extends Salsa20 +{ + /** + * The OpenSSL specific name of the cipher + * + * @var string + */ + protected $cipher_name_openssl = 'chacha20'; + + /** + * Test for engine validity + * + * This is mainly just a wrapper to set things up for \phpseclib\Crypt\Common\SymmetricKey::isValidEngine() + * + * @see \phpseclib\Crypt\Common\SymmetricKey::__construct() + * @param int $engine + * @access protected + * @return bool + */ + protected function isValidEngineHelper($engine) + { + switch ($engine) { + case self::ENGINE_LIBSODIUM: + // PHP 7.2.0 (30 Nov 2017) added support for libsodium + + // we could probably make it so that if $this->counter == 0 then the first block would be done with either OpenSSL + // or PHP and then subsequent blocks would then be done with libsodium but idk - it's not a high priority atm + + // we could also make it so that if $this->counter == 0 and $this->continuousBuffer then do the first string + // with libsodium and subsequent strings with openssl or pure-PHP but again not a high priority + return function_exists('sodium_crypto_aead_chacha20poly1305_ietf_encrypt') && + $this->key_length == 32 && + (($this->usePoly1305 && !isset($this->poly1305Key) && $this->counter == 0) || $this->counter == 1) && + !$this->continuousBuffer; + case self::ENGINE_OPENSSL: + // OpenSSL 1.1.0 (released 25 Aug 2016) added support for chacha20. + // PHP didn't support OpenSSL 1.1.0 until 7.0.19 (11 May 2017) + + // if you attempt to provide openssl with a 128 bit key (as opposed to a 256 bit key) openssl will null + // pad the key to 256 bits and still use the expansion constant for 256-bit keys. the fact that + // openssl treats the IV as both the counter and nonce, however, let's us use openssl in continuous mode + // whereas libsodium does not + if ($this->key_length != 32) { + return false; + } + } + + return parent::isValidEngineHelper($engine); + } + + /** + * Encrypts a message. + * + * @see \phpseclib\Crypt\Common\SymmetricKey::decrypt() + * @see self::crypt() + * @param string $plaintext + * @return string $ciphertext + */ + public function encrypt($plaintext) + { + $this->setup(); + + if ($this->engine == self::ENGINE_LIBSODIUM) { + return $this->encrypt_with_libsodium($plaintext); + } + + return parent::encrypt($plaintext); + } + + /** + * Decrypts a message. + * + * $this->decrypt($this->encrypt($plaintext)) == $this->encrypt($this->encrypt($plaintext)). + * At least if the continuous buffer is disabled. + * + * @see \phpseclib\Crypt\Common\SymmetricKey::encrypt() + * @see self::crypt() + * @param string $ciphertext + * @return string $plaintext + */ + public function decrypt($ciphertext) + { + $this->setup(); + + if ($this->engine == self::ENGINE_LIBSODIUM) { + return $this->decrypt_with_libsodium($ciphertext); + } + + return parent::decrypt($ciphertext); + } + + /** + * Encrypts a message with libsodium + * + * @see self::encrypt() + * @param string $plaintext + * @return string $text + */ + private function encrypt_with_libsodium($plaintext) + { + $params = [$plaintext, $this->aad, $this->nonce, $this->key]; + $ciphertext = strlen($this->nonce) == 8 ? + sodium_crypto_aead_chacha20poly1305_encrypt(...$params) : + sodium_crypto_aead_chacha20poly1305_ietf_encrypt(...$params); + if (!$this->usePoly1305) { + return substr($ciphertext, 0, strlen($plaintext)); + } + + $newciphertext = substr($ciphertext, 0, strlen($plaintext)); + + $this->newtag = $this->usingGeneratedPoly1305Key && strlen($this->nonce) == 12 ? + substr($ciphertext, strlen($plaintext)) : + $this->poly1305($newciphertext); + + return $newciphertext; + } + + /** + * Decrypts a message with libsodium + * + * @see self::decrypt() + * @param string $ciphertext + * @return string $text + */ + private function decrypt_with_libsodium($ciphertext) + { + $params = [$ciphertext, $this->aad, $this->nonce, $this->key]; + + if (isset($this->poly1305Key)) { + if ($this->oldtag === false) { + throw new InsufficientSetupException('Authentication Tag has not been set'); + } + if ($this->usingGeneratedPoly1305Key && strlen($this->nonce) == 12) { + $plaintext = sodium_crypto_aead_chacha20poly1305_ietf_decrypt(...$params); + $this->oldtag = false; + if ($plaintext === false) { + throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match'); + } + return $plaintext; + } + $newtag = $this->poly1305($ciphertext); + if ($this->oldtag != substr($newtag, 0, strlen($this->oldtag))) { + $this->oldtag = false; + throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match'); + } + $this->oldtag = false; + } + + $plaintext = strlen($this->nonce) == 8 ? + sodium_crypto_aead_chacha20poly1305_encrypt(...$params) : + sodium_crypto_aead_chacha20poly1305_ietf_encrypt(...$params); + + return substr($plaintext, 0, strlen($ciphertext)); + } + + /** + * Sets the nonce. + * + * @param string $nonce + */ + public function setNonce($nonce) + { + if (!is_string($nonce)) { + throw new \UnexpectedValueException('The nonce should be a string'); + } + + /* + from https://tools.ietf.org/html/rfc7539#page-7 + + "Note also that the original ChaCha had a 64-bit nonce and 64-bit + block count. We have modified this here to be more consistent with + recommendations in Section 3.2 of [RFC5116]." + */ + switch (strlen($nonce)) { + case 8: // 64 bits + case 12: // 96 bits + break; + default: + throw new \LengthException('Nonce of size ' . strlen($nonce) . ' not supported by this algorithm. Only 64-bit nonces or 96-bit nonces are supported'); + } + + $this->nonce = $nonce; + $this->changed = true; + $this->setEngine(); + } + + /** + * Setup the self::ENGINE_INTERNAL $engine + * + * (re)init, if necessary, the internal cipher $engine + * + * _setup() will be called each time if $changed === true + * typically this happens when using one or more of following public methods: + * + * - setKey() + * + * - setNonce() + * + * - First run of encrypt() / decrypt() with no init-settings + * + * @see self::setKey() + * @see self::setNonce() + * @see self::disableContinuousBuffer() + */ + protected function setup() + { + if (!$this->changed) { + return; + } + + $this->enbuffer = $this->debuffer = ['ciphertext' => '', 'counter' => $this->counter]; + + $this->changed = false; + + if ($this->nonce === false) { + throw new InsufficientSetupException('No nonce has been defined'); + } + + if ($this->key === false) { + throw new InsufficientSetupException('No key has been defined'); + } + + if ($this->usePoly1305 && !isset($this->poly1305Key)) { + $this->usingGeneratedPoly1305Key = true; + if ($this->engine == self::ENGINE_LIBSODIUM) { + return; + } + $this->createPoly1305Key(); + } + + $key = $this->key; + if (strlen($key) == 16) { + $constant = 'expand 16-byte k'; + $key.= $key; + } else { + $constant = 'expand 32-byte k'; + } + + $this->p1 = $constant . $key; + $this->p2 = $this->nonce; + if (strlen($this->nonce) == 8) { + $this->p2 = "\0\0\0\0" . $this->p2; + } + } + + /** + * The quarterround function + * + * @param int $a + * @param int $b + * @param int $c + * @param int $d + */ + protected static function quarterRound(&$a, &$b, &$c, &$d) + { + $a+= $b; $d = self::leftRotate($d ^ $a, 16); + $c+= $d; $b = self::leftRotate($b ^ $c, 12); + $a+= $b; $d = self::leftRotate($d ^ $a, 8); + $c+= $d; $b = self::leftRotate($b ^ $c, 7); + } + + /** + * The doubleround function + * + * @param int $x0...$x16 + */ + protected static function doubleRound(&$x0, &$x1, &$x2, &$x3, &$x4, &$x5, &$x6, &$x7, &$x8, &$x9, &$x10, &$x11, &$x12, &$x13, &$x14, &$x15) + { + // columnRound + static::quarterRound($x0, $x4, $x8, $x12); + static::quarterRound($x1, $x5, $x9, $x13); + static::quarterRound($x2, $x6, $x10, $x14); + static::quarterRound($x3, $x7, $x11, $x15); + // rowRound + static::quarterRound($x0, $x5, $x10, $x15); + static::quarterRound($x1, $x6, $x11, $x12); + static::quarterRound($x2, $x7, $x8, $x13); + static::quarterRound($x3, $x4, $x9, $x14); + } + + /** + * The Salsa20 hash function function + * + * On my laptop this loop unrolled / function dereferenced version of parent::salsa20 encrypts 1mb of text in + * 0.65s vs the 0.85s that it takes with the parent method. + * + * If we were free to assume that the host OS would always be 64-bits then the if condition in leftRotate could + * be eliminated and we could knock this done to 0.60s. + * + * For comparison purposes, RC4 takes 0.16s and AES in CTR mode with the Eval engine takes 0.48s. + * AES in CTR mode with the PHP engine takes 1.19s. Salsa20 / ChaCha20 do not benefit as much from the Eval + * approach due to the fact that there are a lot less variables to de-reference, fewer loops to unroll, etc + * + * @param string $x + */ + protected static function salsa20($x) + { + list(, $x0, $x1, $x2, $x3, $x4, $x5, $x6, $x7, $x8, $x9, $x10, $x11, $x12, $x13, $x14, $x15) = unpack('V*', $x); + $z0 = $x0; + $z1 = $x1; + $z2 = $x2; + $z3 = $x3; + $z4 = $x4; + $z5 = $x5; + $z6 = $x6; + $z7 = $x7; + $z8 = $x8; + $z9 = $x9; + $z10 = $x10; + $z11 = $x11; + $z12 = $x12; + $z13 = $x13; + $z14 = $x14; + $z15 = $x15; + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + // columnRound + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 16); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 12); + $x0+= $x4; $x12 = self::leftRotate($x12 ^ $x0, 8); + $x8+= $x12; $x4 = self::leftRotate($x4 ^ $x8, 7); + + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 16); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 12); + $x1+= $x5; $x13 = self::leftRotate($x13 ^ $x1, 8); + $x9+= $x13; $x5 = self::leftRotate($x5 ^ $x9, 7); + + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 16); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 12); + $x2+= $x6; $x14 = self::leftRotate($x14 ^ $x2, 8); + $x10+= $x14; $x6 = self::leftRotate($x6 ^ $x10, 7); + + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 16); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 12); + $x3+= $x7; $x15 = self::leftRotate($x15 ^ $x3, 8); + $x11+= $x15; $x7 = self::leftRotate($x7 ^ $x11, 7); + + // rowRound + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 16); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 12); + $x0+= $x5; $x15 = self::leftRotate($x15 ^ $x0, 8); + $x10+= $x15; $x5 = self::leftRotate($x5 ^ $x10, 7); + + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 16); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 12); + $x1+= $x6; $x12 = self::leftRotate($x12 ^ $x1, 8); + $x11+= $x12; $x6 = self::leftRotate($x6 ^ $x11, 7); + + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 16); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 12); + $x2+= $x7; $x13 = self::leftRotate($x13 ^ $x2, 8); + $x8+= $x13; $x7 = self::leftRotate($x7 ^ $x8, 7); + + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 16); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 12); + $x3+= $x4; $x14 = self::leftRotate($x14 ^ $x3, 8); + $x9+= $x14; $x4 = self::leftRotate($x4 ^ $x9, 7); + + $x0+= $z0; + $x1+= $z1; + $x2+= $z2; + $x3+= $z3; + $x4+= $z4; + $x5+= $z5; + $x6+= $z6; + $x7+= $z7; + $x8+= $z8; + $x9+= $z9; + $x10+= $z10; + $x11+= $z11; + $x12+= $z12; + $x13+= $z13; + $x14+= $z14; + $x15+= $z15; + + return pack('V*', $x0, $x1, $x2, $x3, $x4, $x5, $x6, $x7, $x8, $x9, $x10, $x11, $x12, $x13, $x14, $x15); + } +} diff --git a/phpseclib/Crypt/Common/SymmetricKey.php b/phpseclib/Crypt/Common/SymmetricKey.php index 5e3bd08b..505ddcd2 100644 --- a/phpseclib/Crypt/Common/SymmetricKey.php +++ b/phpseclib/Crypt/Common/SymmetricKey.php @@ -40,6 +40,7 @@ use phpseclib\Crypt\Hash; use phpseclib\Common\Functions\Strings; use phpseclib\Math\BigInteger; use phpseclib\Math\BinaryField; +use phpseclib\Math\PrimeField; use phpseclib\Exception\BadDecryptionException; use phpseclib\Exception\BadModeException; use phpseclib\Exception\InconsistentSetupException; @@ -533,13 +534,43 @@ abstract class SymmetricKey /** * GCM Binary Field * - * @see self::initialize_static_variables() + * @see self::__construct() * @see self::ghash() * @var BinaryField * @access private */ private static $gcmField; + /** + * Poly1305 Prime Field + * + * @see self::enablePoly1305() + * @see self::poly1305() + * @var PrimeField + * @access private + */ + private static $poly1305Field; + + /** + * Poly1305 Key + * + * @see self::setPoly1305Key() + * @see self::poly1305() + * @var string + * @access private + */ + protected $poly1305Key; + + /** + * Poly1305 Flag + * + * @see self::setPoly1305Key() + * @see self::enablePoly1305() + * @var boolean + * @access private + */ + protected $usePoly1305 = false; + /** * The Original Initialization Vector * @@ -649,7 +680,7 @@ abstract class SymmetricKey } if (!$this->usesIV()) { - throw new \BadMethodCallExceptionn('This algorithm does not use an IV.'); + throw new \BadMethodCallException('This algorithm does not use an IV.'); } if (strlen($iv) != $this->block_size) { @@ -660,6 +691,53 @@ abstract class SymmetricKey $this->changed = true; } + /** + * Enables Poly1305 mode. + * + * Once enabled Poly1305 cannot be disabled. + * + * @access public + * @throws \BadMethodCallException if Poly1305 is enabled whilst in GCM mode + */ + public function enablePoly1305() + { + if ($this->mode == self::MODE_GCM) { + throw new \BadMethodCallException('Poly1305 cannot be used in GCM mode'); + } + + $this->usePoly1305 = true; + } + + /** + * Enables Poly1305 mode. + * + * Once enabled Poly1305 cannot be disabled. If $key is not passed then an attempt to call createPoly1305Key + * will be made. + * + * @access public + * @param string $key optional + * @throws \LengthException if the key isn't long enough + * @throws \BadMethodCallException if Poly1305 is enabled whilst in GCM mode + */ + public function setPoly1305Key($key = null) + { + if ($this->mode == self::MODE_GCM) { + throw new \BadMethodCallException('Poly1305 cannot be used in GCM mode'); + } + + if (!is_string($key) || strlen($key) != 32) { + throw new \LengthException('The Poly1305 key must be 32 bytes long (256 bits)'); + } + + if (!isset(self::$poly1305Field)) { + // 2^130-5 + self::$poly1305Field = new PrimeField(new BigInteger('3fffffffffffffffffffffffffffffffb', 16)); + } + + $this->poly1305Key = $key; + $this->usePoly1305 = true; + } + /** * Sets the nonce. * @@ -667,7 +745,7 @@ abstract class SymmetricKey * * @access public * @param string $nonce - * @throws \RuntimeException if an IV is provided when one shouldn't be + * @throws \BadMethodCallException if an nonce is provided when one shouldn't be */ public function setNonce($nonce) { @@ -683,16 +761,16 @@ abstract class SymmetricKey /** * Sets additional authenticated data * - * setAAD() is only used by gcm + * setAAD() is only used by gcm or in poly1305 mode * * @access public * @param string $aad - * @throws \RuntimeException if mode isn't GCM + * @throws \BadMethodCallException if mode isn't GCM or if poly1305 isn't being utilized */ public function setAAD($aad) { - if ($this->mode != self::MODE_GCM) { - throw new \RuntimeException('Additional authenticated data is only utilized in GCM mode'); + if ($this->mode != self::MODE_GCM && !$this->usePoly1305) { + throw new \BadMethodCallException('Additional authenticated data is only utilized in GCM mode or with Poly1305'); } $this->aad = $aad; @@ -1046,6 +1124,15 @@ abstract class SymmetricKey return $ciphertext; } + if (isset($this->poly1305Key)) { + $cipher = clone $this; + unset($cipher->poly1305Key); + $this->usePoly1305 = false; + $ciphertext = $cipher->encrypt($plaintext); + $this->newtag = $this->poly1305($ciphertext); + return $ciphertext; + } + if ($this->engine === self::ENGINE_OPENSSL) { switch ($this->mode) { case self::MODE_STREAM: @@ -1362,27 +1449,35 @@ abstract class SymmetricKey $this->setup(); - if ($this->mode == self::MODE_GCM) { + if ($this->mode == self::MODE_GCM || isset($this->poly1305Key)) { if ($this->oldtag === false) { throw new InsufficientSetupException('Authentication Tag has not been set'); } - $oldIV = $this->iv; - Strings::increment_str($this->iv); - $cipher = new static('ctr'); - $cipher->setKey($this->key); - $cipher->setIV($this->iv); - $plaintext = $cipher->decrypt($ciphertext); + if (isset($this->poly1305Key)) { + $newtag = $this->poly1305($ciphertext); + } else { + $oldIV = $this->iv; + Strings::increment_str($this->iv); + $cipher = new static('ctr'); + $cipher->setKey($this->key); + $cipher->setIV($this->iv); + $plaintext = $cipher->decrypt($ciphertext); - $s = $this->ghash( - self::nullPad128($this->aad) . - self::nullPad128($ciphertext) . - self::len64($this->aad) . - self::len64($ciphertext) - ); - $cipher->encryptIV = $this->iv = $this->encryptIV = $this->decryptIV = $oldIV; - $newtag = $cipher->encrypt($s); + $s = $this->ghash( + self::nullPad128($this->aad) . + self::nullPad128($ciphertext) . + self::len64($this->aad) . + self::len64($ciphertext) + ); + $cipher->encryptIV = $this->iv = $this->encryptIV = $this->decryptIV = $oldIV; + $newtag = $cipher->encrypt($s); + } if ($this->oldtag != substr($newtag, 0, strlen($newtag))) { + $cipher = clone $this; + unset($cipher->poly1305Key); + $this->usePoly1305 = false; + $plaintext = $cipher->decrypt($ciphertext); $this->oldtag = false; throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match'); } @@ -1672,7 +1767,7 @@ abstract class SymmetricKey /** * Get the authentication tag * - * Only used in GCM mode + * Only used in GCM or Poly1305 mode * * @see self::encrypt() * @param int $length optional @@ -1683,13 +1778,17 @@ abstract class SymmetricKey */ public function getTag($length = 16) { - if ($this->mode != self::MODE_GCM) { - throw new \BadMethodCallException('Only GCM mode utilizes authentication tags'); + if ($this->mode != self::MODE_GCM && !$this->usePoly1305) { + throw new \BadMethodCallException('Authentication tags are only utilized in GCM mode or with Poly1305'); } - // the tag is basically a single encrypted block of a 128-bit cipher. it can't be greater than 16 - // bytes because that's bigger than a block is. if it were 0 you might as well be doing CTR and - // less than 4 provides minimal security that could be trivially easily brute forced. + if ($this->newtag === false) { + throw new \BadMethodCallException('A tag can only be returned after a round of encryption has been performed'); + } + + // the tag is 128-bits. it can't be greater than 16 bytes because that's bigger than the tag is. if it + // were 0 you might as well be doing CTR and less than 4 provides minimal security that could be trivially + // easily brute forced. // see https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf#page=36 // for more info if ($length < 4 || $length > 16) { @@ -1714,8 +1813,12 @@ abstract class SymmetricKey */ public function setTag($tag) { - if ($this->mode != self::MODE_GCM) { - throw new \BadMethodCallException('Only GCM mode utilizes authentication tags'); + if ($this->usePoly1305 && !isset($this->poly1305Key) && method_exists($this, 'createPoly1305Key')) { + $this->createPoly1305Key(); + } + + if ($this->mode != self::MODE_GCM && !$this->usePoly1305) { + throw new \BadMethodCallException('Authentication tags are only utilized in GCM mode or with Poly1305'); } $length = strlen($tag); @@ -2030,9 +2133,6 @@ abstract class SymmetricKey { switch ($engine) { case self::ENGINE_OPENSSL: - if ($this->mode == self::MODE_STREAM && $this->continuousBuffer) { - return false; - } $this->openssl_emulate_ctr = false; $result = $this->cipher_name_openssl && extension_loaded('openssl'); @@ -2146,13 +2246,19 @@ abstract class SymmetricKey $this->engine = null; $candidateEngines = [ - $this->preferredEngine, self::ENGINE_LIBSODIUM, self::ENGINE_OPENSSL_GCM, self::ENGINE_OPENSSL, self::ENGINE_MCRYPT, self::ENGINE_EVAL ]; + if (isset($this->preferredEngine)) { + $temp = [$this->preferredEngine]; + $candidateEngines = array_merge( + $temp, + array_diff($candidateEngines, $temp) + ); + } foreach ($candidateEngines as $engine) { if ($this->isValidEngineHelper($engine)) { $this->engine = $engine; @@ -2246,14 +2352,18 @@ abstract class SymmetricKey $this->changed = false; + if ($this->usePoly1305 && !isset($this->poly1305Key) && method_exists($this, 'createPoly1305Key')) { + $this->createPoly1305Key(); + } + $this->enbuffer = $this->debuffer = ['ciphertext' => '', 'xor' => '', 'pos' => 0, 'enmcrypt_init' => true]; //$this->newtag = $this->oldtag = false; - if ($this->mode == self::MODE_GCM) { + if ($this->usesNonce()) { if ($this->nonce === false) { throw new InsufficientSetupException('No nonce has been defined'); } - if (!in_array($this->engine, [self::ENGINE_LIBSODIUM, self::ENGINE_OPENSSL_GCM])) { + if ($this->mode == self::MODE_GCM && !in_array($this->engine, [self::ENGINE_LIBSODIUM, self::ENGINE_OPENSSL_GCM])) { $this->setupGCM(); } } else { @@ -3078,9 +3188,45 @@ abstract class SymmetricKey * @param string $str * @return string */ - private static function nullPad128($str) + protected static function nullPad128($str) { $len = strlen($str); return $str . str_repeat("\0", 16 * ceil($len / 16) - $len); } -} + + /** + * Calculates Poly1305 MAC + * + * On my system ChaCha20, with libsodium, takes 0.5s. With this custom Poly1305 implementation + * it takes 1.2s. + * + * @see self::decrypt() + * @see self::encrypt() + * @access private + * @param string $text + * @return string + */ + protected function poly1305($text) + { + $s = $this->poly1305Key; // strlen($this->poly1305Key) == 32 + $r = Strings::shift($s, 16); + $r = strrev($r); + $r&= "\x0f\xff\xff\xfc\x0f\xff\xff\xfc\x0f\xff\xff\xfc\x0f\xff\xff\xff"; + $s = strrev($s); + + $r = self::$poly1305Field->newInteger(new BigInteger($r, 256)); + $s = self::$poly1305Field->newInteger(new BigInteger($s, 256)); + $a = self::$poly1305Field->newInteger(new BigInteger()); + + $blocks = str_split($text, 16); + foreach ($blocks as $block) { + $n = strrev($block . chr(1)); + $n = self::$poly1305Field->newInteger(new BigInteger($n, 256)); + $a = $a->add($n); + $a = $a->multiply($r); + } + $r = $a->toBigInteger()->add($s->toBigInteger()); + $mask = "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"; + return strrev($r->toBytes()) & $mask; + } +} \ No newline at end of file diff --git a/phpseclib/Crypt/RC4.php b/phpseclib/Crypt/RC4.php index 7318a74b..9e301435 100644 --- a/phpseclib/Crypt/RC4.php +++ b/phpseclib/Crypt/RC4.php @@ -145,6 +145,9 @@ class RC4 extends StreamCipher protected function isValidEngineHelper($engine) { if ($engine == self::ENGINE_OPENSSL) { + if ($this->continuousBuffer) { + return false; + } if (version_compare(PHP_VERSION, '5.3.7') >= 0) { $this->cipher_name_openssl = 'rc4-40'; } else { diff --git a/phpseclib/Crypt/Salsa20.php b/phpseclib/Crypt/Salsa20.php new file mode 100644 index 00000000..0d59c808 --- /dev/null +++ b/phpseclib/Crypt/Salsa20.php @@ -0,0 +1,541 @@ + + * @copyright 2019 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +use phpseclib\Crypt\Common\StreamCipher; +use phpseclib\Exception\InsufficientSetupException; +use phpseclib\Exception\BadDecryptionException; +use phpseclib\Common\Functions\Strings; + +/** + * Pure-PHP implementation of Salsa20. + * + * @package Salsa20 + * @author Jim Wigginton + * @access public + */ +class Salsa20 extends StreamCipher +{ + /** + * Part 1 of the state + * + * @var string|false + */ + protected $p1 = false; + + /** + * Part 2 of the state + * + * @var string|false + */ + protected $p2 = false; + + /** + * Key Length (in bytes) + * + * @var int + */ + protected $key_length = 32; // = 256 bits + + /** + * Block Length of the cipher + * + * Salsa20 is a stream cipher + * so we the block_size to 0 + * + * @var int + */ + protected $block_size = 0; + + /**#@+ + * @access private + * @see \phpseclib\Crypt\Salsa20::crypt() + */ + const ENCRYPT = 0; + const DECRYPT = 1; + /**#@-*/ + + /** + * Encryption buffer for continuous mode + * + * @var array + */ + protected $enbuffer; + + /** + * Decryption buffer for continuous mode + * + * @var array + */ + protected $debuffer; + + /** + * Counter + * + * @var int + */ + protected $counter = 0; + + /** + * Using Generated Poly1305 Key + * + * @var boolean + */ + protected $usingGeneratedPoly1305Key = false; + + /** + * Default Constructor. + * + * @see \phpseclib\Crypt\Common\SymmetricKey::__construct() + * @return \phpseclib\Crypt\Salsa20 + */ + public function __construct() + { + parent::__construct('stream'); + } + + /** + * Salsa20 does not use an IV + * + * @return bool + */ + public function usesIV() + { + return false; + } + + /** + * Salsa20 uses a nonce + * + * @return bool + */ + public function usesNonce() + { + return true; + } + + /** + * Sets the key. + * + * @param string $key + * @throws \LengthException if the key length isn't supported + */ + public function setKey($key) + { + switch (strlen($key)) { + case 16: + case 32: + break; + default: + throw new \LengthException('Key of size ' . strlen($key) . ' not supported by this algorithm. Only keys of sizes 16 or 32 are supported'); + } + + parent::setKey($key); + } + + /** + * Sets the nonce. + * + * @param string $nonce + */ + public function setNonce($nonce) + { + if (strlen($nonce) != 8) { + throw new \LengthException('Nonce of size ' . strlen($key) . ' not supported by this algorithm. Only an 64-bit nonce is supported'); + } + + $this->nonce = $nonce; + $this->changed = true; + $this->setEngine(); + } + + /** + * Sets the counter. + * + * @param int $counter + */ + public function setCounter($counter) + { + $this->counter = $counter; + $this->setEngine(); + } + + /** + * Creates a Poly1305 key using the method discussed in RFC8439 + * + * See https://tools.ietf.org/html/rfc8439#section-2.6.1 + */ + protected function createPoly1305Key() + { + if ($this->nonce === false) { + throw new InsufficientSetupException('No nonce has been defined'); + } + + if ($this->key === false) { + throw new InsufficientSetupException('No key has been defined'); + } + + $c = clone $this; + $c->setCounter(0); + $c->usePoly1305 = false; + $block = $c->encrypt(str_repeat("\0", 256)); + $this->setPoly1305Key(substr($block, 0, 32)); + + if ($this->counter == 0) { + $this->counter++; + } + } + + /** + * Setup the self::ENGINE_INTERNAL $engine + * + * (re)init, if necessary, the internal cipher $engine + * + * _setup() will be called each time if $changed === true + * typically this happens when using one or more of following public methods: + * + * - setKey() + * + * - setNonce() + * + * - First run of encrypt() / decrypt() with no init-settings + * + * @see self::setKey() + * @see self::setNonce() + * @see self::disableContinuousBuffer() + */ + protected function setup() + { + if (!$this->changed) { + return; + } + + $this->enbuffer = $this->debuffer = ['ciphertext' => '', 'counter' => $this->counter]; + + $this->changed = false; + + if ($this->nonce === false) { + throw new InsufficientSetupException('No nonce has been defined'); + } + + if ($this->key === false) { + throw new InsufficientSetupException('No key has been defined'); + } + + if ($this->usePoly1305 && !isset($this->poly1305Key)) { + $this->usingGeneratedPoly1305Key = true; + $this->createPoly1305Key(); + } + + $key = $this->key; + if (strlen($key) == 16) { + $constant = 'expand 16-byte k'; + $key.= $key; + } else { + $constant = 'expand 32-byte k'; + } + + $this->p1 = substr($constant, 0, 4) . + substr($key, 0, 16) . + substr($constant, 4, 4) . + $this->nonce . + "\0\0\0\0"; + $this->p2 = substr($constant, 8, 4) . + substr($key, 16, 16) . + substr($constant, 12, 4); + } + + /** + * Setup the key (expansion) + */ + protected function setupKey() + { + // Salsa20 does not utilize this method + } + + /** + * Encrypts a message. + * + * @see \phpseclib\Crypt\Common\SymmetricKey::decrypt() + * @see self::crypt() + * @param string $plaintext + * @return string $ciphertext + */ + public function encrypt($plaintext) + { + $ciphertext = $this->crypt($plaintext, self::ENCRYPT); + if (isset($this->poly1305Key)) { + $this->newtag = $this->poly1305($ciphertext); + } + return $ciphertext; + } + + /** + * Decrypts a message. + * + * $this->decrypt($this->encrypt($plaintext)) == $this->encrypt($this->encrypt($plaintext)). + * At least if the continuous buffer is disabled. + * + * @see \phpseclib\Crypt\Common\SymmetricKey::encrypt() + * @see self::crypt() + * @param string $ciphertext + * @return string $plaintext + */ + public function decrypt($ciphertext) + { + if (isset($this->poly1305Key)) { + if ($this->oldtag === false) { + throw new InsufficientSetupException('Authentication Tag has not been set'); + } + $newtag = $this->poly1305($ciphertext); + if ($this->oldtag != substr($newtag, 0, strlen($this->oldtag))) { + $this->oldtag = false; + throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match'); + } + $this->oldtag = false; + } + + return $this->crypt($ciphertext, self::DECRYPT); + } + + /** + * Encrypts a block + * + * @param string $in + */ + protected function encryptBlock($in) + { + // Salsa20 does not utilize this method + } + + /** + * Decrypts a block + * + * @param string $in + */ + protected function decryptBlock($in) + { + // Salsa20 does not utilize this method + } + + /** + * Encrypts or decrypts a message. + * + * @see self::encrypt() + * @see self::decrypt() + * @param string $text + * @param int $mode + * @return string $text + */ + private function crypt($text, $mode) + { + $this->setup(); + if (!$this->continuousBuffer) { + if ($this->engine == self::ENGINE_OPENSSL) { + $iv = pack('V', $this->counter) . $this->p2; + return openssl_encrypt( + $text, + $this->cipher_name_openssl, + $this->key, + OPENSSL_RAW_DATA, + $iv + ); + } + $i = $this->counter; + $blocks = str_split($text, 64); + foreach ($blocks as &$block) { + $block^= static::salsa20($this->p1 . pack('V', $i++) . $this->p2); + } + + return implode('', $blocks); + } + + if ($mode == self::ENCRYPT) { + $buffer = &$this->enbuffer; + } else { + $buffer = &$this->debuffer; + } + if (strlen($buffer['ciphertext'])) { + $ciphertext = $text ^ Strings::shift($buffer['ciphertext'], strlen($text)); + $text = substr($text, strlen($ciphertext)); + if (!strlen($text)) { + return $ciphertext; + } + } + + $overflow = strlen($text) % 64; // & 0x3F + if ($overflow) { + $text2 = Strings::pop($text, $overflow); + if ($this->engine == self::ENGINE_OPENSSL) { + $iv = pack('V', $buffer['counter']) . $this->p2; + // at this point $text should be a multiple of 64 + $buffer['counter']+= (strlen($text) >> 6) + 1; // ie. divide by 64 + $encrypted = openssl_encrypt( + $text . str_repeat("\0", 64), + $this->cipher_name_openssl, + $this->key, + OPENSSL_RAW_DATA, + $iv + ); + $temp = Strings::pop($encrypted, 64); + } else { + $blocks = str_split($text, 64); + if (strlen($text)) { + foreach ($blocks as &$block) { + $block^= static::salsa20($this->p1 . pack('V', $buffer['counter']++) . $this->p2); + } + } + $encrypted = implode('', $blocks); + $temp = static::salsa20($this->p1 . pack('V', $buffer['counter']++) . $this->p2); + } + $ciphertext.= $encrypted . ($text2 ^ $temp); + $buffer['ciphertext'] = substr($temp, $overflow); + } elseif (!strlen($buffer['ciphertext'])) { + if ($this->engine == self::ENGINE_OPENSSL) { + $iv = pack('V', $buffer['counter']) . $this->p2; + $buffer['counter']+= (strlen($text) >> 6); + $ciphertext.= openssl_encrypt( + $text, + $this->cipher_name_openssl, + $this->key, + OPENSSL_RAW_DATA, + $iv + ); + } else { + $blocks = str_split($text, 64); + foreach ($blocks as &$block) { + $block^= static::salsa20($this->p1 . pack('V', $buffer['counter']++) . $this->p2); + } + $ciphertext.= implode('', $blocks); + } + } + + return $ciphertext; + } + + /** + * Left Rotate + * + * @param int $x + * @param int $n + * @return int + */ + protected static function leftRotate($x, $n) + { + $r1 = $x << $n; + if (PHP_INT_SIZE == 8) { + $r1&= 0xFFFFFFFF; + $r2 = ($x & 0xFFFFFFFF) >> (32 - $n); + } else { + $r2 = $x >> (32 - $n); + $r2&= (1 << $n) - 1; + } + return $r1 | $r2; + } + + /** + * The quarterround function + * + * @param int $a + * @param int $b + * @param int $c + * @param int $d + */ + protected static function quarterRound(&$a, &$b, &$c, &$d) + { + $b^= self::leftRotate($a + $d, 7); + $c^= self::leftRotate($b + $a, 9); + $d^= self::leftRotate($c + $b, 13); + $a^= self::leftRotate($d + $c, 18); + } + + /** + * The doubleround function + * + * @param int $x0...$x16 + */ + protected static function doubleRound(&$x0, &$x1, &$x2, &$x3, &$x4, &$x5, &$x6, &$x7, &$x8, &$x9, &$x10, &$x11, &$x12, &$x13, &$x14, &$x15) + { + // columnRound + static::quarterRound( $x0, $x4, $x8, $x12); + static::quarterRound( $x5, $x9, $x13, $x1); + static::quarterRound($x10, $x14, $x2, $x6); + static::quarterRound($x15, $x3, $x7, $x11); + // rowRound + static::quarterRound( $x0, $x1, $x2, $x3); + static::quarterRound( $x5, $x6, $x7, $x4); + static::quarterRound($x10, $x11, $x8, $x9); + static::quarterRound($x15, $x12, $x13, $x14); + } + + /** + * The Salsa20 hash function function + * + * @param string $x + */ + protected static function salsa20($x) + { + $z = $x = unpack('V*', $x); + for ($i = 0; $i < 10; $i++) { + static::doubleRound(...$z); + } + + for ($i = 1; $i <= 16; $i++) { + $x[$i]+= $z[$i]; + } + + return pack('V*', ...$x); + } + + /** + * Calculates Poly1305 MAC + * + * @see self::decrypt() + * @see self::encrypt() + * @access private + * @param string $text + * @return string + */ + protected function poly1305($ciphertext) + { + if (!$this->usingGeneratedPoly1305Key) { + return parent::poly1305($this->aad . $ciphertext); + } else { + /* + sodium_crypto_aead_chacha20poly1305_encrypt does not calculate the poly1305 tag + the same way sodium_crypto_aead_chacha20poly1305_ietf_encrypt does. you can see + how the latter encrypts it in Salsa20::encrypt(). here's how the former encrypts + it: + + $this->newtag = $this->poly1305( + $this->aad . + pack('V', strlen($this->aad)) . "\0\0\0\0" . + $ciphertext . + pack('V', strlen($ciphertext)) . "\0\0\0\0" + ); + + phpseclib opts to use the IETF construction, even when the nonce is 64-bits + instead of 96-bits + */ + return parent::poly1305( + self::nullPad128($this->aad) . + self::nullPad128($ciphertext) . + pack('V', strlen($this->aad)) . "\0\0\0\0" . + pack('V', strlen($ciphertext)) . "\0\0\0\0" + ); + } + } +} diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 5480c8a7..4b111ba6 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -59,6 +59,7 @@ use phpseclib\Crypt\AES; use phpseclib\Crypt\RSA; use phpseclib\Crypt\TripleDES; use phpseclib\Crypt\Twofish; +use phpseclib\Crypt\ChaCha20; use phpseclib\Math\BigInteger; // Used to do Diffie-Hellman key exchange and DSA/RSA signature verification. use phpseclib\System\SSH\Agent; use phpseclib\System\SSH\Agent\Identity as AgentIdentity; @@ -355,6 +356,15 @@ class SSH2 */ private $decrypt = false; + /** + * Server to Client Length Encryption Object + * + * @see self::_get_binary_packet() + * @var object + * @access private + */ + private $lengthDecrypt = false; + /** * Client to Server Encryption Object * @@ -364,6 +374,15 @@ class SSH2 */ private $encrypt = false; + /** + * Client to Server Length Encryption Object + * + * @see self::_send_binary_packet() + * @var object + * @access private + */ + private $lengthEncrypt = false; + /** * Client to Server HMAC Object * @@ -938,7 +957,7 @@ class SSH2 * @var array * @access private */ - var $auth = array(); + var $auth = []; /** * Default Constructor. @@ -1349,15 +1368,25 @@ class SSH2 //'arcfour', // OPTIONAL the ARCFOUR stream cipher with a 128-bit key - // from : - 'aes128-gcm@openssh.com', - 'aes256-gcm@openssh.com', - // CTR modes from : 'aes128-ctr', // RECOMMENDED AES (Rijndael) in SDCTR mode, with 128-bit key 'aes192-ctr', // RECOMMENDED AES with 192-bit key 'aes256-ctr', // RECOMMENDED AES with 256-bit key + // from : + // one of the big benefits of chacha20-poly1305 is speed. the problem is... + // libsodium doesn't generate the poly1305 keys in the way ssh does and openssl's PHP bindings don't even + // seem to support poly1305 currently. so even if libsodium or openssl are being used for the chacha20 + // part, pure-PHP has to be used for the poly1305 part and that's gonna cause a big slow down. + // speed-wise it winds up being faster to use AES (when openssl or mcrypt are available) and some HMAC + // (which is always gonna be super fast to compute thanks to the hash extension, which + // "is bundled and compiled into PHP by default") + 'chacha20-poly1305@openssh.com', + + // from : + 'aes128-gcm@openssh.com', + 'aes256-gcm@openssh.com', + 'twofish128-ctr', // OPTIONAL Twofish in SDCTR mode, with 128-bit key 'twofish192-ctr', // OPTIONAL Twofish with 192-bit key 'twofish256-ctr', // OPTIONAL Twofish with 256-bit key @@ -1882,20 +1911,28 @@ class SSH2 $this->encrypt->setIV(substr($iv, 0, $this->encrypt_block_size)); } - // currently, only AES GCM uses a nonce and per RFC5647, - // "SSH AES-GCM requires a 12-octet Initial IV" - if (!$this->encrypt->usesNonce()) { - $this->encrypt->enableContinuousBuffer(); - } else { - $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); - $this->encrypt->fixed = substr($nonce, 0, 4); - $this->encrypt->invocation_counter = substr($nonce, 4, 8); + switch ($encrypt) { + case 'aes128-gcm@openssh.com': + case 'aes256-gcm@openssh.com': + $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); + $this->encrypt->fixed = substr($nonce, 0, 4); + $this->encrypt->invocation_counter = substr($nonce, 4, 8); + case 'chacha20-poly1305@openssh.com': + break; + default: + $this->encrypt->enableContinuousBuffer(); } $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id); while ($encryptKeyLength > strlen($key)) { $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } + switch ($encrypt) { + case 'chacha20-poly1305@openssh.com': + $encryptKeyLength = 32; + $this->lengthEncrypt = $this->encryption_algorithm_to_crypt_instance($encrypt); + $this->lengthEncrypt->setKey(substr($key, 32, 32)); + } $this->encrypt->setKey(substr($key, 0, $encryptKeyLength)); $this->encrypt->name = $encrypt; } @@ -1918,19 +1955,29 @@ class SSH2 $this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size)); } - if (!$this->decrypt->usesNonce()) { - $this->decrypt->enableContinuousBuffer(); - } else { - // see https://tools.ietf.org/html/rfc5647#section-7.1 - $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); - $this->decrypt->fixed = substr($nonce, 0, 4); - $this->decrypt->invocation_counter = substr($nonce, 4, 8); + switch ($encrypt) { + case 'aes128-gcm@openssh.com': + case 'aes256-gcm@openssh.com': + // see https://tools.ietf.org/html/rfc5647#section-7.1 + $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); + $this->decrypt->fixed = substr($nonce, 0, 4); + $this->decrypt->invocation_counter = substr($nonce, 4, 8); + case 'chacha20-poly1305@openssh.com': + break; + default: + $this->decrypt->enableContinuousBuffer(); } $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'D' . $this->session_id); while ($decryptKeyLength > strlen($key)) { $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } + switch ($decrypt) { + case 'chacha20-poly1305@openssh.com': + $decryptKeyLength = 32; + $this->lengthDecrypt = $this->encryption_algorithm_to_crypt_instance($decrypt); + $this->lengthDecrypt->setKey(substr($key, 32, 32)); + } $this->decrypt->setKey(substr($key, 0, $decryptKeyLength)); $this->decrypt->name = $decrypt; } @@ -2097,6 +2144,8 @@ class SSH2 case 'twofish256-cbc': case 'twofish256-ctr': return 32; + case 'chacha20-poly1305@openssh.com': + return 64; } return null; } @@ -2144,6 +2193,8 @@ class SSH2 case 'aes128-gcm@openssh.com': case 'aes256-gcm@openssh.com': return new AES('gcm'); + case 'chacha20-poly1305@openssh.com': + return new ChaCha20(); } return null; } @@ -3409,28 +3460,59 @@ class SSH2 } if ($this->decrypt) { - // only aes128-gcm@openssh.com and aes256-gcm@openssh.com use nonces - if (!$this->decrypt->usesNonce()) { - $raw = $this->decrypt->decrypt($raw); - } else { - $this->decrypt->setNonce( - $this->decrypt->fixed . - $this->decrypt->invocation_counter - ); - Strings::increment_str($this->decrypt->invocation_counter); - $this->decrypt->setAAD($temp = Strings::shift($raw, 4)); - extract(unpack('Npacket_length', $temp)); - /** - * @var integer $packet_length - */ + switch ($this->decrypt->name) { + case 'aes128-gcm@openssh.com': + case 'aes256-gcm@openssh.com': + $this->decrypt->setNonce( + $this->decrypt->fixed . + $this->decrypt->invocation_counter + ); + Strings::increment_str($this->decrypt->invocation_counter); + $this->decrypt->setAAD($temp = Strings::shift($raw, 4)); + extract(unpack('Npacket_length', $temp)); + /** + * @var integer $packet_length + */ - $raw.= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); - $stop = microtime(true); - $tag = stream_get_contents($this->fsock, $this->decrypt_block_size); - $this->decrypt->setTag($tag); - $raw = $this->decrypt->decrypt($raw); - $raw = $temp . $raw; - $remaining_length = 0; + $raw.= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); + $stop = microtime(true); + $tag = stream_get_contents($this->fsock, $this->decrypt_block_size); + $this->decrypt->setTag($tag); + $raw = $this->decrypt->decrypt($raw); + $raw = $temp . $raw; + $remaining_length = 0; + break; + case 'chacha20-poly1305@openssh.com': + $nonce = pack('N2', 0, $this->get_seq_no); + + $this->lengthDecrypt->setNonce($nonce); + $temp = $this->lengthDecrypt->decrypt($aad = Strings::shift($raw, 4)); + extract(unpack('Npacket_length', $temp)); + /** + * @var integer $packet_length + */ + + $raw.= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); + $stop = microtime(true); + $tag = stream_get_contents($this->fsock, 16); + + $this->decrypt->setNonce($nonce); + $this->decrypt->setCounter(0); + // this is the same approach that's implemented in Salsa20::createPoly1305Key() + // but we don't want to use the same AEAD construction that RFC8439 describes + // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305()) + $this->decrypt->setPoly1305Key( + $this->decrypt->encrypt(str_repeat("\0", 32)) + ); + $this->decrypt->setAAD($aad); + $this->decrypt->setCounter(1); + $this->decrypt->setTag($tag); + $raw = $this->decrypt->decrypt($raw); + $raw = $temp . $raw; + $remaining_length = 0; + break; + default: + $raw = $this->decrypt->decrypt($raw); } } @@ -3447,19 +3529,6 @@ class SSH2 $remaining_length = $packet_length + 4 - $this->decrypt_block_size; } - // quoting , - // "implementations SHOULD check that the packet length is reasonable" - // PuTTY uses 0x9000 as the actual max packet size and so to shall we - // don't do this when GCM mode is used since GCM mode doesn't encrypt the length - if ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) { - if (!$this->bad_key_size_fix && self::bad_algorithm_candidate($this->decrypt ? $this->decrypt->name : '') && !($this->bitmap & SSH2::MASK_LOGIN)) { - $this->bad_key_size_fix = true; - $this->reset_connection(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); - return false; - } - throw new \RuntimeException('Invalid size'); - } - $buffer = $this->read_remaining_bytes($remaining_length); if (!isset($stop)) { @@ -3510,6 +3579,38 @@ class SSH2 */ private function read_remaining_bytes($remaining_length) { + if (!$remaining_length) { + return ''; + } + + $adjustLength = false; + if ($this->decrypt) { + switch ($this->decrypt->name) { + case 'aes128-gcm@openssh.com': + case 'aes256-gcm@openssh.com': + case 'chacha20-poly1305@openssh.com': + $remaining_length+= $this->decrypt_block_size - 4; + $adjustLength = true; + } + } + + // quoting , + // "implementations SHOULD check that the packet length is reasonable" + // PuTTY uses 0x9000 as the actual max packet size and so to shall we + // don't do this when GCM mode is used since GCM mode doesn't encrypt the length + if ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) { + if (!$this->bad_key_size_fix && self::bad_algorithm_candidate($this->decrypt ? $this->decrypt->name : '') && !($this->bitmap & SSH2::MASK_LOGIN)) { + $this->bad_key_size_fix = true; + $this->reset_connection(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + return false; + } + throw new \RuntimeException('Invalid size'); + } + + if ($adjustLength) { + $remaining_length-= $this->decrypt_block_size - 4; + } + $buffer = ''; while ($remaining_length > 0) { $temp = stream_get_contents($this->fsock, $remaining_length); @@ -4113,16 +4214,38 @@ class SSH2 $this->send_seq_no++; if ($this->encrypt) { - if (!$this->encrypt->usesNonce()) { - $packet = $this->encrypt->encrypt($packet); - } else { - $this->encrypt->setNonce( - $this->encrypt->fixed . - $this->encrypt->invocation_counter - ); - Strings::increment_str($this->encrypt->invocation_counter); - $this->encrypt->setAAD($temp = substr($packet, 0, 4)); - $packet = $temp . $this->encrypt->encrypt(substr($packet, 4)); + switch ($this->encrypt->name) { + case 'aes128-gcm@openssh.com': + case 'aes256-gcm@openssh.com': + $this->encrypt->setNonce( + $this->encrypt->fixed . + $this->encrypt->invocation_counter + ); + Strings::increment_str($this->encrypt->invocation_counter); + $this->encrypt->setAAD($temp = substr($packet, 0, 4)); + $packet = $temp . $this->encrypt->encrypt(substr($packet, 4)); + break; + case 'chacha20-poly1305@openssh.com': + $nonce = pack('N2', 0, $this->send_seq_no - 1); + + $this->encrypt->setNonce($nonce); + $this->lengthEncrypt->setNonce($nonce); + + $length = $this->lengthEncrypt->encrypt(substr($packet, 0, 4)); + + $this->encrypt->setCounter(0); + // this is the same approach that's implemented in Salsa20::createPoly1305Key() + // but we don't want to use the same AEAD construction that RFC8439 describes + // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305()) + $this->encrypt->setPoly1305Key( + $this->encrypt->encrypt(str_repeat("\0", 32)) + ); + $this->encrypt->setAAD($length); + $this->encrypt->setCounter(1); + $packet = $length . $this->encrypt->encrypt(substr($packet, 4)); + break; + default: + $packet = $this->encrypt->encrypt($packet); } } diff --git a/tests/Unit/Crypt/ChaCha20.php b/tests/Unit/Crypt/ChaCha20.php new file mode 100644 index 00000000..61ef90e9 --- /dev/null +++ b/tests/Unit/Crypt/ChaCha20.php @@ -0,0 +1,218 @@ + + * @copyright 2014 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + */ + +use phpseclib\Crypt\ChaCha20; + +class Unit_Crypt_ChaCha20Test extends PhpseclibTestCase +{ + // see https://tools.ietf.org/html/rfc8439#section-2.3.2 + public function test232() + { + $key = implode('', range("\00", "\x1f")); + + $nonce = '00:00:00:09:00:00:00:4a:00:00:00:00'; + $nonce = str_replace(':', '', $nonce); + $nonce = pack('H*', $nonce); + + $expected = '10 f1 e7 e4 d1 3b 59 15 50 0f dd 1f a3 20 71 c4' . + 'c7 d1 f4 c7 33 c0 68 03 04 22 aa 9a c3 d4 6c 4e' . + 'd2 82 64 46 07 9f aa 09 14 c2 d7 05 d9 8b 02 a2' . + 'b5 12 9c d1 de 16 4e b9 cb d0 83 e8 a2 50 3c 4e'; + $expected = str_replace(' ', '', $expected); + $expected = pack('H*', $expected); + + $engines = ['PHP', OpenSSL', 'libsodium']; + for ($engines as $engine) { + $c = new ChaCha20(); + $c->setKey($key); + $c->setNonce($nonce); + $c->setCounter(1); + $c->setPreferredEngine($engine); + if ($c->getEngine() != $engine) { + continue; + } + $result = $c->encrypt(str_repeat("\0", 64)); + $this->assertSame($expected, $result, "Failed asserting that ciphertext matches expected value with $engine engine"); + } + } + + // see https://tools.ietf.org/html/rfc8439#section-2.4.2 + public function test242() + { + $key = implode('', range("\00", "\x1f")); + + $nonce = '00:00:00:00:00:00:00:4a:00:00:00:00'; + $nonce = str_replace(':', '', $nonce); + $nonce = pack('H*', $nonce); + + $plaintext = 'Ladies and Gentlemen of the class of \'99: If I could offer you only one tip for the future,' . + ' sunscreen would be it.'; + + $expected = '6e 2e 35 9a 25 68 f9 80 41 ba 07 28 dd 0d 69 81' . + 'e9 7e 7a ec 1d 43 60 c2 0a 27 af cc fd 9f ae 0b' . + 'f9 1b 65 c5 52 47 33 ab 8f 59 3d ab cd 62 b3 57' . + '16 39 d6 24 e6 51 52 ab 8f 53 0c 35 9f 08 61 d8' . + '07 ca 0d bf 50 0d 6a 61 56 a3 8e 08 8a 22 b6 5e' . + '52 bc 51 4d 16 cc f8 06 81 8c e9 1a b7 79 37 36' . + '5a f9 0b bf 74 a3 5b e6 b4 0b 8e ed f2 78 5e 42' . + '87 4d'; + $expected = str_replace(' ', '', $expected); + $expected = pack('H*', $expected); + + $engines = ['PHP', OpenSSL', 'libsodium']; + for ($engines as $engine) { + $c = new ChaCha20(); + $c->setKey($key); + $c->setNonce($nonce); + $c->setCounter(1); + $c->setPreferredEngine($engine); + if ($c->getEngine() != $engine) { + continue; + } + $result = $c->encrypt($plaintext); + $this->assertSame($expected, $result, "Failed asserting that ciphertext matches expected value with $engine engine"); + } + } + + // see https://tools.ietf.org/html/rfc8439#section-2.5.2 + public function test252() + { + $key = '85:d6:be:78:57:55:6d:33:7f:44:52:fe:42:d5:06:a8:01:0' . + '3:80:8a:fb:0d:b2:fd:4a:bf:f6:af:41:49:f5:1b'; + $key = str_replace(':', '', $key); + $key = pack('H*', $key); + + $plaintext = 'Cryptographic Forum Research Group'; + + $expected = 'a8:06:1d:c1:30:51:36:c6:c2:2b:8b:af:0c:01:27:a9'; + $expected = str_replace(':', '', $expected); + $expected = pack('H*', $expected); + + $c = new ChaCha20; + $r = new \ReflectionClass(get_class($c)); + $p = $r->getProperty('poly1305Key'); + $p->setAccessible(true); + $p->setValue($c, $key); + + $m = $r->getMethod('poly1305'); + $m->setAccessible(true); + $result = $m->invokeArgs($c, [$plaintext]); + + $this->assertSame($expected, $result, 'Failed asserting that poly1305 matches expected value'); + } + + // see https://tools.ietf.org/html/rfc8439#section-2.6.2 + public function test262() + { + $key = implode('', range("\80", "\x9f")); + + $nonce = '00 00 00 00 00 01 02 03 04 05 06 07'; + $nonce = str_replace(' ', '', $nonce); + $nonce = pack('H*', $nonce); + + $expected = '8a d5 a0 8b 90 5f 81 cc 81 50 40 27 4a b2 94 71' . + 'a8 33 b6 37 e3 fd 0d a5 08 db b8 e2 fd d1 a6 46'; + $expected = str_replace(' ', '', $expected); + $expected = pack('H*', $expected); + + $engines = ['PHP', OpenSSL', 'libsodium']; + for ($engines as $engine) { + $c = new ChaCha20(); + $c->setKey($key); + $c->setNonce($nonce); + //$c->setCounter(0); + $c->setPreferredEngine($engine); + if ($c->getEngine() != $engine) { + continue; + } + $result = $c->encrypt($plaintext); + $this->assertSame($expected, $result, "Failed asserting that ciphertext matches expected value with $engine engine"); + } + } + + // https://tools.ietf.org/html/rfc8439#section-2.8.2 + public function test282() + { + $key = implode('', range("\80", "\x9f")); + + $nonce = "\x07\0\0\0" . "\x40\x41\x42\x43\x44\x45\x46\x47"; + + $plaintext = 'Ladies and Gentlemen of the class of \'99: If I could offer you only one tip for the future,' . + ' sunscreen would be it.'; + + $aad = '50 51 52 53 c0 c1 c2 c3 c4 c5 c6 c7'; + $aad = str_replace(' ', '', $aad); + $aad = pack('H*', $aad); + + $expected = 'd3 1a 8d 34 64 8e 60 db 7b 86 af bc 53 ef 7e c2' . + 'a4 ad ed 51 29 6e 08 fe a9 e2 b5 a7 36 ee 62 d6' . + '3d be a4 5e 8c a9 67 12 82 fa fb 69 da 92 72 8b' . + '1a 71 de 0a 9e 06 0b 29 05 d6 a5 b6 7e cd 3b 36' . + '92 dd bd 7f 2d 77 8b 8c 98 03 ae e3 28 09 1b 58' . + 'fa b3 24 e4 fa d6 75 94 55 85 80 8b 48 31 d7 bc' . + '3f f4 de f0 8e 4b 7a 9d e5 76 d2 65 86 ce c6 4b' . + '61 16'; + $expected = str_replace(' ', '', $expected); + $expected = pack('H*', $expected); + + $tag = '1a:e1:0b:59:4f:09:e2:6a:7e:90:2e:cb:d0:60:06:91'; + $tag = str_replace(' ', '', $tag); + $tag = pack('H*', $tag); + + $engines = ['PHP', OpenSSL', 'libsodium']; + for ($engines as $engine) { + $c = new ChaCha20(); + $c->enablePoly1305(); + $c->setKey($key); + $c->setNonce($nonce); + $c->setAAD($aad); + $c->setPreferredEngine($engine); + if ($c->getEngine() != $engine) { + continue; + } + $result = $c->encrypt($plaintext); + $this->assertSame($expected, $result, "Failed asserting that ciphertext matches expected value with $engine engine"); + $this->assertSame($tag, $c->getTag(), "Failed asserting that the tag matches the expected value with $engine engine"); + } + } + + public function testContinuousBuffer() + { + $key = str_repeat("\0", 16); + $nonce = str_repeat("\0", 8); + + $partitions = [1, 63, 70]; + + $plaintext = str_repeat("\0", array_sum($partitions)); + + $engines = ['PHP', OpenSSL', 'libsodium']; + for ($engines as $engine) { + $c = new ChaCha20(); + $c->setKey($key); + $c->setNonce($nonce); + $c->setPreferredEngine($engine); + + $c2 = new ChaCha20(); + $c2->setKey($key); + $c2->setNonce($nonce); + $c2->setPreferredEngine($engine); + $c2->enableContinuousBuffer(); + + if ($c2->getEngine() != $engine) { + continue; + } + + $p1 = $c->encrypt($plaintext); + $p2 = ''; + foreach ($partitions as $partition) { + $p2.= $c2->encrypt(str_repeat("\0", $partition)); + } + + $this->assertSame($p1, $p2, "Failed asserting that ciphertext matches expected value with $engine engine"); + } + } +} diff --git a/tests/Unit/Crypt/Salsa20.php b/tests/Unit/Crypt/Salsa20.php new file mode 100644 index 00000000..b4e74526 --- /dev/null +++ b/tests/Unit/Crypt/Salsa20.php @@ -0,0 +1,160 @@ + + * @copyright 2014 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + */ + +use phpseclib\Crypt\Salsa20; + +class Unit_Crypt_Salsa20Test extends PhpseclibTestCase +{ + public function engineVectors() + { + $engines = [ + 'PHP', + ]; + // tests from http://www.ecrypt.eu.org/stream/svn/viewcvs.cgi/ecrypt/trunk/submissions/salsa20/full/verified.test-vectors?logsort=rev&rev=210&view=markup + // more specifically, it's vector # 0 in each set + $tests = [ + // key size: 128 bits + // set 1 + [ + 'key' => '80000000000000000000000000000000', + 'iv' => '0000000000000000', + 'result' => 'F7A274D268316790A67EC058F45C0F2A' . + '067A99FCDE6236C0CEF8E056349FE54C' . + '5F13AC74D2539570FD34FEAB06C57205' . + '3949B59585742181A5A760223AFA22D4' + ], + // set 2 + [ + 'key' => '00000000000000000000000000000000', + 'iv' => '0000000000000000', + 'result' => '6D3937FFA13637648E477623277644AD' . + 'AD3854E6B2B3E4D68155356F68B30490' . + '842B2AEA2E32239BE84E613C6CE1B9BD' . + '026094962CB1A6757AF5A13DDAF8252C' + ], + // set 3 + [ + 'key' => '000102030405060708090A0B0C0D0E0F', + 'iv' => '0000000000000000', + 'result' => 'F3BCF4D6381742839C5627050D4B227F' . + 'EB1ECCC527BF605C4CB9D6FB0618F419' . + 'B51846707550BBEEE381E44A50A406D0' . + '20C8433D08B19C98EFC867ED9897EDBB' + ], + // set 4 + [ + 'key' => '0053A6F94C9FF24598EB3E91E4378ADD', + 'iv' => '0000000000000000', + 'result' => '196D1A0977F0585B23367497D449E11D' . + 'E328ECD944BC133F786348C9591B35B7' . + '189CDDD934757ED8F18FBC984DA377A8' . + '07147F1A6A9A8759FD2A062FD76D275E' + ], + // set 5 + [ + 'key' => '00000000000000000000000000000000', + 'iv' => '8000000000000000', + 'result' => '104639D9F65C879F7DFF8A82A94C130C' . + 'D6C727B3BC8127943ACDF0AB7AD6D28B' . + 'F2ADF50D81F50C53D0FDFE15803854C7' . + 'D67F6C9B4752275696E370A467A4C1F8' + ], + // set 6 + [ + 'key' => '0053A6F94C9FF24598EB3E91E4378ADD', + 'iv' => '0D74DB42A91077DE', + 'result' => '620BB4C2ED20F4152F0F86053D3F5595' . + '8E1FBA48F5D86B25C8F31559F3158072' . + '6E7ED8525D0B9EA5264BF97750713476' . + '1EF65FE195274AFBF000938C03BA59A7' + ], + // key size: 256 bits + // set 1 + [ + 'key' => '8000000000000000000000000000000000000000000000000000000000000000', + 'iv' => '0000000000000000', + 'result' => '50EC2485637DB19C6E795E9C73938280' . + '6F6DB320FE3D0444D56707D7B456457F' . + '3DB3E8D7065AF375A225A70951C8AB74' . + '4EC4D595E85225F08E2BC03FE1C42567' + ], + // set 2 + [ + 'key' => '0000000000000000000000000000000000000000000000000000000000000000', + 'iv' => '0000000000000000', + 'result' => '7C3A1499A63B507B0BC75824ABEEAA26' . + '109101C5B915F0F554DD9950045D02FA' . + 'FF815CA8B2C7CFF3625765697B80B026' . + '7EA87E25412564BD71DD05843A60465E' + ], + // set 3 + [ + 'key' => '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F', + 'iv' => '0000000000000000', + 'result' => '8C03E9237FEE95D5041C753C204D2B35' . + '764E4A53035A76F9EFBADD7E63E60B69' . + 'BF23F7C5FD39B2249B0C628FB654D521' . + '4EB588371E5D2F34BF51396AF3ACB666' + ], + // set 4 + [ + 'key' => '0053A6F94C9FF24598EB3E91E4378ADD3083D6297CCF2275C81B6EC11467BA0D', + 'iv' => '0000000000000000', + 'result' => '2052F9A2853E989133D10938222AC76D' . + 'B8B4CBA135ACB59970DDF9C074C6271A' . + '5C4E2A7A00D2D697EDFC9B1FF9B365C8' . + '7347B23020663A30711A71E3A02AB00C' + ], + // set 5 + [ + 'key' => '0000000000000000000000000000000000000000000000000000000000000000', + 'iv' => '8000000000000000', + 'result' => 'FE40F57D1586D7664C2FCA5AB10BD7C7' . + '9DE3234836E76949F9DC01CBFABC6D6C' . + '42AB27DDC748B4DF7991092972AB4985' . + 'CEC19B3E7C2C85D6E25A338DEC288282' + ], + // set 6 + [ + 'key' => '0053A6F94C9FF24598EB3E91E4378ADD3083D6297CCF2275C81B6EC11467BA0D', + 'iv' => '0D74DB42A91077DE', + 'result' => 'C349B6A51A3EC9B712EAED3F90D8BCEE' . + '69B7628645F251A996F55260C62EF31F' . + 'D6C6B0AEA94E136C9D984AD2DF3578F7' . + '8E457527B03A0450580DD874F63B1AB9' + ], + ]; + + $result = []; + + foreach ($engines as $engine) { + foreach ($tests as $test) { + foreach ($test['output'] as $output) { + $result[] = [$engine, $test['key'], $output['iv'], $output['result']]; + } + } + } + + return $result; + } + + /** + * @dataProvider engineVectors + */ + public function testVectors($engine, $key, $iv, $expected) + { + $cipher = new Salsa(); + $cipher->setPreferredEngine($engine); + $cipher->setKey(pack('H*', $key)); + $cipher->setNonce(pack('H*', $iv)); + if ($cipher->getEngine() != $engine) { + self::markTestSkipped('Unable to initialize ' . $engine . ' engine for ' . (strlen($key) * 8) . '-bit key'); + } + $result = $cipher->encrypt(str_repeat("\0", 64); + $this->assertEquals(bin2hex($result), $expected, "Failed asserting that key $key / $iv yielded expected output in $engine engine"); + } +}