Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions src/Codeaken/SshKey/Exception/InvalidKeyTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php
namespace Codeaken\SshKey\Exception;

class InvalidKeyTypeException extends \InvalidArgumentException
{
}
38 changes: 28 additions & 10 deletions src/Codeaken/SshKey/SshKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,42 @@ abstract class SshKey
const FORMAT_OPENSSH = 'openssh';
const FORMAT_PKCS1 = 'pkcs1';
const FORMAT_PKCS8 = 'pkcs8';
const FORMAT_PUTTY = 'putty';

protected $formatToConstant = [
self::FORMAT_OPENSSH => 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();
Expand All @@ -51,4 +64,9 @@ protected static function readFile($filename)

return $fileData;
}

protected function normalizeLineEndings($data)
{
return str_replace("\r\n", "\n", $data);
}
}
27 changes: 23 additions & 4 deletions src/Codeaken/SshKey/SshKeyPair.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
<?php
namespace Codeaken\SshKey;

use Codeaken\SshKey\Exception\InvalidKeyTypeException;
use phpseclib\Crypt\RSA;

class SshKeyPair
{
/** @var SshPublicKey */
private $publicKey;

/** @var SshPrivateKey */
private $privateKey;

public function __construct($publicKey, $privateKey)
public function __construct(SshPrivateKey $privateKey, SshPublicKey $publicKey = null)
{
$this->publicKey = $publicKey;
$this->privateKey = $privateKey;
$this->publicKey = $publicKey;

if (!$publicKey) {
$this->publicKey = $privateKey->getPublicKey();
}
}

public function getPublicKey()
Expand All @@ -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;

Expand All @@ -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);
}
}
7 changes: 7 additions & 0 deletions src/Codeaken/SshKey/SshPrivateKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 18 additions & 2 deletions src/Codeaken/SshKey/SshPublicKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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());
Expand Down
90 changes: 90 additions & 0 deletions tests/SshKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions tests/keys/id_nopass_generated_comment_rsa.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC24I0GkZK/SaOpuURrQi9NLxDDUvku5caBuoYKljk+wz+jhnMqcrHll6RBXAcG93tVZz6USEqAoFntaEY74fTvwVBw05Sl1uabCYZXShbsvEU6+yXbRHxfw1VCAxr8a4qJWtU/0ADCWYXQa3AKv/cjQxiqmx7b6ONVXt06ccw3Owqh+zAB7ez0X5HOYzviDMQZV9UA2p1ojyfLHayddEOhn7cARdXBYPXwp1gaTwnzJjhAsWYxXggbNPseM+EqGuXKwp1hmI+8dZiKCGCv7zjH+lZy4aIVBzZ3h0uvDXwdrGjxYWjMZD+rF0XjlRu86aQvYIo2O9NEpeBVbHczeNB9 phpseclib-generated-key
2 changes: 1 addition & 1 deletion tests/keys/id_nopass_rsa
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ mKrLNpmIaabGelnHSITBi9uhrbRUYRLOSyKu6CVRgY7kt1VmI5aic2dyxMHgaU/G
gJ8CB10CgYEAsHH2s6oBKJAgQT3Iso5umW8sEPgrGLC9EiC8Ut6NMxSrq5OexlF7
YYFywVDfC2g6RpS2GvzFXNujPxzv3rJtiX+Axnx+b9L2KIUxNZ2VKjw5dHm+4GRf
gc8jGNf5sYRwlrBnQZD8FnSceEZJ7vZ1QnXStQs8U/y015E1Xbc/sVM=
-----END RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----
26 changes: 26 additions & 0 deletions tests/keys/id_nopass_rsa.ppk
Original file line number Diff line number Diff line change
@@ -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