Skip to content

Commit

Permalink
New Feature: Support AES-GCM (#5)
Browse files Browse the repository at this point in the history
* feat: 🎸 Implement encryption with HMAC signature verification

* fix: 🐛 Wrong namespace in tests

* test: 💍 Test new feature

* Revert "feat: 🎸 Implement encryption with HMAC signature verification"

This reverts commit e615000.

* Revert "test: 💍 Test new feature"

This reverts commit 01dd73a.

* feat: 🎸 Support AES-GCM Method

* fix: 🐛 Move core logic from decrypt() to mustDecrypt()

For providing more details

* test: 💍 Add detailed tests

* fix: 🐛 Don't pass $tag argument withoug GCM
  • Loading branch information
mpyw authored Jun 29, 2021
1 parent bd175dc commit c33cd8f
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 20 deletions.
62 changes: 49 additions & 13 deletions src/Cryptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ class Cryptor implements CryptorInterface
/**
* @var int
*/
protected $length;
protected $ivLength;

/**
* @var null|int
*/
protected $tagLength;

/**
* Constructor.
Expand All @@ -34,7 +39,13 @@ public function __construct(string $method = self::DEFAULT_CIPHER_METHOD)
}

$this->method = $method;
$this->length = openssl_cipher_iv_length($method);
$this->ivLength = openssl_cipher_iv_length($method);

set_error_handler(function () {});
openssl_encrypt('', $this->method, '', OPENSSL_RAW_DATA, $this->random($this->ivLength), $tag);
restore_error_handler();

$this->tagLength = $tag === null ? null : strlen($tag);
}

/**
Expand All @@ -46,16 +57,19 @@ public function __construct(string $method = self::DEFAULT_CIPHER_METHOD)
*/
public function encrypt(string $data, string $password): string
{
$iv = $this->random($this->length);
$encrypted = openssl_encrypt($data, $this->method, $password, OPENSSL_RAW_DATA, $iv);
$iv = $this->random($this->ivLength);
$tag = null;
$encrypted = $this->tagLength
? openssl_encrypt($data, $this->method, $password, OPENSSL_RAW_DATA, $iv, $tag)
: openssl_encrypt($data, $this->method, $password, OPENSSL_RAW_DATA, $iv);

if ($encrypted === false) {
// @codeCoverageIgnoreStart
throw new EncryptionFailedException(openssl_error_string());
// @codeCoverageIgnoreEnd
}

return "$iv$encrypted";
return "$iv$encrypted$tag";
}

/**
Expand All @@ -67,14 +81,11 @@ public function encrypt(string $data, string $password): string
*/
public function decrypt(string $data, string $password)
{
$iv = substr($data, 0, $this->length);

if (strlen($iv) !== $this->length) {
try {
return $this->mustDecrypt($data, $password);
} catch (DecryptionFailedException $e) {
return false;
}

$data = substr($data, $this->length);
return openssl_decrypt($data, $this->method, $password, OPENSSL_RAW_DATA, $iv);
}

/**
Expand All @@ -87,10 +98,35 @@ public function decrypt(string $data, string $password)
*/
public function mustDecrypt(string $data, string $password): string
{
$decrypted = $this->decrypt($data, $password);
$originalData = $data;

$iv = substr($data, 0, $this->ivLength);
if (strlen($iv) !== $this->ivLength) {
throw new DecryptionFailedException('invalid iv length.', $originalData);
}
$data = substr($data, $this->ivLength);

$tag = null;
if ($this->tagLength !== null) {
$tag = substr($data, -$this->tagLength);
if (strlen($tag) !== $this->tagLength) {
throw new DecryptionFailedException('invalid tag length.', $originalData);
}
$data = substr($data, 0, -$this->tagLength);
}

$decrypted = $this->tagLength
? openssl_decrypt($data, $this->method, $password, OPENSSL_RAW_DATA, $iv, $tag)
: openssl_decrypt($data, $this->method, $password, OPENSSL_RAW_DATA, $iv);

if ($decrypted === false) {
throw new DecryptionFailedException(openssl_error_string(), $data);
$error = openssl_error_string();
if ($error === false) {
$error = $this->tagLength
? 'invalid tag content.'
: 'unknown error.';
}
throw new DecryptionFailedException($error, $originalData);
}

return $decrypted;
Expand Down
57 changes: 54 additions & 3 deletions tests/CryptorTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Tests;
namespace Mpyw\EasyCrypt\Tests;

use Mpyw\EasyCrypt\Cryptor;
use Mpyw\EasyCrypt\Exceptions\DecryptionFailedException;
Expand Down Expand Up @@ -32,6 +32,20 @@ public function testAes256(): void
$this->assertFalse($cryptor->decrypt($encryptedA, 'passward'));
}

public function testAes256Gcm(): void
{
$cryptor = new Cryptor('aes-256-gcm');

$encryptedA = $cryptor->encrypt('data', 'password');
$encryptedB = $cryptor->encrypt('data', 'password');

$this->assertSame('data', $cryptor->decrypt($encryptedA, 'password'));
$this->assertSame('data', $cryptor->decrypt($encryptedB, 'password'));
$this->assertNotSame($encryptedA, $encryptedB);

$this->assertFalse($cryptor->decrypt($encryptedA, 'passward'));
}

public function testRc4(): void
{
$cryptor = new Cryptor('rc4');
Expand All @@ -46,7 +60,7 @@ public function testRc4(): void
$this->assertNotFalse($cryptor->decrypt($encryptedA, 'passward'));
}

public function testInvalidIv(): void
public function testInvalidIvLength(): void
{
$cryptor = new Cryptor();
$this->assertFalse($cryptor->decrypt('', 'password'));
Expand All @@ -57,7 +71,44 @@ public function testInvalidIv(): void
} catch (DecryptionFailedException $e) {
$this->assertSame('', $e->getData());
$this->assertSame('Failed to decrypt.', $e->getMessage());
$this->assertSame('error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt', $e->getOriginalMessage());
$this->assertSame('invalid iv length.', $e->getOriginalMessage());
}
}

public function testInvalidTagLength(): void
{
$cryptor = new Cryptor('aes-256-gcm');
$this->assertFalse($cryptor->decrypt(str_repeat('x', 16), 'password'));

try {
$cryptor->mustDecrypt(str_repeat('x', 16), 'password');
$this->assertTrue(false);
} catch (DecryptionFailedException $e) {
$this->assertSame(str_repeat('x', 16), $e->getData());
$this->assertSame('Failed to decrypt.', $e->getMessage());
$this->assertSame('invalid tag length.', $e->getOriginalMessage());
}
}

public function testInvalidTagContent(): void
{
$cryptor = new Cryptor('aes-256-gcm');

$corrupted = substr_replace(
$cryptor->encrypt('', 'password'),
str_repeat('x', 16),
-16
);

$this->assertFalse($cryptor->decrypt($corrupted, 'password'));

try {
$cryptor->mustDecrypt($corrupted, 'password');
$this->assertTrue(false);
} catch (DecryptionFailedException $e) {
$this->assertSame($corrupted, $e->getData());
$this->assertSame('Failed to decrypt.', $e->getMessage());
$this->assertSame('invalid tag content.', $e->getOriginalMessage());
}
}
}
44 changes: 40 additions & 4 deletions tests/FixedPasswordCryptorTest.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<?php

namespace Tests;
namespace Mpyw\EasyCrypt\Tests;

use Mpyw\EasyCrypt\Cryptor;
use Mpyw\EasyCrypt\Exceptions\DecryptionFailedException;
use Mpyw\EasyCrypt\Exceptions\UnsupportedCipherException;
use Mpyw\EasyCrypt\FixedPasswordCryptor;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -43,7 +42,7 @@ public function testRc4(): void
$this->assertNotFalse($anotherCryptor->decrypt($encryptedA));
}

public function testInvalidIv(): void
public function testInvalidIvLength(): void
{
$cryptor = new FixedPasswordCryptor('password');
$this->assertFalse($cryptor->decrypt(''));
Expand All @@ -54,7 +53,44 @@ public function testInvalidIv(): void
} catch (DecryptionFailedException $e) {
$this->assertSame('', $e->getData());
$this->assertSame('Failed to decrypt.', $e->getMessage());
$this->assertSame('error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt', $e->getOriginalMessage());
$this->assertSame('invalid iv length.', $e->getOriginalMessage());
}
}

public function testInvalidTagLength(): void
{
$cryptor = new FixedPasswordCryptor('password', new Cryptor('aes-256-gcm'));
$this->assertFalse($cryptor->decrypt(str_repeat('x', 16)));

try {
$cryptor->mustDecrypt(str_repeat('x', 16));
$this->assertTrue(false);
} catch (DecryptionFailedException $e) {
$this->assertSame(str_repeat('x', 16), $e->getData());
$this->assertSame('Failed to decrypt.', $e->getMessage());
$this->assertSame('invalid tag length.', $e->getOriginalMessage());
}
}

public function testInvalidTagContent(): void
{
$cryptor = new FixedPasswordCryptor('password', new Cryptor('aes-256-gcm'));

$corrupted = substr_replace(
$cryptor->encrypt(''),
str_repeat('x', 16),
-16
);

$this->assertFalse($cryptor->decrypt($corrupted));

try {
$cryptor->mustDecrypt($corrupted);
$this->assertTrue(false);
} catch (DecryptionFailedException $e) {
$this->assertSame($corrupted, $e->getData());
$this->assertSame('Failed to decrypt.', $e->getMessage());
$this->assertSame('invalid tag content.', $e->getOriginalMessage());
}
}
}

0 comments on commit c33cd8f

Please sign in to comment.