1
0
mirror of https://github.com/danog/tgseclib.git synced 2024-11-30 04:39:02 +01:00

add Salsa20 / ChaCha20 stream ciphers

This commit is contained in:
terrafrost 2019-03-18 06:59:00 -05:00
parent 8ce392f218
commit 494d20efc8
7 changed files with 2076 additions and 102 deletions

View File

@ -0,0 +1,783 @@
<?php
/**
* Pure-PHP implementation of ChaCha20.
*
* PHP version 5
*
* @category Crypt
* @package ChaCha20
* @author Jim Wigginton <terrafrost@php.net>
* @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 <terrafrost@php.net>
* @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);
}
}

View File

@ -40,6 +40,7 @@ use phpseclib\Crypt\Hash;
use phpseclib\Common\Functions\Strings; use phpseclib\Common\Functions\Strings;
use phpseclib\Math\BigInteger; use phpseclib\Math\BigInteger;
use phpseclib\Math\BinaryField; use phpseclib\Math\BinaryField;
use phpseclib\Math\PrimeField;
use phpseclib\Exception\BadDecryptionException; use phpseclib\Exception\BadDecryptionException;
use phpseclib\Exception\BadModeException; use phpseclib\Exception\BadModeException;
use phpseclib\Exception\InconsistentSetupException; use phpseclib\Exception\InconsistentSetupException;
@ -533,13 +534,43 @@ abstract class SymmetricKey
/** /**
* GCM Binary Field * GCM Binary Field
* *
* @see self::initialize_static_variables() * @see self::__construct()
* @see self::ghash() * @see self::ghash()
* @var BinaryField * @var BinaryField
* @access private * @access private
*/ */
private static $gcmField; 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 * The Original Initialization Vector
* *
@ -649,7 +680,7 @@ abstract class SymmetricKey
} }
if (!$this->usesIV()) { 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) { if (strlen($iv) != $this->block_size) {
@ -660,6 +691,53 @@ abstract class SymmetricKey
$this->changed = true; $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. * Sets the nonce.
* *
@ -667,7 +745,7 @@ abstract class SymmetricKey
* *
* @access public * @access public
* @param string $nonce * @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) public function setNonce($nonce)
{ {
@ -683,16 +761,16 @@ abstract class SymmetricKey
/** /**
* Sets additional authenticated data * Sets additional authenticated data
* *
* setAAD() is only used by gcm * setAAD() is only used by gcm or in poly1305 mode
* *
* @access public * @access public
* @param string $aad * @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) public function setAAD($aad)
{ {
if ($this->mode != self::MODE_GCM) { if ($this->mode != self::MODE_GCM && !$this->usePoly1305) {
throw new \RuntimeException('Additional authenticated data is only utilized in GCM mode'); throw new \BadMethodCallException('Additional authenticated data is only utilized in GCM mode or with Poly1305');
} }
$this->aad = $aad; $this->aad = $aad;
@ -1046,6 +1124,15 @@ abstract class SymmetricKey
return $ciphertext; 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) { if ($this->engine === self::ENGINE_OPENSSL) {
switch ($this->mode) { switch ($this->mode) {
case self::MODE_STREAM: case self::MODE_STREAM:
@ -1362,27 +1449,35 @@ abstract class SymmetricKey
$this->setup(); $this->setup();
if ($this->mode == self::MODE_GCM) { if ($this->mode == self::MODE_GCM || isset($this->poly1305Key)) {
if ($this->oldtag === false) { if ($this->oldtag === false) {
throw new InsufficientSetupException('Authentication Tag has not been set'); throw new InsufficientSetupException('Authentication Tag has not been set');
} }
$oldIV = $this->iv; if (isset($this->poly1305Key)) {
Strings::increment_str($this->iv); $newtag = $this->poly1305($ciphertext);
$cipher = new static('ctr'); } else {
$cipher->setKey($this->key); $oldIV = $this->iv;
$cipher->setIV($this->iv); Strings::increment_str($this->iv);
$plaintext = $cipher->decrypt($ciphertext); $cipher = new static('ctr');
$cipher->setKey($this->key);
$cipher->setIV($this->iv);
$plaintext = $cipher->decrypt($ciphertext);
$s = $this->ghash( $s = $this->ghash(
self::nullPad128($this->aad) . self::nullPad128($this->aad) .
self::nullPad128($ciphertext) . self::nullPad128($ciphertext) .
self::len64($this->aad) . self::len64($this->aad) .
self::len64($ciphertext) self::len64($ciphertext)
); );
$cipher->encryptIV = $this->iv = $this->encryptIV = $this->decryptIV = $oldIV; $cipher->encryptIV = $this->iv = $this->encryptIV = $this->decryptIV = $oldIV;
$newtag = $cipher->encrypt($s); $newtag = $cipher->encrypt($s);
}
if ($this->oldtag != substr($newtag, 0, strlen($newtag))) { if ($this->oldtag != substr($newtag, 0, strlen($newtag))) {
$cipher = clone $this;
unset($cipher->poly1305Key);
$this->usePoly1305 = false;
$plaintext = $cipher->decrypt($ciphertext);
$this->oldtag = false; $this->oldtag = false;
throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match'); throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match');
} }
@ -1672,7 +1767,7 @@ abstract class SymmetricKey
/** /**
* Get the authentication tag * Get the authentication tag
* *
* Only used in GCM mode * Only used in GCM or Poly1305 mode
* *
* @see self::encrypt() * @see self::encrypt()
* @param int $length optional * @param int $length optional
@ -1683,13 +1778,17 @@ abstract class SymmetricKey
*/ */
public function getTag($length = 16) public function getTag($length = 16)
{ {
if ($this->mode != self::MODE_GCM) { if ($this->mode != self::MODE_GCM && !$this->usePoly1305) {
throw new \BadMethodCallException('Only GCM mode utilizes authentication tags'); 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 if ($this->newtag === false) {
// bytes because that's bigger than a block is. if it were 0 you might as well be doing CTR and throw new \BadMethodCallException('A tag can only be returned after a round of encryption has been performed');
// less than 4 provides minimal security that could be trivially easily brute forced. }
// 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 // see https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf#page=36
// for more info // for more info
if ($length < 4 || $length > 16) { if ($length < 4 || $length > 16) {
@ -1714,8 +1813,12 @@ abstract class SymmetricKey
*/ */
public function setTag($tag) public function setTag($tag)
{ {
if ($this->mode != self::MODE_GCM) { if ($this->usePoly1305 && !isset($this->poly1305Key) && method_exists($this, 'createPoly1305Key')) {
throw new \BadMethodCallException('Only GCM mode utilizes authentication tags'); $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); $length = strlen($tag);
@ -2030,9 +2133,6 @@ abstract class SymmetricKey
{ {
switch ($engine) { switch ($engine) {
case self::ENGINE_OPENSSL: case self::ENGINE_OPENSSL:
if ($this->mode == self::MODE_STREAM && $this->continuousBuffer) {
return false;
}
$this->openssl_emulate_ctr = false; $this->openssl_emulate_ctr = false;
$result = $this->cipher_name_openssl && $result = $this->cipher_name_openssl &&
extension_loaded('openssl'); extension_loaded('openssl');
@ -2146,13 +2246,19 @@ abstract class SymmetricKey
$this->engine = null; $this->engine = null;
$candidateEngines = [ $candidateEngines = [
$this->preferredEngine,
self::ENGINE_LIBSODIUM, self::ENGINE_LIBSODIUM,
self::ENGINE_OPENSSL_GCM, self::ENGINE_OPENSSL_GCM,
self::ENGINE_OPENSSL, self::ENGINE_OPENSSL,
self::ENGINE_MCRYPT, self::ENGINE_MCRYPT,
self::ENGINE_EVAL self::ENGINE_EVAL
]; ];
if (isset($this->preferredEngine)) {
$temp = [$this->preferredEngine];
$candidateEngines = array_merge(
$temp,
array_diff($candidateEngines, $temp)
);
}
foreach ($candidateEngines as $engine) { foreach ($candidateEngines as $engine) {
if ($this->isValidEngineHelper($engine)) { if ($this->isValidEngineHelper($engine)) {
$this->engine = $engine; $this->engine = $engine;
@ -2246,14 +2352,18 @@ abstract class SymmetricKey
$this->changed = false; $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->enbuffer = $this->debuffer = ['ciphertext' => '', 'xor' => '', 'pos' => 0, 'enmcrypt_init' => true];
//$this->newtag = $this->oldtag = false; //$this->newtag = $this->oldtag = false;
if ($this->mode == self::MODE_GCM) { if ($this->usesNonce()) {
if ($this->nonce === false) { if ($this->nonce === false) {
throw new InsufficientSetupException('No nonce has been defined'); 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(); $this->setupGCM();
} }
} else { } else {
@ -3078,9 +3188,45 @@ abstract class SymmetricKey
* @param string $str * @param string $str
* @return string * @return string
*/ */
private static function nullPad128($str) protected static function nullPad128($str)
{ {
$len = strlen($str); $len = strlen($str);
return $str . str_repeat("\0", 16 * ceil($len / 16) - $len); 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;
}
}

View File

@ -145,6 +145,9 @@ class RC4 extends StreamCipher
protected function isValidEngineHelper($engine) protected function isValidEngineHelper($engine)
{ {
if ($engine == self::ENGINE_OPENSSL) { if ($engine == self::ENGINE_OPENSSL) {
if ($this->continuousBuffer) {
return false;
}
if (version_compare(PHP_VERSION, '5.3.7') >= 0) { if (version_compare(PHP_VERSION, '5.3.7') >= 0) {
$this->cipher_name_openssl = 'rc4-40'; $this->cipher_name_openssl = 'rc4-40';
} else { } else {

541
phpseclib/Crypt/Salsa20.php Normal file
View File

@ -0,0 +1,541 @@
<?php
/**
* Pure-PHP implementation of Salsa20.
*
* PHP version 5
*
* @category Crypt
* @package Salsa20
* @author Jim Wigginton <terrafrost@php.net>
* @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 <terrafrost@php.net>
* @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"
);
}
}
}

View File

@ -59,6 +59,7 @@ use phpseclib\Crypt\AES;
use phpseclib\Crypt\RSA; use phpseclib\Crypt\RSA;
use phpseclib\Crypt\TripleDES; use phpseclib\Crypt\TripleDES;
use phpseclib\Crypt\Twofish; 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\Math\BigInteger; // Used to do Diffie-Hellman key exchange and DSA/RSA signature verification.
use phpseclib\System\SSH\Agent; use phpseclib\System\SSH\Agent;
use phpseclib\System\SSH\Agent\Identity as AgentIdentity; use phpseclib\System\SSH\Agent\Identity as AgentIdentity;
@ -355,6 +356,15 @@ class SSH2
*/ */
private $decrypt = false; 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 * Client to Server Encryption Object
* *
@ -364,6 +374,15 @@ class SSH2
*/ */
private $encrypt = false; 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 * Client to Server HMAC Object
* *
@ -938,7 +957,7 @@ class SSH2
* @var array * @var array
* @access private * @access private
*/ */
var $auth = array(); var $auth = [];
/** /**
* Default Constructor. * Default Constructor.
@ -1349,15 +1368,25 @@ class SSH2
//'arcfour', // OPTIONAL the ARCFOUR stream cipher with a 128-bit key //'arcfour', // OPTIONAL the ARCFOUR stream cipher with a 128-bit key
// from <https://tools.ietf.org/html/rfc5647>:
'aes128-gcm@openssh.com',
'aes256-gcm@openssh.com',
// CTR modes from <http://tools.ietf.org/html/rfc4344#section-4>: // CTR modes from <http://tools.ietf.org/html/rfc4344#section-4>:
'aes128-ctr', // RECOMMENDED AES (Rijndael) in SDCTR mode, with 128-bit key 'aes128-ctr', // RECOMMENDED AES (Rijndael) in SDCTR mode, with 128-bit key
'aes192-ctr', // RECOMMENDED AES with 192-bit key 'aes192-ctr', // RECOMMENDED AES with 192-bit key
'aes256-ctr', // RECOMMENDED AES with 256-bit key 'aes256-ctr', // RECOMMENDED AES with 256-bit key
// from <https://git.io/fhxOl>:
// 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 <https://tools.ietf.org/html/rfc5647>:
'aes128-gcm@openssh.com',
'aes256-gcm@openssh.com',
'twofish128-ctr', // OPTIONAL Twofish in SDCTR mode, with 128-bit key 'twofish128-ctr', // OPTIONAL Twofish in SDCTR mode, with 128-bit key
'twofish192-ctr', // OPTIONAL Twofish with 192-bit key 'twofish192-ctr', // OPTIONAL Twofish with 192-bit key
'twofish256-ctr', // OPTIONAL Twofish with 256-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)); $this->encrypt->setIV(substr($iv, 0, $this->encrypt_block_size));
} }
// currently, only AES GCM uses a nonce and per RFC5647, switch ($encrypt) {
// "SSH AES-GCM requires a 12-octet Initial IV" case 'aes128-gcm@openssh.com':
if (!$this->encrypt->usesNonce()) { case 'aes256-gcm@openssh.com':
$this->encrypt->enableContinuousBuffer(); $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id);
} else { $this->encrypt->fixed = substr($nonce, 0, 4);
$nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); $this->encrypt->invocation_counter = substr($nonce, 4, 8);
$this->encrypt->fixed = substr($nonce, 0, 4); case 'chacha20-poly1305@openssh.com':
$this->encrypt->invocation_counter = substr($nonce, 4, 8); break;
default:
$this->encrypt->enableContinuousBuffer();
} }
$key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id); $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id);
while ($encryptKeyLength > strlen($key)) { while ($encryptKeyLength > strlen($key)) {
$key.= $kexHash->hash($keyBytes . $this->exchange_hash . $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->setKey(substr($key, 0, $encryptKeyLength));
$this->encrypt->name = $encrypt; $this->encrypt->name = $encrypt;
} }
@ -1918,19 +1955,29 @@ class SSH2
$this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size)); $this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size));
} }
if (!$this->decrypt->usesNonce()) { switch ($encrypt) {
$this->decrypt->enableContinuousBuffer(); case 'aes128-gcm@openssh.com':
} else { case 'aes256-gcm@openssh.com':
// see https://tools.ietf.org/html/rfc5647#section-7.1 // see https://tools.ietf.org/html/rfc5647#section-7.1
$nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id);
$this->decrypt->fixed = substr($nonce, 0, 4); $this->decrypt->fixed = substr($nonce, 0, 4);
$this->decrypt->invocation_counter = substr($nonce, 4, 8); $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); $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'D' . $this->session_id);
while ($decryptKeyLength > strlen($key)) { while ($decryptKeyLength > strlen($key)) {
$key.= $kexHash->hash($keyBytes . $this->exchange_hash . $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->setKey(substr($key, 0, $decryptKeyLength));
$this->decrypt->name = $decrypt; $this->decrypt->name = $decrypt;
} }
@ -2097,6 +2144,8 @@ class SSH2
case 'twofish256-cbc': case 'twofish256-cbc':
case 'twofish256-ctr': case 'twofish256-ctr':
return 32; return 32;
case 'chacha20-poly1305@openssh.com':
return 64;
} }
return null; return null;
} }
@ -2144,6 +2193,8 @@ class SSH2
case 'aes128-gcm@openssh.com': case 'aes128-gcm@openssh.com':
case 'aes256-gcm@openssh.com': case 'aes256-gcm@openssh.com':
return new AES('gcm'); return new AES('gcm');
case 'chacha20-poly1305@openssh.com':
return new ChaCha20();
} }
return null; return null;
} }
@ -3409,28 +3460,59 @@ class SSH2
} }
if ($this->decrypt) { if ($this->decrypt) {
// only aes128-gcm@openssh.com and aes256-gcm@openssh.com use nonces switch ($this->decrypt->name) {
if (!$this->decrypt->usesNonce()) { case 'aes128-gcm@openssh.com':
$raw = $this->decrypt->decrypt($raw); case 'aes256-gcm@openssh.com':
} else { $this->decrypt->setNonce(
$this->decrypt->setNonce( $this->decrypt->fixed .
$this->decrypt->fixed . $this->decrypt->invocation_counter
$this->decrypt->invocation_counter );
); Strings::increment_str($this->decrypt->invocation_counter);
Strings::increment_str($this->decrypt->invocation_counter); $this->decrypt->setAAD($temp = Strings::shift($raw, 4));
$this->decrypt->setAAD($temp = Strings::shift($raw, 4)); extract(unpack('Npacket_length', $temp));
extract(unpack('Npacket_length', $temp)); /**
/** * @var integer $packet_length
* @var integer $packet_length */
*/
$raw.= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); $raw.= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4);
$stop = microtime(true); $stop = microtime(true);
$tag = stream_get_contents($this->fsock, $this->decrypt_block_size); $tag = stream_get_contents($this->fsock, $this->decrypt_block_size);
$this->decrypt->setTag($tag); $this->decrypt->setTag($tag);
$raw = $this->decrypt->decrypt($raw); $raw = $this->decrypt->decrypt($raw);
$raw = $temp . $raw; $raw = $temp . $raw;
$remaining_length = 0; $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; $remaining_length = $packet_length + 4 - $this->decrypt_block_size;
} }
// quoting <http://tools.ietf.org/html/rfc4253#section-6.1>,
// "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); $buffer = $this->read_remaining_bytes($remaining_length);
if (!isset($stop)) { if (!isset($stop)) {
@ -3510,6 +3579,38 @@ class SSH2
*/ */
private function read_remaining_bytes($remaining_length) 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 <http://tools.ietf.org/html/rfc4253#section-6.1>,
// "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 = ''; $buffer = '';
while ($remaining_length > 0) { while ($remaining_length > 0) {
$temp = stream_get_contents($this->fsock, $remaining_length); $temp = stream_get_contents($this->fsock, $remaining_length);
@ -4113,16 +4214,38 @@ class SSH2
$this->send_seq_no++; $this->send_seq_no++;
if ($this->encrypt) { if ($this->encrypt) {
if (!$this->encrypt->usesNonce()) { switch ($this->encrypt->name) {
$packet = $this->encrypt->encrypt($packet); case 'aes128-gcm@openssh.com':
} else { case 'aes256-gcm@openssh.com':
$this->encrypt->setNonce( $this->encrypt->setNonce(
$this->encrypt->fixed . $this->encrypt->fixed .
$this->encrypt->invocation_counter $this->encrypt->invocation_counter
); );
Strings::increment_str($this->encrypt->invocation_counter); Strings::increment_str($this->encrypt->invocation_counter);
$this->encrypt->setAAD($temp = substr($packet, 0, 4)); $this->encrypt->setAAD($temp = substr($packet, 0, 4));
$packet = $temp . $this->encrypt->encrypt(substr($packet, 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);
} }
} }

View File

@ -0,0 +1,218 @@
<?php
/**
* @author Jim Wigginton <terrafrost@php.net>
* @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");
}
}
}

View File

@ -0,0 +1,160 @@
<?php
/**
* @author Jim Wigginton <terrafrost@php.net>
* @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");
}
}