diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3c91000 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [2.0.0] - 2017-09-21 + +### Added +- This CHANGELOG file +- `SshKeyPair`: added typehints +- `SshPrivateKey`: added `getPublicKey` Method +- `SshPublicKey`: added `fromPrivateKey()` Method +- `SshPublicKey`: added `setComment()` Method +- `SshKey`: added `getSize()` +- added Putty Key Format +- added linux line ending +### Changed +- `SshKey`: `getKeyData()` has now `openssh` as default format +- `SshKey`: `getKeyData()` return normalized line endings (linux) +- [BC Break] `SshKeyPair`: changed constructor signature, switched public with private to allow to `null` as public key +- `SshKeyPair`: increased default bit size to 4096 +- `SshPublicKey`: `constructor` to allow to set the comment & added typehints diff --git a/src/Codeaken/SshKey/Exception/InvalidKeyTypeException.php b/src/Codeaken/SshKey/Exception/InvalidKeyTypeException.php new file mode 100644 index 0000000..ee2ce5d --- /dev/null +++ b/src/Codeaken/SshKey/Exception/InvalidKeyTypeException.php @@ -0,0 +1,6 @@ + RSA::PUBLIC_FORMAT_OPENSSH, + self::FORMAT_PKCS1 => RSA::PUBLIC_FORMAT_PKCS1, + self::FORMAT_PKCS8 => RSA::PUBLIC_FORMAT_PKCS8, + self::FORMAT_PUTTY => RSA::PRIVATE_FORMAT_PUTTY, + ]; public function __construct() { $this->key = new RSA(); } - public function getKeyData($format) + public function getKeyData($format = self::FORMAT_OPENSSH) { - $formatToConstant = [ - self::FORMAT_OPENSSH => RSA::PUBLIC_FORMAT_OPENSSH, - self::FORMAT_PKCS1 => RSA::PUBLIC_FORMAT_PKCS1, - self::FORMAT_PKCS8 => RSA::PUBLIC_FORMAT_PKCS8, - ]; - - if ( ! isset($formatToConstant[$format])) { + if ( ! isset($this->formatToConstant[$format])) { throw new \DomainException("Invalid format: $format"); } if ('private' == $this->getKeyType()) { - return $this->key->getPrivateKey($formatToConstant[$format]); + $keyData = $this->key->getPrivateKey($this->formatToConstant[$format]); } else { - return $this->key->getPublicKey($formatToConstant[$format]); + $keyData = $this->key->getPublicKey($this->formatToConstant[$format]); + } + + if ($format != self::FORMAT_PUTTY) { + $keyData = $this->normalizeLineEndings($keyData); } + + return $keyData; + } + + public function getSize() + { + return $this->key->getSize(); } abstract protected function getKeyType(); @@ -51,4 +64,9 @@ protected static function readFile($filename) return $fileData; } + + protected function normalizeLineEndings($data) + { + return str_replace("\r\n", "\n", $data); + } } diff --git a/src/Codeaken/SshKey/SshKeyPair.php b/src/Codeaken/SshKey/SshKeyPair.php index 6454f9b..32cccf5 100644 --- a/src/Codeaken/SshKey/SshKeyPair.php +++ b/src/Codeaken/SshKey/SshKeyPair.php @@ -1,17 +1,25 @@ publicKey = $publicKey; $this->privateKey = $privateKey; + $this->publicKey = $publicKey; + + if (!$publicKey) { + $this->publicKey = $privateKey->getPublicKey(); + } } public function getPublicKey() @@ -24,7 +32,18 @@ public function getPrivateKey() return $this->privateKey; } - public static function generate($bits = 2048, $password = '') + public static function fromFile($filename, $password = '') + { + $privateKey = SshPrivateKey::fromFile($filename, $password); + + if ('' == $privateKey->getKeyData()) { + throw new InvalidKeyTypeException('You have to create a KeyPair from a Private Key'); + } + + return new self($privateKey); + } + + public static function generate(int $bits = 4096, string $password = '') { $bits = (int)$bits; @@ -39,6 +58,6 @@ public static function generate($bits = 2048, $password = '') $publicKey = new SshPublicKey($keys['publickey']); $privateKey = new SshPrivateKey($keys['privatekey'], $password); - return new SshKeyPair($publicKey, $privateKey); + return new self($privateKey, $publicKey); } } diff --git a/src/Codeaken/SshKey/SshPrivateKey.php b/src/Codeaken/SshKey/SshPrivateKey.php index 159d9c9..be243a6 100644 --- a/src/Codeaken/SshKey/SshPrivateKey.php +++ b/src/Codeaken/SshKey/SshPrivateKey.php @@ -20,6 +20,13 @@ public function __construct($keyData, $password = '') } } + public function getPublicKey($format = self::FORMAT_OPENSSH) + { + $keyData = $this->key->getPublicKey($this->formatToConstant[$format]); + + return new SshPublicKey($keyData); + } + public static function fromFile($filename, $password = '') { return new SshPrivateKey(SshKey::readFile($filename), $password); diff --git a/src/Codeaken/SshKey/SshPublicKey.php b/src/Codeaken/SshKey/SshPublicKey.php index 45b7fba..5d5de06 100644 --- a/src/Codeaken/SshKey/SshPublicKey.php +++ b/src/Codeaken/SshKey/SshPublicKey.php @@ -3,18 +3,27 @@ class SshPublicKey extends SshKey { - public function __construct($keyData) + public function __construct(string $keyData, string $comment = null) { parent::__construct(); if ( ! $this->key->loadKey($keyData)) { throw new Exception\LoadKeyException(); } + + if ($comment) { + $this->setComment($comment); + } + } + + public static function fromPrivateKey(SshPrivateKey $privateKey, $format = self::FORMAT_OPENSSH) + { + return $privateKey->getPublicKey($format); } public static function fromFile($filename) { - return new SshPublicKey(SshKey::readFile($filename)); + return new self(SshKey::readFile($filename)); } public function getFingerprint() @@ -24,6 +33,13 @@ public function getFingerprint() return implode(':', str_split(md5(base64_decode($keyParts[1])), 2)); } + public function setComment($comment) + { + $this->key->setComment($comment); + + return $this; + } + public function getComment() { return trim($this->key->getComment()); diff --git a/tests/SshKeyTest.php b/tests/SshKeyTest.php index 7421e0b..545beec 100644 --- a/tests/SshKeyTest.php +++ b/tests/SshKeyTest.php @@ -164,4 +164,94 @@ public function testCreateKeyPairWithPrivateKeyPassword() $this->assertTrue($keyPair->getPrivateKey()->hasPassword()); $this->assertEquals('abc123', $keyPair->getPrivateKey()->getPassword()); } + + public function testGetPublicKeyFromPrivateKey() + { + $expectedPublicKeyContents = file_get_contents("{$this->keysDir}/id_nopass_generated_comment_rsa.pub"); + + $privateKeyContents = file_get_contents("{$this->keysDir}/id_nopass_rsa"); + $privateKey = new SshPrivateKey($privateKeyContents); + $publicKey = $privateKey->getPublicKey(); + + $this->assertEquals($expectedPublicKeyContents, $publicKey->getKeyData()); + } + + public function testSetCommentOnPublicKey() + { + $key = SshPublicKey::fromFile("{$this->keysDir}/id_nopass_generated_comment_rsa.pub"); + + $key->setComment('new comment'); + $this->assertEquals('new comment', $key->getComment()); + } + + public function testSetCommentInConstructor() + { + $keyData = file_get_contents("{$this->keysDir}/id_nopass_generated_comment_rsa.pub"); + + $key = new SshPublicKey($keyData, 'new comment'); + + $this->assertEquals('new comment', $key->getComment()); + + } + + public function testCreatePublicKeyFromPrivateKey() + { + $expectedPublicKeyContents = file_get_contents("{$this->keysDir}/id_nopass_generated_comment_rsa.pub"); + + $privateKeyContents = file_get_contents("{$this->keysDir}/id_nopass_rsa"); + $privateKey = new SshPrivateKey($privateKeyContents); + + $publicKey = SshPublicKey::fromPrivateKey($privateKey); + + $this->assertEquals($expectedPublicKeyContents, $publicKey->getKeyData()); + } + + public function testPrivateKeyGetSize() + { + $keyPair = SshKeyPair::generate(2048); + + $actualSize = $keyPair->getPrivateKey()->getSize(); + + $this->assertEquals(2048, $actualSize); + } + + public function testPublicKeyGetSize() + { + $keyPair = SshKeyPair::generate(2048); + + $actualSize = $keyPair->getPublicKey()->getSize(); + + $this->assertEquals(2048, $actualSize); + } + + public function testKeyPairFromPrivateKeyFile() + { + $keyPair = SshKeyPair::fromFile("{$this->keysDir}/id_nopass_rsa"); + + $this->assertInstanceOf('Codeaken\SshKey\SshKeyPair', $keyPair); + + $expectedPrivateKeyData = file_get_contents("{$this->keysDir}/id_nopass_rsa"); + $actualPrivateKeyData = $keyPair->getPrivateKey()->getKeyData(); + $this->assertEquals($expectedPrivateKeyData, $actualPrivateKeyData); + + $expectedPublicKeyData = file_get_contents("{$this->keysDir}/id_nopass_generated_comment_rsa.pub"); + $actualPublicKeyData = $keyPair->getPublicKey()->getKeyData(); + $this->assertEquals($expectedPublicKeyData, $actualPublicKeyData); + } + + public function testKeyPairFromPublicKeyFile() + { + $this->setExpectedException('Codeaken\SshKey\Exception\InvalidKeyTypeException'); + SshKeyPair::fromFile("{$this->keysDir}/id_nopass_rsa.pub"); + } + + public function testGetPuttyKeyFormat() + { + $key = SshPrivateKey::fromFile("{$this->keysDir}/id_nopass_rsa"); + + $actualKeyData = $key->getKeyData(SshKey::FORMAT_PUTTY); + $expectedKeyData = file_get_contents("{$this->keysDir}/id_nopass_rsa.ppk"); + + $this->assertEquals($expectedKeyData, $actualKeyData); + } } diff --git a/tests/keys/id_nopass_generated_comment_rsa.pub b/tests/keys/id_nopass_generated_comment_rsa.pub new file mode 100644 index 0000000..5357cf3 --- /dev/null +++ b/tests/keys/id_nopass_generated_comment_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC24I0GkZK/SaOpuURrQi9NLxDDUvku5caBuoYKljk+wz+jhnMqcrHll6RBXAcG93tVZz6USEqAoFntaEY74fTvwVBw05Sl1uabCYZXShbsvEU6+yXbRHxfw1VCAxr8a4qJWtU/0ADCWYXQa3AKv/cjQxiqmx7b6ONVXt06ccw3Owqh+zAB7ez0X5HOYzviDMQZV9UA2p1ojyfLHayddEOhn7cARdXBYPXwp1gaTwnzJjhAsWYxXggbNPseM+EqGuXKwp1hmI+8dZiKCGCv7zjH+lZy4aIVBzZ3h0uvDXwdrGjxYWjMZD+rF0XjlRu86aQvYIo2O9NEpeBVbHczeNB9 phpseclib-generated-key \ No newline at end of file diff --git a/tests/keys/id_nopass_rsa b/tests/keys/id_nopass_rsa index 8eb44c6..96b5852 100644 --- a/tests/keys/id_nopass_rsa +++ b/tests/keys/id_nopass_rsa @@ -24,4 +24,4 @@ mKrLNpmIaabGelnHSITBi9uhrbRUYRLOSyKu6CVRgY7kt1VmI5aic2dyxMHgaU/G gJ8CB10CgYEAsHH2s6oBKJAgQT3Iso5umW8sEPgrGLC9EiC8Ut6NMxSrq5OexlF7 YYFywVDfC2g6RpS2GvzFXNujPxzv3rJtiX+Axnx+b9L2KIUxNZ2VKjw5dHm+4GRf gc8jGNf5sYRwlrBnQZD8FnSceEZJ7vZ1QnXStQs8U/y015E1Xbc/sVM= ------END RSA PRIVATE KEY----- +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/keys/id_nopass_rsa.ppk b/tests/keys/id_nopass_rsa.ppk new file mode 100644 index 0000000..d22d977 --- /dev/null +++ b/tests/keys/id_nopass_rsa.ppk @@ -0,0 +1,26 @@ +PuTTY-User-Key-File-2: ssh-rsa +Encryption: none +Comment: phpseclib-generated-key +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQC24I0GkZK/SaOpuURrQi9NLxDDUvku5caB +uoYKljk+wz+jhnMqcrHll6RBXAcG93tVZz6USEqAoFntaEY74fTvwVBw05Sl1uab +CYZXShbsvEU6+yXbRHxfw1VCAxr8a4qJWtU/0ADCWYXQa3AKv/cjQxiqmx7b6ONV +Xt06ccw3Owqh+zAB7ez0X5HOYzviDMQZV9UA2p1ojyfLHayddEOhn7cARdXBYPXw +p1gaTwnzJjhAsWYxXggbNPseM+EqGuXKwp1hmI+8dZiKCGCv7zjH+lZy4aIVBzZ3 +h0uvDXwdrGjxYWjMZD+rF0XjlRu86aQvYIo2O9NEpeBVbHczeNB9 +Private-Lines: 14 +AAABAQCboZvCtE5bhjLG9Mj6MrgIin5Mi6dONvNpYbBDADc1Z3oYEwqdXEBy8Esz +6Dp+vkxykMub662jq1L8jFoBCjmldDGd4yHExI8577ApRv8ddte/6w37fVwPLy+2 +XugvWuHqJKgIh16uBvuvNE+EhnuuwaITRrHLWnVlKLdCzqeJc8A/MT9gcOikCO/Q +jKuU3kx75nxOcnfoLKyoZexyMMsoTBkpKgjxgT1G3m8DQtG2o28D5Wc1YAiHaXVl +taTNsylhvSEdCd/nDJbC0fR08TmfXuKiksM9JlzxuMl1jbTq66LFLcYew5KQ35r0 +agB6h8mBGJk9SSZqSMVUTMAhaK+tAAAAgQDZtvTj+xSIJ2AnPmNtTBR4qylP2Kku +Ii2/wGu3NQm+ZIII6F764jehygbVJ796OiBTd7V+qcxmf+OSZl7Cfxj7FVC3ROaU +F4Se9e9tWh13vd7+l4plq88K7lWPPaHEkz0/hpYUSK3rB2RQxsVqwWOwYXallnqh +KC2G3cXDsVLGpwAAAIEA1wlI0CqFibyunS3d3h7gnh1HJQ0ZYsckXnBoW664dlWg +I/gF6SA6m0qVAmoGwWvqEziQ2b37jSWaHuQmKxPvkx4awyiAOQJCUNelTadgZV6B +i3ztME2LF9uo+tMcFj1OKlq42D3OspYTNhbHumYZGXraFolfMytlY8ttmWUbuDsA +AACBALBx9rOqASiQIEE9yLKObplvLBD4KxiwvRIgvFLejTMUq6uTnsZRe2GBcsFQ +3wtoOkaUthr8xVzboz8c796ybYl/gMZ8fm/S9iiFMTWdlSo8OXR5vuBkX4HPIxjX ++bGEcJawZ0GQ/BZ0nHhGSe72dUJ10rULPFP8tNeRNV23P7FT +Private-MAC: 3b7241f634ded38e048f22e6afce18a9e9c4e0ef