/** * @covers Hex::encode() * @covers Hex::decode() * @covers Hex::encodeUpper() */ public function testRandom() { for ($i = 1; $i < 32; ++$i) { for ($j = 0; $j < 50; ++$j) { $random = \random_bytes($i); $enc = Hex::encode($random); $this->assertSame($random, Hex::decode($enc)); $this->assertSame(\bin2hex($random), $enc); $enc = Hex::encodeUpper($random); $this->assertSame($random, Hex::decode($enc)); $this->assertSame(\strtoupper(\bin2hex($random)), $enc); } } }
public function testVectorBase16() { $this->assertSame(Hex::encode(''), ''); $this->assertSame(Hex::encode('f'), '66'); $this->assertSame(Hex::encode('fo'), '666f'); $this->assertSame(Hex::encode('foo'), '666f6f'); $this->assertSame(Hex::encode('foob'), '666f6f62'); $this->assertSame(Hex::encode('fooba'), '666f6f6261'); $this->assertSame(Hex::encode('foobar'), '666f6f626172'); $this->assertSame(Hex::encodeUpper(''), ''); $this->assertSame(Hex::encodeUpper('f'), '66'); $this->assertSame(Hex::encodeUpper('fo'), '666F'); $this->assertSame(Hex::encodeUpper('foo'), '666F6F'); $this->assertSame(Hex::encodeUpper('foob'), '666F6F62'); $this->assertSame(Hex::encodeUpper('fooba'), '666F6F6261'); $this->assertSame(Hex::encodeUpper('foobar'), '666F6F626172'); }
/** * Convert a private key to the appropriate format. * * @access public * @param \phpseclib\Math\BigInteger $n * @param \phpseclib\Math\BigInteger $e * @param \phpseclib\Math\BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @return string */ static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, $primes, $exponents, $coefficients, $password = '') { if (count($primes) != 2) { return false; } $raw = array('modulus' => $n->toBytes(true), 'publicExponent' => $e->toBytes(true), 'privateExponent' => $d->toBytes(true), 'prime1' => $primes[1]->toBytes(true), 'prime2' => $primes[2]->toBytes(true), 'exponent1' => $exponents[1]->toBytes(true), 'exponent2' => $exponents[2]->toBytes(true), 'coefficient' => $coefficients[2]->toBytes(true)); $key = "PuTTY-User-Key-File-2: ssh-rsa\r\nEncryption: "; $encryption = !empty($password) || is_string($password) ? 'aes256-cbc' : 'none'; $key .= $encryption; $key .= "\r\nComment: " . self::$comment . "\r\n"; $public = pack('Na*Na*Na*', strlen('ssh-rsa'), 'ssh-rsa', strlen($raw['publicExponent']), $raw['publicExponent'], strlen($raw['modulus']), $raw['modulus']); $source = pack('Na*Na*Na*Na*', strlen('ssh-rsa'), 'ssh-rsa', strlen($encryption), $encryption, strlen(self::$comment), self::$comment, strlen($public), $public); $public = Base64::encode($public); $key .= "Public-Lines: " . (strlen($public) + 63 >> 6) . "\r\n"; $key .= chunk_split($public, 64); $private = pack('Na*Na*Na*Na*', strlen($raw['privateExponent']), $raw['privateExponent'], strlen($raw['prime1']), $raw['prime1'], strlen($raw['prime2']), $raw['prime2'], strlen($raw['coefficient']), $raw['coefficient']); if (empty($password) && !is_string($password)) { $source .= pack('Na*', strlen($private), $private); $hashkey = 'putty-private-key-file-mac-key'; } else { $private .= Random::string(16 - (strlen($private) & 15)); $source .= pack('Na*', strlen($private), $private); $crypto = new AES(); $crypto->setKey(static::generateSymmetricKey($password, 32)); $crypto->setIV(str_repeat("", $crypto->getBlockLength() >> 3)); $crypto->disablePadding(); $private = $crypto->encrypt($private); $hashkey = 'putty-private-key-file-mac-key' . $password; } $private = Base64::encode($private); $key .= 'Private-Lines: ' . (strlen($private) + 63 >> 6) . "\r\n"; $key .= chunk_split($private, 64); $hash = new Hash('sha1'); $hash->setKey(sha1($hashkey, true)); $key .= 'Private-MAC: ' . Hex::encode($hash->hash($source)) . "\r\n"; return $key; }
/** * Break a public or private key down into its constituent components * * @access public * @param string $key * @param string $password optional * @return array */ static function load($key, $password = '') { if (!is_string($key)) { return false; } $components = array('isPublicKey' => strpos($key, 'PUBLIC') !== false); /* Although PKCS#1 proposes a format that public and private keys can use, encrypting them is "outside the scope" of PKCS#1. PKCS#1 then refers you to PKCS#12 and PKCS#15 if you're wanting to protect private keys, however, that's not what OpenSSL* does. OpenSSL protects private keys by adding two new "fields" to the key - DEK-Info and Proc-Type. These fields are discussed here: http://tools.ietf.org/html/rfc1421#section-4.6.1.1 http://tools.ietf.org/html/rfc1421#section-4.6.1.3 DES-EDE3-CBC as an algorithm, however, is not discussed anywhere, near as I can tell. DES-CBC and DES-EDE are discussed in RFC1423, however, DES-EDE3-CBC isn't, nor is its key derivation function. As is, the definitive authority on this encoding scheme isn't the IETF but rather OpenSSL's own implementation. ie. the implementation *is* the standard and any bugs that may exist in that implementation are part of the standard, as well. * OpenSSL is the de facto standard. It's utilized by OpenSSH and other projects */ if (preg_match('#DEK-Info: (.+),(.+)#', $key, $matches)) { $iv = Hex::decode(trim($matches[2])); // remove the Proc-Type / DEK-Info sections as they're no longer needed $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $key); $ciphertext = self::_extractBER($key); if ($ciphertext === false) { $ciphertext = $key; } $crypto = self::getEncryptionObject($matches[1]); $crypto->setKey(self::generateSymmetricKey($password, $iv, $crypto->getKeyLength() >> 3)); $crypto->setIV($iv); $key = $crypto->decrypt($ciphertext); if ($key === false) { return false; } } else { if (self::$format != self::MODE_DER) { $decoded = self::_extractBER($key); if ($decoded !== false) { $key = $decoded; } elseif (self::$format == self::MODE_PEM) { return false; } } } if (ord(Strings::shift($key)) != self::ASN1_SEQUENCE) { return false; } if (ASN1::decodeLength($key) != strlen($key)) { return false; } $tag = ord(Strings::shift($key)); /* intended for keys for which OpenSSL's asn1parse returns the following: 0:d=0 hl=4 l= 631 cons: SEQUENCE 4:d=1 hl=2 l= 1 prim: INTEGER :00 7:d=1 hl=2 l= 13 cons: SEQUENCE 9:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption 20:d=2 hl=2 l= 0 prim: NULL 22:d=1 hl=4 l= 609 prim: OCTET STRING ie. PKCS8 keys */ if ($tag == self::ASN1_INTEGER && substr($key, 0, 3) == "0") { Strings::shift($key, 3); $tag = self::ASN1_SEQUENCE; } if ($tag == self::ASN1_SEQUENCE) { $temp = Strings::shift($key, ASN1::decodeLength($key)); if (ord(Strings::shift($temp)) != self::ASN1_OBJECT) { return false; } $length = ASN1::decodeLength($temp); switch (Strings::shift($temp, $length)) { case "*†H†÷\r": // rsaEncryption break; case "*†H†÷\r": // pbeWithMD5AndDES-CBC /* PBEParameter ::= SEQUENCE { salt OCTET STRING (SIZE(8)), iterationCount INTEGER } */ if (ord(Strings::shift($temp)) != self::ASN1_SEQUENCE) { return false; } if (ASN1::decodeLength($temp) != strlen($temp)) { return false; } Strings::shift($temp); // assume it's an octet string $salt = Strings::shift($temp, ASN1::decodeLength($temp)); if (ord(Strings::shift($temp)) != self::ASN1_INTEGER) { return false; } ASN1::decodeLength($temp); list(, $iterationCount) = unpack('N', str_pad($temp, 4, chr(0), STR_PAD_LEFT)); Strings::shift($key); // assume it's an octet string $length = ASN1::decodeLength($key); if (strlen($key) != $length) { return false; } $crypto = new DES(DES::MODE_CBC); $crypto->setPassword($password, 'pbkdf1', 'md5', $salt, $iterationCount); $key = $crypto->decrypt($key); if ($key === false) { return false; } return self::load($key); default: return false; } /* intended for keys for which OpenSSL's asn1parse returns the following: 0:d=0 hl=4 l= 290 cons: SEQUENCE 4:d=1 hl=2 l= 13 cons: SEQUENCE 6:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption 17:d=2 hl=2 l= 0 prim: NULL 19:d=1 hl=4 l= 271 prim: BIT STRING */ $tag = ord(Strings::shift($key)); // skip over the BIT STRING / OCTET STRING tag ASN1::decodeLength($key); // skip over the BIT STRING / OCTET STRING length // "The initial octet shall encode, as an unsigned binary integer wtih bit 1 as the least significant bit, the number of // unused bits in the final subsequent octet. The number shall be in the range zero to seven." // -- http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf (section 8.6.2.2) if ($tag == self::ASN1_BITSTRING) { Strings::shift($key); } if (ord(Strings::shift($key)) != self::ASN1_SEQUENCE) { return false; } if (ASN1::decodeLength($key) != strlen($key)) { return false; } $tag = ord(Strings::shift($key)); } if ($tag != self::ASN1_INTEGER) { return false; } $length = ASN1::decodeLength($key); $temp = Strings::shift($key, $length); if (strlen($temp) != 1 || ord($temp) > 2) { $components['modulus'] = new BigInteger($temp, 256); Strings::shift($key); // skip over self::ASN1_INTEGER $length = ASN1::decodeLength($key); $components[$components['isPublicKey'] ? 'publicExponent' : 'privateExponent'] = new BigInteger(Strings::shift($key, $length), 256); return $components; } if (ord(Strings::shift($key)) != self::ASN1_INTEGER) { return false; } $length = ASN1::decodeLength($key); $components['modulus'] = new BigInteger(Strings::shift($key, $length), 256); Strings::shift($key); $length = ASN1::decodeLength($key); $components['publicExponent'] = new BigInteger(Strings::shift($key, $length), 256); Strings::shift($key); $length = ASN1::decodeLength($key); $components['privateExponent'] = new BigInteger(Strings::shift($key, $length), 256); Strings::shift($key); $length = ASN1::decodeLength($key); $components['primes'] = array(1 => new BigInteger(Strings::shift($key, $length), 256)); Strings::shift($key); $length = ASN1::decodeLength($key); $components['primes'][] = new BigInteger(Strings::shift($key, $length), 256); Strings::shift($key); $length = ASN1::decodeLength($key); $components['exponents'] = array(1 => new BigInteger(Strings::shift($key, $length), 256)); Strings::shift($key); $length = ASN1::decodeLength($key); $components['exponents'][] = new BigInteger(Strings::shift($key, $length), 256); Strings::shift($key); $length = ASN1::decodeLength($key); $components['coefficients'] = array(2 => new BigInteger(Strings::shift($key, $length), 256)); if (!empty($key)) { if (ord(Strings::shift($key)) != self::ASN1_SEQUENCE) { return false; } ASN1::decodeLength($key); while (!empty($key)) { if (ord(Strings::shift($key)) != self::ASN1_SEQUENCE) { return false; } ASN1::decodeLength($key); $key = substr($key, 1); $length = ASN1::decodeLength($key); $components['primes'][] = new BigInteger(Strings::shift($key, $length), 256); Strings::shift($key); $length = ASN1::decodeLength($key); $components['exponents'][] = new BigInteger(Strings::shift($key, $length), 256); Strings::shift($key); $length = ASN1::decodeLength($key); $components['coefficients'][] = new BigInteger(Strings::shift($key, $length), 256); } } return $components; }
/** * Generates a digest from $bytes * * @see self::_setupInlineCrypt() * @access private * @param $bytes * @return string */ function _hashInlineCryptFunction($bytes) { if (!isset(self::$WHIRLPOOL_AVAILABLE)) { self::$WHIRLPOOL_AVAILABLE = extension_loaded('hash') && in_array('whirlpool', hash_algos()); } $result = ''; $hash = $bytes; switch (true) { case self::$WHIRLPOOL_AVAILABLE: foreach (str_split($bytes, 64) as $t) { $hash = hash('whirlpool', $hash, true); $result .= $t ^ $hash; } return $result . hash('whirlpool', $hash, true); default: $len = strlen($bytes); for ($i = 0; $i < $len; $i += 20) { $t = substr($bytes, $i, 20); $hash = Hex::decode(sha1($hash)); $result .= $t ^ $hash; } return $result . Hex::decode(sha1($hash)); } }
/** * Performs modular exponentiation. * * Here's an example: * <code> * <?php * $a = new \phpseclib\Math\BigInteger('10'); * $b = new \phpseclib\Math\BigInteger('20'); * $c = new \phpseclib\Math\BigInteger('30'); * * $c = $a->modPow($b, $c); * * echo $c->toString(); // outputs 10 * ?> * </code> * * @param \phpseclib\Math\BigInteger $e * @param \phpseclib\Math\BigInteger $n * @return \phpseclib\Math\BigInteger * @access public * @internal The most naive approach to modular exponentiation has very unreasonable requirements, and * and although the approach involving repeated squaring does vastly better, it, too, is impractical * for our purposes. The reason being that division - by far the most complicated and time-consuming * of the basic operations (eg. +,-,*,/) - occurs multiple times within it. * * Modular reductions resolve this issue. Although an individual modular reduction takes more time * then an individual division, when performed in succession (with the same modulo), they're a lot faster. * * The two most commonly used modular reductions are Barrett and Montgomery reduction. Montgomery reduction, * although faster, only works when the gcd of the modulo and of the base being used is 1. In RSA, when the * base is a power of two, the modulo - a product of two primes - is always going to have a gcd of 1 (because * the product of two odd numbers is odd), but what about when RSA isn't used? * * In contrast, Barrett reduction has no such constraint. As such, some bigint implementations perform a * Barrett reduction after every operation in the modpow function. Others perform Barrett reductions when the * modulo is even and Montgomery reductions when the modulo is odd. BigInteger.java's modPow method, however, * uses a trick involving the Chinese Remainder Theorem to factor the even modulo into two numbers - one odd and * the other, a power of two - and recombine them, later. This is the method that this modPow function uses. * {@link http://islab.oregonstate.edu/papers/j34monex.pdf Montgomery Reduction with Even Modulus} elaborates. */ function modPow(BigInteger $e, BigInteger $n) { $n = $this->bitmask !== false && $this->bitmask->compare($n) < 0 ? $this->bitmask : $n->abs(); if ($e->compare(new static()) < 0) { $e = $e->abs(); $temp = $this->modInverse($n); if ($temp === false) { return false; } return $this->_normalize($temp->modPow($e, $n)); } if (MATH_BIGINTEGER_MODE == self::MODE_GMP) { $temp = new static(); $temp->value = gmp_powm($this->value, $e->value, $n->value); return $this->_normalize($temp); } if ($this->compare(new static()) < 0 || $this->compare($n) > 0) { list(, $temp) = $this->divide($n); return $temp->modPow($e, $n); } if (defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) { $components = array('modulus' => $n->toBytes(true), 'publicExponent' => $e->toBytes(true)); $components = array('modulus' => pack('Ca*a*', 2, self::_encodeASN1Length(strlen($components['modulus'])), $components['modulus']), 'publicExponent' => pack('Ca*a*', 2, self::_encodeASN1Length(strlen($components['publicExponent'])), $components['publicExponent'])); $RSAPublicKey = pack('Ca*a*a*', 48, self::_encodeASN1Length(strlen($components['modulus']) + strlen($components['publicExponent'])), $components['modulus'], $components['publicExponent']); $rsaOID = Hex::decode('300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA $RSAPublicKey = chr(0) . $RSAPublicKey; $RSAPublicKey = chr(3) . self::_encodeASN1Length(strlen($RSAPublicKey)) . $RSAPublicKey; $encapsulated = pack('Ca*a*', 48, self::_encodeASN1Length(strlen($rsaOID . $RSAPublicKey)), $rsaOID . $RSAPublicKey); $RSAPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . chunk_split(base64_encode($encapsulated)) . '-----END PUBLIC KEY-----'; $plaintext = str_pad($this->toBytes(), strlen($n->toBytes(true)) - 1, "", STR_PAD_LEFT); if (openssl_public_encrypt($plaintext, $result, $RSAPublicKey, OPENSSL_NO_PADDING)) { return new static($result, 256); } } if (MATH_BIGINTEGER_MODE == self::MODE_BCMATH) { $temp = new static(); $temp->value = bcpowmod($this->value, $e->value, $n->value, 0); return $this->_normalize($temp); } if (empty($e->value)) { $temp = new static(); $temp->value = array(1); return $this->_normalize($temp); } if ($e->value == array(1)) { list(, $temp) = $this->divide($n); return $this->_normalize($temp); } if ($e->value == array(2)) { $temp = new static(); $temp->value = self::_square($this->value); list(, $temp) = $temp->divide($n); return $this->_normalize($temp); } return $this->_normalize($this->_slidingWindow($e, $n, self::BARRETT)); // the following code, although not callable, can be run independently of the above code // although the above code performed better in my benchmarks the following could might // perform better under different circumstances. in lieu of deleting it it's just been // made uncallable // is the modulo odd? if ($n->value[0] & 1) { return $this->_normalize($this->_slidingWindow($e, $n, self::MONTGOMERY)); } // if it's not, it's even // find the lowest set bit (eg. the max pow of 2 that divides $n) for ($i = 0; $i < count($n->value); ++$i) { if ($n->value[$i]) { $temp = decbin($n->value[$i]); $j = strlen($temp) - strrpos($temp, '1') - 1; $j += 26 * $i; break; } } // at this point, 2^$j * $n/(2^$j) == $n $mod1 = clone $n; $mod1->_rshift($j); $mod2 = new static(); $mod2->value = array(1); $mod2->_lshift($j); $part1 = $mod1->value != array(1) ? $this->_slidingWindow($e, $mod1, self::MONTGOMERY) : new static(); $part2 = $this->_slidingWindow($e, $mod2, self::POWEROF2); $y1 = $mod2->modInverse($mod1); $y2 = $mod1->modInverse($mod2); $result = $part1->multiply($mod2); $result = $result->multiply($y1); $temp = $part2->multiply($mod1); $temp = $temp->multiply($y2); $result = $result->add($temp); list(, $result) = $result->divide($n); return $this->_normalize($result); }
/** * Get the Distinguished Name for a certificates subject * * @param mixed $format optional * @param array $dn optional * @access public * @return bool */ function getDN($format = self::DN_ARRAY, $dn = null) { if (!isset($dn)) { $dn = isset($this->currentCert['tbsCertList']) ? $this->currentCert['tbsCertList']['issuer'] : $this->dn; } switch ((int) $format) { case self::DN_ARRAY: return $dn; case self::DN_ASN1: $asn1 = new ASN1(); $asn1->loadOIDs($this->oids); $filters = array(); $filters['rdnSequence']['value'] = array('type' => ASN1::TYPE_UTF8_STRING); $asn1->loadFilters($filters); $this->_mapOutDNs($dn, 'rdnSequence', $asn1); return $asn1->encodeDER($dn, $this->Name); case self::DN_CANON: // No SEQUENCE around RDNs and all string values normalized as // trimmed lowercase UTF-8 with all spacing as one blank. // constructed RDNs will not be canonicalized $asn1 = new ASN1(); $asn1->loadOIDs($this->oids); $filters = array(); $filters['value'] = array('type' => ASN1::TYPE_UTF8_STRING); $asn1->loadFilters($filters); $result = ''; $this->_mapOutDNs($dn, 'rdnSequence', $asn1); foreach ($dn['rdnSequence'] as $rdn) { foreach ($rdn as $i => $attr) { $attr =& $rdn[$i]; if (is_array($attr['value'])) { foreach ($attr['value'] as $type => $v) { $type = array_search($type, $asn1->ANYmap, true); if ($type !== false && isset($asn1->stringTypeSize[$type])) { $v = $asn1->convert($v, $type); if ($v !== false) { $v = preg_replace('/\\s+/', ' ', $v); $attr['value'] = strtolower(trim($v)); break; } } } } } $result .= $asn1->encodeDER($rdn, $this->RelativeDistinguishedName); } return $result; case self::DN_HASH: $dn = $this->getDN(self::DN_CANON, $dn); $hash = new Hash('sha1'); $hash = $hash->hash($dn); extract(unpack('Vhash', $hash)); return strtolower(Hex::encode(pack('N', $hash))); } // Default is to return a string. $start = true; $output = ''; $result = array(); $asn1 = new ASN1(); $asn1->loadOIDs($this->oids); $filters = array(); $filters['rdnSequence']['value'] = array('type' => ASN1::TYPE_UTF8_STRING); $asn1->loadFilters($filters); $this->_mapOutDNs($dn, 'rdnSequence', $asn1); foreach ($dn['rdnSequence'] as $field) { $prop = $field[0]['type']; $value = $field[0]['value']; $delim = ', '; switch ($prop) { case 'id-at-countryName': $desc = 'C'; break; case 'id-at-stateOrProvinceName': $desc = 'ST'; break; case 'id-at-organizationName': $desc = 'O'; break; case 'id-at-organizationalUnitName': $desc = 'OU'; break; case 'id-at-commonName': $desc = 'CN'; break; case 'id-at-localityName': $desc = 'L'; break; case 'id-at-surname': $desc = 'SN'; break; case 'id-at-uniqueIdentifier': $delim = '/'; $desc = 'x500UniqueIdentifier'; break; case 'id-at-postalAddress': $delim = '/'; $desc = 'postalAddress'; break; default: $delim = '/'; $desc = preg_replace('#.+-([^-]+)$#', '$1', $prop); } if (!$start) { $output .= $delim; } if (is_array($value)) { foreach ($value as $type => $v) { $type = array_search($type, $asn1->ANYmap, true); if ($type !== false && isset($asn1->stringTypeSize[$type])) { $v = $asn1->convert($v, $type); if ($v !== false) { $value = $v; break; } } } if (is_array($value)) { $value = array_pop($value); // Always strip data type. } } elseif (is_object($value) && $value instanceof Element) { $callback = create_function('$x', 'return "\\x" . bin2hex($x[0]);'); $value = strtoupper(preg_replace_callback('#[^\\x20-\\x7E]#', $callback, $value->element)); } $output .= $desc . '=' . $value; $result[$desc] = isset($result[$desc]) ? array_merge((array) $dn[$prop], array($value)) : $value; $start = false; } return $format == self::DN_OPENSSL ? $result : $output; }
/** * Convert a private key to the appropriate format. * * @access public * @param \phpseclib\Math\BigInteger $n * @param \phpseclib\Math\BigInteger $e * @param \phpseclib\Math\BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @return string */ static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, $primes, $exponents, $coefficients, $password = '') { $num_primes = count($primes); $raw = array('version' => $num_primes == 2 ? chr(0) : chr(1), 'modulus' => $n->toBytes(true), 'publicExponent' => $e->toBytes(true), 'privateExponent' => $d->toBytes(true), 'prime1' => $primes[1]->toBytes(true), 'prime2' => $primes[2]->toBytes(true), 'exponent1' => $exponents[1]->toBytes(true), 'exponent2' => $exponents[2]->toBytes(true), 'coefficient' => $coefficients[2]->toBytes(true)); $components = array(); foreach ($raw as $name => $value) { $components[$name] = pack('Ca*a*', self::ASN1_INTEGER, ASN1::encodeLength(strlen($value)), $value); } $RSAPrivateKey = implode('', $components); if ($num_primes > 2) { $OtherPrimeInfos = ''; for ($i = 3; $i <= $num_primes; $i++) { // OtherPrimeInfos ::= SEQUENCE SIZE(1..MAX) OF OtherPrimeInfo // // OtherPrimeInfo ::= SEQUENCE { // prime INTEGER, -- ri // exponent INTEGER, -- di // coefficient INTEGER -- ti // } $OtherPrimeInfo = pack('Ca*a*', self::ASN1_INTEGER, ASN1::encodeLength(strlen($primes[$i]->toBytes(true))), $primes[$i]->toBytes(true)); $OtherPrimeInfo .= pack('Ca*a*', self::ASN1_INTEGER, ASN1::encodeLength(strlen($exponents[$i]->toBytes(true))), $exponents[$i]->toBytes(true)); $OtherPrimeInfo .= pack('Ca*a*', self::ASN1_INTEGER, ASN1::encodeLength(strlen($coefficients[$i]->toBytes(true))), $coefficients[$i]->toBytes(true)); $OtherPrimeInfos .= pack('Ca*a*', self::ASN1_SEQUENCE, ASN1::encodeLength(strlen($OtherPrimeInfo)), $OtherPrimeInfo); } $RSAPrivateKey .= pack('Ca*a*', self::ASN1_SEQUENCE, ASN1::encodeLength(strlen($OtherPrimeInfos)), $OtherPrimeInfos); } $RSAPrivateKey = pack('Ca*a*', self::ASN1_SEQUENCE, ASN1::encodeLength(strlen($RSAPrivateKey)), $RSAPrivateKey); if (!empty($password) || is_string($password)) { $cipher = self::getEncryptionObject(self::$defaultEncryptionAlgorithm); $iv = Random::string($cipher->getBlockLength() >> 3); $cipher->setKey(self::generateSymmetricKey($password, $iv, $cipher->getKeyLength() >> 3)); $cipher->setIV($iv); $iv = strtoupper(Hex::encode($iv)); $RSAPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\r\n" . "Proc-Type: 4,ENCRYPTED\r\n" . "DEK-Info: " . self::$defaultEncryptionAlgorithm . ",{$iv}\r\n" . "\r\n" . chunk_split(Base64::encode($cipher->encrypt($RSAPrivateKey)), 64) . '-----END RSA PRIVATE KEY-----'; } else { $RSAPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\r\n" . chunk_split(Base64::encode($RSAPrivateKey), 64) . '-----END RSA PRIVATE KEY-----'; } return $RSAPrivateKey; }
/** * Coerce a string into base64 format. * * @param string $hash * @param string $algo * @return string * @throws \Exception */ protected function coerceBase64(string $hash, string $algo = 'sha256') : string { switch ($algo) { case 'sha256': $limits = ['raw' => 32, 'hex' => 64, 'pad_min' => 40, 'pad_max' => 44]; break; default: throw new \Exception('Browsers currently only support sha256 public key pins.'); } $len = Binary::safeStrlen($hash); if ($len === $limits['hex']) { $hash = Base64::encode(Hex::decode($hash)); } elseif ($len === $limits['raw']) { $hash = Base64::encode($hash); } elseif ($len > $limits['pad_min'] && $len < $limits['pad_max']) { // Padding was stripped! $hash .= \str_repeat('=', $len % 4); // Base64UrlSsafe encoded. if (\strpos($hash, '_') !== false || \strpos($hash, '-') !== false) { $hash = Base64UrlSafe::decode($hash); } else { $hash = Base64::decode($hash); } $hash = Base64::encode($hash); } return $hash; }
/** * EMSA-PKCS1-V1_5-ENCODE * * See {@link http://tools.ietf.org/html/rfc3447#section-9.2 RFC3447#section-9.2}. * * @access private * @param string $m * @param int $emLen * @throws \LengthException if the intended encoded message length is too short * @return string */ function _emsa_pkcs1_v1_5_encode($m, $emLen) { $h = $this->hash->hash($m); // see http://tools.ietf.org/html/rfc3447#page-43 switch ($this->hashName) { case 'md2': $t = Hex::decode('3020300c06082a864886f70d020205000410'); break; case 'md5': $t = Hex::decode('3020300c06082a864886f70d020505000410'); break; case 'sha1': $t = Hex::decode('3021300906052b0e03021a05000414'); break; case 'sha256': $t = Hex::decode('3031300d060960864801650304020105000420'); break; case 'sha384': $t = Hex::decode('3041300d060960864801650304020205000430'); break; case 'sha512': $t = Hex::decode('3051300d060960864801650304020305000440'); } $t .= $h; $tLen = strlen($t); if ($emLen < $tLen + 11) { throw new \LengthException('Intended encoded message length too short'); } $ps = str_repeat(chr(0xff), $emLen - $tLen - 3); $em = "{$ps}{$t}"; return $em; }
/** * Wrap a private key appropriately * * @access public * @param string $key * @param string $type * @param string $password * @return string */ static function wrapPrivateKey($key, $type, $password) { if (empty($password) || !is_string($password)) { return "-----BEGIN {$type} PRIVATE KEY-----\r\n" . chunk_split(Base64::encode($key), 64) . "-----END {$type} PRIVATE KEY-----"; } $cipher = self::getEncryptionObject(self::$defaultEncryptionAlgorithm); $iv = Random::string($cipher->getBlockLength() >> 3); $cipher->setKey(self::generateSymmetricKey($password, $iv, $cipher->getKeyLength() >> 3)); $cipher->setIV($iv); $iv = strtoupper(Hex::encode($iv)); return "-----BEGIN {$type} PRIVATE KEY-----\r\n" . "Proc-Type: 4,ENCRYPTED\r\n" . "DEK-Info: " . self::$defaultEncryptionAlgorithm . ",{$iv}\r\n" . "\r\n" . chunk_split(Base64::encode($cipher->encrypt($key)), 64) . "-----END {$type} PRIVATE KEY-----"; }
/** * Generate a random string. * * Although microoptimizations are generally discouraged as they impair readability this function is ripe with * microoptimizations because this function has the potential of being called a huge number of times. * eg. for RSA key generation. * * @param int $length * @throws \RuntimeException if a symmetric cipher is needed but not loaded * @return string */ static function string($length) { if (version_compare(PHP_VERSION, '7.0.0', '>=')) { try { return \random_bytes($length); } catch (\Throwable $e) { // If a sufficient source of randomness is unavailable, random_bytes() will throw an // object that implements the Throwable interface (Exception, TypeError, Error). // We don't actually need to do anything here. The string() method should just continue // as normal. Note, however, that if we don't have a sufficient source of randomness for // random_bytes(), most of the other calls here will fail too, so we'll end up using // the PHP implementation. } } if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { // method 1. prior to PHP 5.3 this would call rand() on windows hence the function_exists('class_alias') call. // ie. class_alias is a function that was introduced in PHP 5.3 if (extension_loaded('mcrypt') && function_exists('class_alias')) { return mcrypt_create_iv($length); } // method 2. openssl_random_pseudo_bytes was introduced in PHP 5.3.0 but prior to PHP 5.3.4 there was, // to quote <http://php.net/ChangeLog-5.php#5.3.4>, "possible blocking behavior". as of 5.3.4 // openssl_random_pseudo_bytes and mcrypt_create_iv do the exact same thing on Windows. ie. they both // call php_win32_get_random_bytes(): // // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/openssl/openssl.c#L5008 // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1392 // // php_win32_get_random_bytes() is defined thusly: // // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/win32/winutil.c#L80 // // we're calling it, all the same, in the off chance that the mcrypt extension is not available if (extension_loaded('openssl') && version_compare(PHP_VERSION, '5.3.4', '>=')) { return openssl_random_pseudo_bytes($length); } } else { // method 1. the fastest if (extension_loaded('openssl')) { return openssl_random_pseudo_bytes($length); } // method 2 static $fp = true; if ($fp === true) { // warning's will be output unles the error suppression operator is used. errors such as // "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc. $fp = @fopen('/dev/urandom', 'rb'); } if ($fp !== true && $fp !== false) { // surprisingly faster than !is_bool() or is_resource() return fread($fp, $length); } // method 3. pretty much does the same thing as method 2 per the following url: // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1391 // surprisingly slower than method 2. maybe that's because mcrypt_create_iv does a bunch of error checking that we're // not doing. regardless, this'll only be called if this PHP script couldn't open /dev/urandom due to open_basedir // restrictions or some such if (extension_loaded('mcrypt')) { return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); } } // at this point we have no choice but to use a pure-PHP CSPRNG // cascade entropy across multiple PHP instances by fixing the session and collecting all // environmental variables, including the previous session data and the current session // data. // // mt_rand seeds itself by looking at the PID and the time, both of which are (relatively) // easy to guess at. linux uses mouse clicks, keyboard timings, etc, as entropy sources, but // PHP isn't low level to be able to use those as sources and on a web server there's not likely // going to be a ton of keyboard or mouse action. web servers do have one thing that we can use // however, a ton of people visiting the website. obviously you don't want to base your seeding // soley on parameters a potential attacker sends but (1) not everything in $_SERVER is controlled // by the user and (2) this isn't just looking at the data sent by the current user - it's based // on the data sent by all users. one user requests the page and a hash of their info is saved. // another user visits the page and the serialization of their data is utilized along with the // server envirnment stuff and a hash of the previous http request data (which itself utilizes // a hash of the session data before that). certainly an attacker should be assumed to have // full control over his own http requests. he, however, is not going to have control over // everyone's http requests. static $crypto = false, $v; if ($crypto === false) { // save old session data $old_session_id = session_id(); $old_use_cookies = ini_get('session.use_cookies'); $old_session_cache_limiter = session_cache_limiter(); $_OLD_SESSION = isset($_SESSION) ? $_SESSION : false; if ($old_session_id != '') { session_write_close(); } session_id(1); ini_set('session.use_cookies', 0); session_cache_limiter(''); session_start(); $v = $seed = $_SESSION['seed'] = Hex::decode(sha1((isset($_SERVER) ? self::safe_serialize($_SERVER) : '') . (isset($_POST) ? self::safe_serialize($_POST) : '') . (isset($_GET) ? self::safe_serialize($_GET) : '') . (isset($_COOKIE) ? self::safe_serialize($_COOKIE) : '') . self::safe_serialize($GLOBALS) . self::safe_serialize($_SESSION) . self::safe_serialize($_OLD_SESSION))); if (!isset($_SESSION['count'])) { $_SESSION['count'] = 0; } $_SESSION['count']++; session_write_close(); // restore old session data if ($old_session_id != '') { session_id($old_session_id); session_start(); ini_set('session.use_cookies', $old_use_cookies); session_cache_limiter($old_session_cache_limiter); } else { if ($_OLD_SESSION !== false) { $_SESSION = $_OLD_SESSION; unset($_OLD_SESSION); } else { unset($_SESSION); } } // in SSH2 a shared secret and an exchange hash are generated through the key exchange process. // the IV client to server is the hash of that "nonce" with the letter A and for the encryption key it's the letter C. // if the hash doesn't produce enough a key or an IV that's long enough concat successive hashes of the // original hash and the current hash. we'll be emulating that. for more info see the following URL: // // http://tools.ietf.org/html/rfc4253#section-7.2 // // see the is_string($crypto) part for an example of how to expand the keys $key = Hex::decode(sha1($seed . 'A')); $iv = Hex::decode(sha1($seed . 'C')); // ciphers are used as per the nist.gov link below. also, see this link: // // http://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Designs_based_on_cryptographic_primitives switch (true) { case class_exists('\\phpseclib\\Crypt\\AES'): $crypto = new AES(Base::MODE_CTR); break; case class_exists('\\phpseclib\\Crypt\\Twofish'): $crypto = new Twofish(Base::MODE_CTR); break; case class_exists('\\phpseclib\\Crypt\\Blowfish'): $crypto = new Blowfish(Base::MODE_CTR); break; case class_exists('\\phpseclib\\Crypt\\TripleDES'): $crypto = new TripleDES(Base::MODE_CTR); break; case class_exists('\\phpseclib\\Crypt\\DES'): $crypto = new DES(Base::MODE_CTR); break; case class_exists('\\phpseclib\\Crypt\\RC4'): $crypto = new RC4(); break; default: throw new \RuntimeException(__CLASS__ . ' requires at least one symmetric cipher be loaded'); } $crypto->setKey($key); $crypto->setIV($iv); $crypto->enableContinuousBuffer(); } //return $crypto->encrypt(str_repeat("\0", $length)); // the following is based off of ANSI X9.31: // // http://csrc.nist.gov/groups/STM/cavp/documents/rng/931rngext.pdf // // OpenSSL uses that same standard for it's random numbers: // // http://www.opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/fips-1.0/rand/fips_rand.c // (do a search for "ANS X9.31 A.2.4") $result = ''; while (strlen($result) < $length) { $i = $crypto->encrypt(microtime()); // strlen(microtime()) == 21 $r = $crypto->encrypt($i ^ $v); // strlen($v) == 20 $v = $crypto->encrypt($r ^ $i); // strlen($r) == 20 $result .= $r; } return substr($result, 0, $length); }
/** * Convert a public key to the appropriate format * * @access public * @param \phpseclib\Math\BigInteger $n * @param \phpseclib\Math\BigInteger $e * @return string */ static function savePublicKey(BigInteger $n, BigInteger $e) { $modulus = $n->toBytes(true); $publicExponent = $e->toBytes(true); // from <http://tools.ietf.org/html/rfc3447#appendix-A.1.1>: // RSAPublicKey ::= SEQUENCE { // modulus INTEGER, -- n // publicExponent INTEGER -- e // } $components = array('modulus' => pack('Ca*a*', self::ASN1_INTEGER, self::_encodeLength(strlen($modulus)), $modulus), 'publicExponent' => pack('Ca*a*', self::ASN1_INTEGER, self::_encodeLength(strlen($publicExponent)), $publicExponent)); $RSAPublicKey = pack('Ca*a*a*', self::ASN1_SEQUENCE, self::_encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), $components['modulus'], $components['publicExponent']); // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. $rsaOID = Hex::decode('300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA $RSAPublicKey = chr(0) . $RSAPublicKey; $RSAPublicKey = chr(3) . self::_encodeLength(strlen($RSAPublicKey)) . $RSAPublicKey; $RSAPublicKey = pack('Ca*a*', self::ASN1_SEQUENCE, self::_encodeLength(strlen($rsaOID . $RSAPublicKey)), $rsaOID . $RSAPublicKey); $RSAPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . chunk_split(Base64::encode($RSAPublicKey), 64) . '-----END PUBLIC KEY-----'; return $RSAPublicKey; }
/** * Converts a BigInteger to a hex string (eg. base-16)). * * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're * saved as two's compliment. * * Here's an example: * <code> * <?php * $a = new \phpseclib\Math\BigInteger('65'); * * echo $a->toHex(); // outputs '41' * ?> * </code> * * @param bool $twos_compliment * @return string * @access public * @internal Converts a base-2**26 number to base-2**8 */ function toHex($twos_compliment = false) { return Hex::encode($this->toBytes($twos_compliment)); }
/** * Parse Attributes * * See '7. File Attributes' of draft-ietf-secsh-filexfer-13 for more info. * * @param string $response * @return array * @access private */ function _parseAttributes(&$response) { $attr = array(); if (strlen($response) < 4) { //user_error('Malformed file attributes'); return array(); } extract(unpack('Nflags', Strings::shift($response, 4))); // SFTPv4+ have a type field (a byte) that follows the above flag field foreach ($this->attributes as $key => $value) { switch ($flags & $key) { case NET_SFTP_ATTR_SIZE: // 0x00000001 // The size attribute is defined as an unsigned 64-bit integer. // The following will use floats on 32-bit platforms, if necessary. // As can be seen in the BigInteger class, floats are generally // IEEE 754 binary64 "double precision" on such platforms and // as such can represent integers of at least 2^50 without loss // of precision. Interpreted in filesize, 2^50 bytes = 1024 TiB. $attr['size'] = hexdec(Hex::encode(Strings::shift($response, 8))); break; case NET_SFTP_ATTR_UIDGID: // 0x00000002 (SFTPv3 only) if (strlen($response) < 8) { //user_error('Malformed file attributes'); return $attr; } $attr += unpack('Nuid/Ngid', Strings::shift($response, 8)); break; case NET_SFTP_ATTR_PERMISSIONS: // 0x00000004 if (strlen($response) < 4) { //user_error('Malformed file attributes'); return $attr; } $attr += unpack('Npermissions', Strings::shift($response, 4)); // mode == permissions; permissions was the original array key and is retained for bc purposes. // mode was added because that's the more industry standard terminology $attr += array('mode' => $attr['permissions']); $fileType = $this->_parseMode($attr['permissions']); if ($fileType !== false) { $attr += array('type' => $fileType); } break; case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008 if (strlen($response) < 8) { //user_error('Malformed file attributes'); return $attr; } $attr += unpack('Natime/Nmtime', Strings::shift($response, 8)); break; case NET_SFTP_ATTR_EXTENDED: // 0x80000000 if (strlen($response) < 4) { //user_error('Malformed file attributes'); return $attr; } extract(unpack('Ncount', Strings::shift($response, 4))); for ($i = 0; $i < $count; $i++) { if (strlen($response) < 4) { //user_error('Malformed file attributes'); return $attr; } extract(unpack('Nlength', Strings::shift($response, 4))); $key = Strings::shift($response, $length); if (strlen($response) < 4) { //user_error('Malformed file attributes'); return $attr; } extract(unpack('Nlength', Strings::shift($response, 4))); $attr[$key] = Strings::shift($response, $length); } } } return $attr; }
/** * Get the Distinguished Name for a certificates subject * * @param mixed $format optional * @param array $dn optional * @access public * @return bool */ function getDN($format = self::DN_ARRAY, $dn = null) { if (!isset($dn)) { $dn = isset($this->currentCert['tbsCertList']) ? $this->currentCert['tbsCertList']['issuer'] : $this->dn; } switch ((int) $format) { case self::DN_ARRAY: return $dn; case self::DN_ASN1: $asn1 = new ASN1(); $asn1->loadOIDs($this->oids); $filters = array(); $filters['rdnSequence']['value'] = array('type' => ASN1::TYPE_UTF8_STRING); $asn1->loadFilters($filters); return $asn1->encodeDER($dn, $this->Name); case self::DN_OPENSSL: $dn = $this->getDN(self::DN_STRING, $dn); if ($dn === false) { return false; } $attrs = preg_split('#((?:^|, *|/)[a-z][a-z0-9]*=)#i', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); $dn = array(); for ($i = 1; $i < count($attrs); $i += 2) { $prop = trim($attrs[$i], ', =/'); $value = $attrs[$i + 1]; if (!isset($dn[$prop])) { $dn[$prop] = $value; } else { $dn[$prop] = array_merge((array) $dn[$prop], array($value)); } } return $dn; case self::DN_CANON: // No SEQUENCE around RDNs and all string values normalized as // trimmed lowercase UTF-8 with all spacing as one blank. $asn1 = new ASN1(); $asn1->loadOIDs($this->oids); $filters = array(); $filters['value'] = array('type' => ASN1::TYPE_UTF8_STRING); $asn1->loadFilters($filters); $result = ''; foreach ($dn['rdnSequence'] as $rdn) { foreach ($rdn as $i => $attr) { $attr =& $rdn[$i]; if (is_array($attr['value'])) { foreach ($attr['value'] as $type => $v) { $type = array_search($type, $asn1->ANYmap, true); if ($type !== false && isset($asn1->stringTypeSize[$type])) { $v = $asn1->convert($v, $type); if ($v !== false) { $v = preg_replace('/\\s+/', ' ', $v); $attr['value'] = strtolower(trim($v)); break; } } } } } $result .= $asn1->encodeDER($rdn, $this->RelativeDistinguishedName); } return $result; case self::DN_HASH: $dn = $this->getDN(self::DN_CANON, $dn); $hash = new Hash('sha1'); $hash = $hash->hash($dn); extract(unpack('Vhash', $hash)); return Hex::encode(pack('N', $hash)); } // Default is to return a string. $start = true; $output = ''; $asn1 = new ASN1(); foreach ($dn['rdnSequence'] as $field) { $prop = $field[0]['type']; $value = $field[0]['value']; $delim = ', '; switch ($prop) { case 'id-at-countryName': $desc = 'C='; break; case 'id-at-stateOrProvinceName': $desc = 'ST='; break; case 'id-at-organizationName': $desc = 'O='; break; case 'id-at-organizationalUnitName': $desc = 'OU='; break; case 'id-at-commonName': $desc = 'CN='; break; case 'id-at-localityName': $desc = 'L='; break; case 'id-at-surname': $desc = 'SN='; break; case 'id-at-uniqueIdentifier': $delim = '/'; $desc = 'x500UniqueIdentifier='; break; default: $delim = '/'; $desc = preg_replace('#.+-([^-]+)$#', '$1', $prop) . '='; } if (!$start) { $output .= $delim; } if (is_array($value)) { foreach ($value as $type => $v) { $type = array_search($type, $asn1->ANYmap, true); if ($type !== false && isset($asn1->stringTypeSize[$type])) { $v = $asn1->convert($v, $type); if ($v !== false) { $value = $v; break; } } } if (is_array($value)) { $value = array_pop($value); // Always strip data type. } } $output .= $desc . $value; $start = false; } return $output; }