Skip to content
Merged
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
20 changes: 8 additions & 12 deletions src/Channel.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Channel {
* Constructor
* @param AblyRest $ably Ably API instance
* @param string $name Channel's name
* @param array $options Channel options ['encrypted', 'cipherParams']
* @param ChannelOptions|array|null $options Channel options (for encrypted channels)
* @throws AblyException
*/
public function __construct( AblyRest $ably, $name, $options = array() ) {
Expand Down Expand Up @@ -63,17 +63,17 @@ public function publish() {
if ( count($args) == 1 && is_a( $args[0], 'Ably\Models\Message' ) ) { // single Message
$msg = $args[0];

if ( $this->options->encrypted ) {
$msg->setCipherParams( $this->options->cipherParams );
if ( $this->options->cipher ) {
$msg->setCipherParams( $this->options->cipher );
}

$json = $msg->toJSON();
} else if ( count($args) == 1 && is_array( $args[0] ) ) { // array of Messages
$jsonArray = array();

foreach ( $args[0] as $msg ) {
if ( $this->options->encrypted ) {
$msg->setCipherParams( $this->options->cipherParams );
if ( $this->options->cipher ) {
$msg->setCipherParams( $this->options->cipher );
}

$jsonArray[] = $msg->toJSON();
Expand All @@ -86,8 +86,8 @@ public function publish() {
$msg->data = $args[1];
if ( count($args) == 3 ) $msg->clientId = $args[2];

if ( $this->options->encrypted ) {
$msg->setCipherParams( $this->options->cipherParams );
if ( $this->options->cipher ) {
$msg->setCipherParams( $this->options->cipher );
}

$json = $msg->toJSON();
Expand Down Expand Up @@ -132,7 +132,7 @@ public function getPath() {
* @return CipherParams|null Cipher params if the channel is encrypted
*/
public function getCipherParams() {
return $this->options->encrypted ? $this->options->cipherParams : null;
return $this->options->cipher;
}

/**
Expand All @@ -149,9 +149,5 @@ public function getOptions() {
*/
public function setOptions( $options = array() ) {
$this->options = new ChannelOptions( $options );

if ($this->options->encrypted && !$this->options->cipherParams) {
throw new AblyException( 'Channel created as encrypted, but no cipherParams provided' );
}
}
}
17 changes: 13 additions & 4 deletions src/Models/ChannelOptions.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
<?php
namespace Ably\Models;

use Ably\Utils\Crypto;

/**
* Channel options
*/
class ChannelOptions extends BaseOptions {

/**
* @var boolean indicating if the channel should be encrypted
* @var \Ably\Models\CipherParams|null Parameters of the cipher used on the channel, null if unencrypted
*/
public $encrypted = false;
public $cipher = null;

/**
* @var \Ably\Models\CipherParams parameters of the cipher used on the channel
* Transforms `cipher` from array to CipherParams, if necessary
*/
public $cipherParams = null;
public function __construct( $options = array() ) {
parent::__construct( $options );

if ( is_array( $this->cipher ) ) {
$this->cipher = Crypto::getDefaultParams( $this->cipher );
}
}
}
32 changes: 15 additions & 17 deletions src/Models/CipherParams.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,33 @@
class CipherParams {
/** @var string Key used for encryption, may be a binary string. */
public $key;
/** @var string Algorithm to be used for encryption. The only supported algorithm is currently 'aes'. */
/** @var string Algorithm to be used for encryption. The only officially supported algorithm is currently 'aes'. */
public $algorithm;
/** @var string Key length of the algorithm. Valid values for 'aes' are 128 or 256. */
public $keyLength;
/** @var string Algorithm mode. The only supported mode is currenty 'cbc'. */
/** @var string Algorithm mode. The only supported mode for 'aes' is currently 'cbc'. */
public $mode;
/** @var string Initialization vector for encryption, may be a binary string. */
public $iv;

/**
* Constructor. The encryption algorithm defaults to the only supported algorithm - AES CBC with
* a default key length of 128. A random IV is generated.
* @param string|null $key Encryption key, if not provided a random key is generated.
* @param string|null $algorithm Encryption algorithm, defaults to 'aes'.
* @param Integer|null $keyLength Cipher key length, defaults to 128.
* @param string|null $mode Algorithm mode, defaults to 'cbc'.
*/
public function __construct( $key = null, $algorithm = 'aes', $keyLength = 128, $mode = 'cbc' ) {
$this->key = $key ? $key : openssl_random_pseudo_bytes( 16 );
$this->algorithm = $algorithm;
$this->keyLength = $keyLength;
$this->mode = $mode;
$this->iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( $this->getAlgorithmString() ) );
public function __construct() {
}

/**
* @return string Algorithm string as required by openssl - for instance `aes-128-cbc`
*/
public function getAlgorithmString() {
return $this->algorithm . '-' . $this->keyLength . '-' . $this->mode;
return $this->algorithm
. ($this->keyLength ? '-' . $this->keyLength : '')
. ($this->mode ? '-' . $this->mode : '');
}

public function generateIV() {
$this->iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( $this->getAlgorithmString() ) );
}

public function checkValidAlgorithm() {
$validAlgs = openssl_get_cipher_methods( true );
return in_array( $this->getAlgorithmString(), $validAlgs );
}
}
72 changes: 66 additions & 6 deletions src/Utils/Crypto.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace Ably\Utils;

use Ably\Models\CipherParams;
use Ably\Exceptions\AblyException;

/**
* Provides static methods for encryption/decryption
Expand Down Expand Up @@ -34,18 +35,77 @@ public static function encrypt( $plaintext, $cipherParams ) {
public static function decrypt( $payload, $cipherParams ) {
$raw = defined( 'OPENSSL_RAW_DATA' ) ? OPENSSL_RAW_DATA : true;

$iv = substr( $payload, 0, 16 );
$ciphertext = substr( $payload, 16 );
$ivLength = openssl_cipher_iv_length( $cipherParams->getAlgorithmString() );
$iv = substr( $payload, 0, $ivLength );
$ciphertext = substr( $payload, $ivLength );
return openssl_decrypt( $ciphertext, $cipherParams->getAlgorithmString(), $cipherParams->key, $raw, $iv );
}

/**
* Returns default encryption parameters.
* @param $key string|null Encryption key, if not provided a random key is generated.
* @param $params Array Array containing optional cipher parameters. A `key` must be specified.
* The key may be either a binary string or a base64 encoded string, in which case `'base64Key' => true` must be set.
* `iv` can also be provided as binary or base64 string (`'base64IV' => true`), although you shouldn't need it in most cases.
* @return CipherParams Default encryption parameters.
*/
public static function getDefaultParams( $key = null ) {
return new CipherParams( $key );
public static function getDefaultParams( $params ) {
if ( !isset( $params['key'] ) ) throw new AblyException ( 'No key specified.', 40003, 400 );

$cipherParams = new CipherParams();

if ( isset( $params['base64Key'] ) && $params['base64Key'] ) {
$params['key'] = strtr( $params['key'], '_-', '/+' );
$params['key'] = base64_decode( $params['key'] );
}

$cipherParams->key = $params['key'];
$cipherParams->algorithm = isset( $params['algorithm'] ) ? $params['algorithm'] : 'aes';

if ($cipherParams->algorithm == 'aes') {
$cipherParams->mode = isset( $params['mode'] ) ? $params['mode'] : 'cbc';
$cipherParams->keyLength = isset( $params['keyLength'] ) ? $params['keyLength'] : strlen( $cipherParams->key ) * 8;

if ( !in_array( $cipherParams->keyLength, array( 128, 256 ) ) ) {
throw new AblyException ( 'Unsupported keyLength. Only 128 and 256 bits are supported.', 40003, 400 );
}

if ( $cipherParams->keyLength / 8 != strlen( $cipherParams->key ) ) {
throw new AblyException ( 'keyLength does not match the actual key length.', 40003, 400 );
}

if ( !in_array( $cipherParams->getAlgorithmString(), array( 'aes-128-cbc', 'aes-256-cbc' ) ) ) {
throw new AblyException ( 'Unsupported cipher configuration "' . $cipherParams->getAlgorithmString()
. '". The supported configurations are aes-128-cbc and aes-256-cbc', 40003, 400 );
}
} else {
if ( isset( $params['mode'] ) ) $cipherParams->mode = $params['mode'];
if ( isset( $params['keyLength'] ) ) $cipherParams->keyLength = $params['keyLength'];

if ( !$cipherParams->checkValidAlgorithm() ) {
throw new AblyException( 'The specified algorithm "'.$cipherParams->getAlgorithmString().'"'
. ' is not supported by openssl. See openssl_get_cipher_methods.', 40003, 400 );
}
}

if ( isset( $params['iv'] ) ) {
$cipherParams->iv = $params['iv'];
if ( isset( $params['base64Iv'] ) && $params['base64Iv'] ) {
$cipherParams->iv = strtr( $cipherParams->iv, '_-', '/+' );
$cipherParams->iv = base64_decode( $cipherParams->iv );
}
} else {
$cipherParams->generateIV();
}

return $cipherParams;
}

/**
* Generates a random encryption key.
* @param $keyLength|null The length of the key to be generated in bits, defaults to 256.
*/
public static function generateRandomKey( $keyLength = 256 ) {
return openssl_random_pseudo_bytes( $keyLength / 8 );
}

/**
Expand All @@ -57,6 +117,6 @@ protected static function updateIV( CipherParams $cipherParams ) {
$ivLength = strlen( $cipherParams->iv );

$cipherParams->iv = openssl_encrypt( str_repeat( ' ', $ivLength ), $cipherParams->getAlgorithmString(), $cipherParams->key, $raw, $cipherParams->iv );
$cipherParams->iv = substr( $cipherParams->iv, 0, $ivLength);
$cipherParams->iv = substr( $cipherParams->iv, 0, $ivLength );
}
}
24 changes: 12 additions & 12 deletions tests/ChannelMessagesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Ably\Http;
use Ably\Log;
use Ably\Exceptions\AblyException;
use Ably\Models\CipherParams;
use Ably\Models\Message;
use Ably\Utils\Crypto;

Expand Down Expand Up @@ -109,7 +108,9 @@ public function testPublishMessagesVariousTypesUnencrypted() {
* Publish events with data of various datatypes to an aes-128-cbc encrypted channel
*/
public function testPublishMessagesVariousTypesAES128() {
$options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 128 ));
$options = array( 'cipher' => array(
'key' => Crypto::generateRandomKey( 128 ),
) );
$encrypted1 = self::$ably->channels->get( 'persisted:encrypted1', $options );

$this->assertNotNull( $encrypted1->getCipherParams(), 'Expected channel to be encrypted' );
Expand All @@ -121,7 +122,9 @@ public function testPublishMessagesVariousTypesAES128() {
* Publish events with data of various datatypes to an aes-256-cbc encrypted channel
*/
public function testPublishMessagesVariousTypesAES256() {
$options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 256 ));
$options = array( 'cipher' => array(
'key' => Crypto::generateRandomKey( 256 ),
) );
$encrypted2 = self::$ably->channels->get( 'persisted:encrypted2', $options );

$this->assertNotNull( $encrypted2->getCipherParams(), 'Expected channel to be encrypted' );
Expand Down Expand Up @@ -273,7 +276,7 @@ public function testEncryptedMessageUnencryptedHistory() {

$payload = 'This is a test message';

$options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 128 ));
$options = array( 'cipher' => array( 'key' => Crypto::generateRandomKey( 128 ) ) );
$encrypted1 = $ably->channel( 'persisted:mismatch1', $options );
$encrypted1->publish( 'test', $payload );

Expand All @@ -296,7 +299,7 @@ public function testUnencryptedMessageEncryptedHistory() {
$encrypted = self::$ably->channel( 'persisted:mismatch2' );
$encrypted->publish( 'test', $payload );

$options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 128 ));
$options = array( 'cipher' => array( 'key' => Crypto::generateRandomKey( 128 ) ) );
$unencrypted = self::$ably->channel( 'persisted:mismatch2', $options );
$messages = $unencrypted->history();
$this->assertNotNull( $messages, 'Expected non-null messages' );
Expand All @@ -321,11 +324,11 @@ public function testEncryptionKeyMismatch() {

$payload = 'This is a test message';

$options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 128 ));
$options = array( 'cipher' => array( 'key' => 'fake key 1xxxxxx' ) );
$encrypted1 = $ably->channel( 'persisted:mismatch3', $options );
$encrypted1->publish( 'test', $payload );

$options2 = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'DIFFERENT PASSWORD', 'aes', 128 ));
$options2 = array( 'cipher' => array( 'key' => 'fake key 2xxxxxx' ) );
$encrypted2 = $ably->channel( 'persisted:mismatch3', $options2 );
$messages = $encrypted2->history();
$msg = $messages->items[0];
Expand All @@ -351,10 +354,7 @@ public function testChannelCaching() {

$this->assertNull( $channel3->getCipherParams(), 'Expected the channel to not have CipherParams' );

self::$ably->channel( 'cache_test', array(
'encrypted' => true,
'cipherParams' => new CipherParams( 'password', 'aes', 128 )
) );
self::$ably->channel( 'cache_test', array( 'cipher' => array( 'key' => Crypto::generateRandomKey( 128 ) ) ) );

$this->assertNotNull( $channel3->getCipherParams(), 'Expected the channel to have CipherParams even when specified for a new instance' );
}
Expand All @@ -379,7 +379,7 @@ public function testMessageEncodings() {
$msg->data = hex2bin( '00102030405060708090a0b0c0d0e0f0ff' );
$this->assertEquals( 'base64', $this->getMessageEncoding( $msg ), 'Expected empty message encoding' );

$msg->setCipherParams( Crypto::getDefaultParams( 'password' ) );
$msg->setCipherParams( Crypto::getDefaultParams( array( 'key' => Crypto::generateRandomKey( 128 ) ) ) );

$msg->data = 'This is a UTF-8 string message payload. äôč ビール';
$this->assertEquals( 'utf-8/cipher+aes-128-cbc/base64', $this->getMessageEncoding( $msg ), 'Expected empty message encoding' );
Expand Down
Loading