* @copyright 2015 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\Common\Formats\Keys; use ParagonIE\ConstantTime\Base64; use ParagonIE\ConstantTime\Hex; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\AES; use phpseclib3\Crypt\DES; use phpseclib3\Crypt\Random; use phpseclib3\Crypt\TripleDES; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\File\ASN1; /** * PKCS1 Formatted Key Handler * * @author Jim Wigginton */ abstract class PKCS1 extends PKCS { /** * Default encryption algorithm * * @var string */ private static $defaultEncryptionAlgorithm = 'AES-128-CBC'; /** * Sets the default encryption algorithm */ public static function setEncryptionAlgorithm(string $algo): void { self::$defaultEncryptionAlgorithm = $algo; } /** * Returns the mode constant corresponding to the mode string * * @return int * @throws \UnexpectedValueException if the block cipher mode is unsupported */ private static function getEncryptionMode(string $mode) { switch ($mode) { case 'CBC': case 'ECB': case 'CFB': case 'OFB': case 'CTR': return $mode; } throw new \UnexpectedValueException('Unsupported block cipher mode of operation'); } /** * Returns a cipher object corresponding to a string * * @return AES|DES|TripleDES * @throws \UnexpectedValueException if the encryption algorithm is unsupported */ private static function getEncryptionObject(string $algo) { $modes = '(CBC|ECB|CFB|OFB|CTR)'; switch (true) { case preg_match("#^AES-(128|192|256)-$modes$#", $algo, $matches): $cipher = new AES(self::getEncryptionMode($matches[2])); $cipher->setKeyLength((int) $matches[1]); return $cipher; case preg_match("#^DES-EDE3-$modes$#", $algo, $matches): return new TripleDES(self::getEncryptionMode($matches[1])); case preg_match("#^DES-$modes$#", $algo, $matches): return new DES(self::getEncryptionMode($matches[1])); default: throw new UnsupportedAlgorithmException($algo . ' is not a supported algorithm'); } } /** * Generate a symmetric key for PKCS#1 keys */ private static function generateSymmetricKey(string $password, string $iv, int $length): string { $symkey = ''; $iv = substr($iv, 0, 8); while (strlen($symkey) < $length) { $symkey .= md5($symkey . $password . $iv, true); } return substr($symkey, 0, $length); } /** * Break a public or private key down into its constituent components * * @param string|array $key * @param string|false $password * @return array|string */ protected static function load($key, $password = '') { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } /* 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 = ASN1::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); } else { if (self::$format != self::MODE_DER) { $decoded = ASN1::extractBER($key); if ($decoded !== false) { $key = $decoded; } elseif (self::$format == self::MODE_PEM) { throw new \UnexpectedValueException('Expected base64-encoded PEM format but was unable to decode base64 text'); } } } return $key; } /** * Wrap a private key appropriately * * @param string|false $password * @param array $options optional */ protected static function wrapPrivateKey(string $key, string $type, $password, array $options = []): string { if (empty($password) || !is_string($password)) { return "-----BEGIN $type PRIVATE KEY-----\r\n" . chunk_split(Base64::encode($key), 64) . "-----END $type PRIVATE KEY-----"; } $encryptionAlgorithm = $options['encryptionAlgorithm'] ?? self::$defaultEncryptionAlgorithm; $cipher = self::getEncryptionObject($encryptionAlgorithm); $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: " . $encryptionAlgorithm . ",$iv\r\n" . "\r\n" . chunk_split(Base64::encode($cipher->encrypt($key)), 64) . "-----END $type PRIVATE KEY-----"; } /** * Wrap a public key appropriately */ protected static function wrapPublicKey(string $key, string $type): string { return "-----BEGIN $type PUBLIC KEY-----\r\n" . chunk_split(Base64::encode($key), 64) . "-----END $type PUBLIC KEY-----"; } }