phpseclib/phpseclib/Crypt/Salsa20.php

504 lines
14 KiB
PHP

<?php
/**
* Pure-PHP implementation of Salsa20.
*
* PHP version 5
*
* @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
*/
declare(strict_types=1);
namespace phpseclib3\Crypt;
use phpseclib3\Common\Functions\Strings;
use phpseclib3\Crypt\Common\StreamCipher;
use phpseclib3\Exception\BadDecryptionException;
use phpseclib3\Exception\InsufficientSetupException;
use phpseclib3\Exception\LengthException;
/**
* Pure-PHP implementation of Salsa20.
*
* @author Jim Wigginton <terrafrost@php.net>
*/
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
/**
* @see \phpseclib3\Crypt\Salsa20::crypt()
*/
public const ENCRYPT = 0;
/**
* @see \phpseclib3\Crypt\Salsa20::crypt()
*/
public 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;
/**
* Salsa20 uses a nonce
*/
public function usesNonce(): bool
{
return true;
}
/**
* Sets the key.
*
* @throws LengthException if the key length isn't supported
*/
public function setKey(string $key): void
{
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.
*/
public function setNonce(string $nonce): void
{
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.
*/
public function setCounter(int $counter): void
{
$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(): void
{
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(): void
{
if (!$this->changed) {
return;
}
$this->enbuffer = $this->debuffer = ['ciphertext' => '', 'counter' => $this->counter];
$this->changed = $this->nonIVChanged = 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(): void
{
// Salsa20 does not utilize this method
}
/**
* Encrypts a message.
*
* @return string $ciphertext
* @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt()
* @see self::crypt()
*/
public function encrypt(string $plaintext): string
{
$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.
*
* @return string $plaintext
* @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt()
* @see self::crypt()
*/
public function decrypt(string $ciphertext): string
{
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
*/
protected function encryptBlock(string $in): string
{
// Salsa20 does not utilize this method
return '';
}
/**
* Decrypts a block
*/
protected function decryptBlock(string $in): string
{
// Salsa20 does not utilize this method
return '';
}
/**
* Encrypts or decrypts a message.
*
* @return string $text
* @see self::decrypt()
* @see self::encrypt()
*/
private function crypt(string $text, int $mode): string
{
$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 = '';
} else {
$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
*/
protected static function leftRotate(int $x, int $n): int
{
if (PHP_INT_SIZE == 8) {
$r1 = $x << $n;
$r1 &= 0xFFFFFFFF;
$r2 = ($x & 0xFFFFFFFF) >> (32 - $n);
} else {
$x = (int) $x;
$r1 = $x << $n;
$r2 = $x >> (32 - $n);
$r2 &= (1 << $n) - 1;
}
return $r1 | $r2;
}
/**
* The quarterround function
*/
protected static function quarterRound(int &$a, int &$b, int &$c, int &$d): void
{
$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 (by reference)
* @param int $x1 (by reference)
* @param int $x2 (by reference)
* @param int $x3 (by reference)
* @param int $x4 (by reference)
* @param int $x5 (by reference)
* @param int $x6 (by reference)
* @param int $x7 (by reference)
* @param int $x8 (by reference)
* @param int $x9 (by reference)
* @param int $x10 (by reference)
* @param int $x11 (by reference)
* @param int $x12 (by reference)
* @param int $x13 (by reference)
* @param int $x14 (by reference)
* @param int $x15 (by reference)
*/
protected static function doubleRound(int &$x0, int &$x1, int &$x2, int &$x3, int &$x4, int &$x5, int &$x6, int &$x7, int &$x8, int &$x9, int &$x10, int &$x11, int &$x12, int &$x13, int &$x14, int &$x15): void
{
// 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
*/
protected static function salsa20(string $x)
{
$z = $x = unpack('V*', $x);
for ($i = 0; $i < 10; $i++) {
static::doubleRound($z[1], $z[2], $z[3], $z[4], $z[5], $z[6], $z[7], $z[8], $z[9], $z[10], $z[11], $z[12], $z[13], $z[14], $z[15], $z[16]);
}
for ($i = 1; $i <= 16; $i++) {
$x[$i] += $z[$i];
}
return pack('V*', ...$x);
}
/**
* Calculates Poly1305 MAC
*
* @see self::decrypt()
* @see self::encrypt()
*/
protected function poly1305(string $text): string
{
if (!$this->usingGeneratedPoly1305Key) {
return parent::poly1305($this->aad . $text);
} 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($text) .
pack('V', strlen($this->aad)) . "\0\0\0\0" .
pack('V', strlen($text)) . "\0\0\0\0"
);
}
}
}