/** * 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; }
/** * 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, ASN1::encodeLength(strlen($modulus)), $modulus), 'publicExponent' => pack('Ca*a*', self::ASN1_INTEGER, ASN1::encodeLength(strlen($publicExponent)), $publicExponent)); $RSAPublicKey = pack('Ca*a*a*', self::ASN1_SEQUENCE, ASN1::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), $components['modulus'], $components['publicExponent']); $RSAPublicKey = "-----BEGIN RSA PUBLIC KEY-----\r\n" . chunk_split(Base64::encode($RSAPublicKey), 64) . '-----END RSA PUBLIC KEY-----'; return $RSAPublicKey; }
/** * ASN.1 Encode (Helper function) * * @param string $source * @param string $mapping * @param int $idx * @return string * @throws \RuntimeException if the input has an error in it * @access private */ function _encode_der($source, $mapping, $idx = null, $special = array()) { if ($source instanceof Element) { return $source->element; } // do not encode (implicitly optional) fields with value set to default if (isset($mapping['default']) && $source === $mapping['default']) { return ''; } if (isset($idx)) { if (isset($special[$idx])) { $source = call_user_func($special[$idx], $source); } $this->location[] = $idx; } $tag = $mapping['type']; switch ($tag) { case self::TYPE_SET: // Children order is not important, thus process in sequence. // Children order is not important, thus process in sequence. case self::TYPE_SEQUENCE: $tag |= 0x20; // set the constructed bit // ignore the min and max if (isset($mapping['min']) && isset($mapping['max'])) { $value = array(); $child = $mapping['children']; foreach ($source as $content) { $temp = $this->_encode_der($content, $child, null, $special); if ($temp === false) { return false; } $value[] = $temp; } /* "The encodings of the component values of a set-of value shall appear in ascending order, the encodings being compared as octet strings with the shorter components being padded at their trailing end with 0-octets. NOTE - The padding octets are for comparison purposes only and do not appear in the encodings." -- sec 11.6 of http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf */ if ($mapping['type'] == self::TYPE_SET) { sort($value); } $value = implode($value, ''); break; } $value = ''; foreach ($mapping['children'] as $key => $child) { if (!array_key_exists($key, $source)) { if (!isset($child['optional'])) { return false; } continue; } $temp = $this->_encode_der($source[$key], $child, $key, $special); if ($temp === false) { return false; } // An empty child encoding means it has been optimized out. // Else we should have at least one tag byte. if ($temp === '') { continue; } // if isset($child['constant']) is true then isset($child['optional']) should be true as well if (isset($child['constant'])) { /* From X.680-0207.pdf#page=58 (30.6): "The tagging construction specifies explicit tagging if any of the following holds: ... c) the "Tag Type" alternative is used and the value of "TagDefault" for the module is IMPLICIT TAGS or AUTOMATIC TAGS, but the type defined by "Type" is an untagged choice type, an untagged open type, or an untagged "DummyReference" (see ITU-T Rec. X.683 | ISO/IEC 8824-4, 8.3)." */ if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { $subtag = chr(self::CLASS_CONTEXT_SPECIFIC << 6 | 0x20 | $child['constant']); $temp = $subtag . Functions::encodeLength(strlen($temp)) . $temp; } else { $subtag = chr(self::CLASS_CONTEXT_SPECIFIC << 6 | ord($temp[0]) & 0x20 | $child['constant']); $temp = $subtag . substr($temp, 1); } } $value .= $temp; } break; case self::TYPE_CHOICE: $temp = false; foreach ($mapping['children'] as $key => $child) { if (!isset($source[$key])) { continue; } $temp = $this->_encode_der($source[$key], $child, $key, $special); if ($temp === false) { return false; } // An empty child encoding means it has been optimized out. // Else we should have at least one tag byte. if ($temp === '') { continue; } $tag = ord($temp[0]); // if isset($child['constant']) is true then isset($child['optional']) should be true as well if (isset($child['constant'])) { if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { $subtag = chr(self::CLASS_CONTEXT_SPECIFIC << 6 | 0x20 | $child['constant']); $temp = $subtag . Functions::encodeLength(strlen($temp)) . $temp; } else { $subtag = chr(self::CLASS_CONTEXT_SPECIFIC << 6 | ord($temp[0]) & 0x20 | $child['constant']); $temp = $subtag . substr($temp, 1); } } } if (isset($idx)) { array_pop($this->location); } if ($temp && isset($mapping['cast'])) { $temp[0] = chr($mapping['class'] << 6 | $tag & 0x20 | $mapping['cast']); } return $temp; case self::TYPE_INTEGER: case self::TYPE_ENUMERATED: if (!isset($mapping['mapping'])) { if (is_numeric($source)) { $source = new BigInteger($source); } $value = $source->toBytes(true); } else { $value = array_search($source, $mapping['mapping']); if ($value === false) { return false; } $value = new BigInteger($value); $value = $value->toBytes(true); } if (!strlen($value)) { $value = chr(0); } break; case self::TYPE_UTC_TIME: case self::TYPE_GENERALIZED_TIME: $format = $mapping['type'] == self::TYPE_UTC_TIME ? 'y' : 'Y'; $format .= 'mdHis'; $value = @gmdate($format, strtotime($source)) . 'Z'; break; case self::TYPE_BIT_STRING: if (isset($mapping['mapping'])) { $bits = array_fill(0, count($mapping['mapping']), 0); $size = 0; for ($i = 0; $i < count($mapping['mapping']); $i++) { if (in_array($mapping['mapping'][$i], $source)) { $bits[$i] = 1; $size = $i; } } if (isset($mapping['min']) && $mapping['min'] >= 1 && $size < $mapping['min']) { $size = $mapping['min'] - 1; } $offset = 8 - ($size + 1 & 7); $offset = $offset !== 8 ? $offset : 0; $value = chr($offset); for ($i = $size + 1; $i < count($mapping['mapping']); $i++) { unset($bits[$i]); } $bits = implode('', array_pad($bits, $size + $offset + 1, 0)); $bytes = explode(' ', rtrim(chunk_split($bits, 8, ' '))); foreach ($bytes as $byte) { $value .= chr(bindec($byte)); } break; } case self::TYPE_OCTET_STRING: /* The initial octet shall encode, as an unsigned binary integer with 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#page=16 */ $value = Base64::decode($source); break; case self::TYPE_OBJECT_IDENTIFIER: $oid = preg_match('#(?:\\d+\\.)+#', $source) ? $source : array_search($source, $this->oids); if ($oid === false) { throw new \RuntimeException('Invalid OID'); return false; } $value = ''; $parts = explode('.', $oid); $value = chr(40 * $parts[0] + $parts[1]); for ($i = 2; $i < count($parts); $i++) { $temp = ''; if (!$parts[$i]) { $temp = ""; } else { while ($parts[$i]) { $temp = chr(0x80 | $parts[$i] & 0x7f) . $temp; $parts[$i] >>= 7; } $temp[strlen($temp) - 1] = $temp[strlen($temp) - 1] & chr(0x7f); } $value .= $temp; } break; case self::TYPE_ANY: $loc = $this->location; if (isset($idx)) { array_pop($this->location); } switch (true) { case !isset($source): return $this->_encode_der(null, array('type' => self::TYPE_NULL) + $mapping, null, $special); case is_int($source): case $source instanceof BigInteger: return $this->_encode_der($source, array('type' => self::TYPE_INTEGER) + $mapping, null, $special); case is_float($source): return $this->_encode_der($source, array('type' => self::TYPE_REAL) + $mapping, null, $special); case is_bool($source): return $this->_encode_der($source, array('type' => self::TYPE_BOOLEAN) + $mapping, null, $special); case is_array($source) && count($source) == 1: $typename = implode('', array_keys($source)); $outtype = array_search($typename, $this->ANYmap, true); if ($outtype !== false) { return $this->_encode_der($source[$typename], array('type' => $outtype) + $mapping, null, $special); } } $filters = $this->filters; foreach ($loc as $part) { if (!isset($filters[$part])) { $filters = false; break; } $filters = $filters[$part]; } if ($filters === false) { throw new \RuntimeException('No filters defined for ' . implode('/', $loc)); return false; } return $this->_encode_der($source, $filters + $mapping, null, $special); case self::TYPE_NULL: $value = ''; break; case self::TYPE_NUMERIC_STRING: case self::TYPE_TELETEX_STRING: case self::TYPE_PRINTABLE_STRING: case self::TYPE_UNIVERSAL_STRING: case self::TYPE_UTF8_STRING: case self::TYPE_BMP_STRING: case self::TYPE_IA5_STRING: case self::TYPE_VISIBLE_STRING: case self::TYPE_VIDEOTEX_STRING: case self::TYPE_GRAPHIC_STRING: case self::TYPE_GENERAL_STRING: $value = $source; break; case self::TYPE_BOOLEAN: $value = $source ? "ÿ" : ""; break; default: throw new \RuntimeException('Mapping provides no type definition for ' . implode('/', $this->location)); return false; } if (isset($idx)) { array_pop($this->location); } if (isset($mapping['cast'])) { if (isset($mapping['explicit']) || $mapping['type'] == self::TYPE_CHOICE) { $value = chr($tag) . Functions::encodeLength(strlen($value)) . $value; $tag = $mapping['class'] << 6 | 0x20 | $mapping['cast']; } else { $tag = $mapping['class'] << 6 | ord($temp[0]) & 0x20 | $mapping['cast']; } } return chr($tag) . Functions::encodeLength(strlen($value)) . $value; }
/** * 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, ASN1::encodeLength(strlen($modulus)), $modulus), 'publicExponent' => pack('Ca*a*', self::ASN1_INTEGER, ASN1::encodeLength(strlen($publicExponent)), $publicExponent)); $RSAPublicKey = pack('Ca*a*a*', self::ASN1_SEQUENCE, ASN1::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), $components['modulus'], $components['publicExponent']); // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. $rsaOID = "0\r\t*†H†÷\r"; // hex version of MA0GCSqGSIb3DQEBAQUA $RSAPublicKey = chr(0) . $RSAPublicKey; $RSAPublicKey = chr(3) . ASN1::encodeLength(strlen($RSAPublicKey)) . $RSAPublicKey; $RSAPublicKey = pack('Ca*a*', self::ASN1_SEQUENCE, ASN1::encodeLength(strlen($rsaOID . $RSAPublicKey)), $rsaOID . $RSAPublicKey); $RSAPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . chunk_split(Base64::encode($RSAPublicKey), 64) . '-----END PUBLIC KEY-----'; return $RSAPublicKey; }