From 4144a582d68f89e25ad1f49e0b3e5e2cf4fe5822 Mon Sep 17 00:00:00 2001 From: Vladimir Melnik Date: Tue, 24 Mar 2020 01:38:22 +0200 Subject: [PATCH] added auth tag & salt like 3ncr.org --- lib/rdcrypto.js | 38 ++++++++++++++++++-------------- lib/rdcrypto.test.js | 15 +++++++++---- src/Readdle/Crypt/Crypto.php | 27 +++++++++++++++++------ src/Readdle/Crypt/CryptoTest.php | 19 +++++++++++++--- src/Readdle/Crypt/Secret.php | 14 +++++++----- src/Readdle/Crypt/SecretTest.php | 17 +++++++++++++- 6 files changed, 93 insertions(+), 37 deletions(-) diff --git a/lib/rdcrypto.js b/lib/rdcrypto.js index 8604d4a..3e508bc 100644 --- a/lib/rdcrypto.js +++ b/lib/rdcrypto.js @@ -1,25 +1,26 @@ const crypto = require('crypto'); const IV_LENGTH = 16; -const CRYPT_METHOD = 'aes-128-cbc'; -const SECRET_LENGTH = 16; // 128 bit or 16 characters +const CRYPT_METHOD = 'aes-256-gcm'; +const SECRET_LENGTH = 32; // 256 bit or 32 characters -function RDCrypto(secret) { - if (secret.length !== SECRET_LENGTH) { - throw new Error(`Secret length should be exactly ${IV_LENGTH} characters long`); +function RDCrypto(secret, salt) { + if (secret.length < IV_LENGTH) { + throw new Error(`Secret length should be greater than ${IV_LENGTH} characters`); } - this.cryptoMarker = '-CRYPT-'; - this.secret = secret; + this.cryptoMarker = '-CRYPT-V2-'; + this.secret = crypto.pbkdf2Sync(secret, salt, 100, SECRET_LENGTH, 'sha3-256'); } RDCrypto.prototype.encrypt = function (value) { const iv = crypto.randomBytes(IV_LENGTH); - const cipher = crypto.createCipheriv(CRYPT_METHOD, Buffer.from(this.secret), iv); + const cipher = crypto.createCipheriv(CRYPT_METHOD, this.secret, iv); let encrypted = cipher.update(value); encrypted = Buffer.concat([encrypted, cipher.final()]); + const tag = cipher.getAuthTag(); - return `${this.cryptoMarker}${iv.toString('hex')}${encrypted.toString('hex')}`; + return `${this.cryptoMarker}${iv.toString('hex')}${encrypted.toString('hex')}${tag.toString("hex")}`; }; RDCrypto.prototype.decrypt = function (value) { @@ -27,23 +28,28 @@ RDCrypto.prototype.decrypt = function (value) { return value; } + if (value.length < SECRET_LENGTH + IV_LENGTH * 2) { + return value; + } + const originalValue = value; value = value.substr(this.cryptoMarker.length); try { const iv = Buffer.from(value.substring(0, IV_LENGTH * 2), "hex"); - const encrypted = Buffer.from(value.substring(IV_LENGTH * 2), "hex"); + const encrypted = Buffer.from(value.substring(IV_LENGTH * 2, value.length - SECRET_LENGTH), "hex"); + const tag = Buffer.from(value.slice(-SECRET_LENGTH), "hex"); - const decipher = crypto.createDecipheriv(CRYPT_METHOD, Buffer.from(this.secret), iv); - let decrypted = decipher.update(encrypted); + const decipher = crypto.createDecipheriv(CRYPT_METHOD, this.secret, iv); + decipher.setAuthTag(tag); + const decrypted = decipher.update(encrypted); - decrypted = Buffer.concat([decrypted, decipher.final()]); - return decrypted.toString(); + return Buffer.concat([decrypted, decipher.final()]).toString(); } catch (err) { return originalValue; } }; -module.exports = function (secret) { - return new RDCrypto(secret); +module.exports = function (secret, salt) { + return new RDCrypto(secret, salt); }; diff --git a/lib/rdcrypto.test.js b/lib/rdcrypto.test.js index 9fd58be..7c130ff 100644 --- a/lib/rdcrypto.test.js +++ b/lib/rdcrypto.test.js @@ -1,8 +1,9 @@ const secret = "NGYR4rBcywrVLqON"; -const value = "value"; +const salt = "08c51bfa2b2a4812b1a8582a"; +const value = "value"; test('successful encrypt/decrypt', () => { - const rdcrypto = require('./rdcrypto')(secret); + const rdcrypto = require('./rdcrypto')(secret, salt); const encrypted = rdcrypto.encrypt(value); expect(rdcrypto.decrypt(encrypted)).toBe(value); }); @@ -12,13 +13,19 @@ test('invalid secret provided', () => { }); test('invalid encrypted value should be returned as is', () => { - const rdcrypto = require('./rdcrypto')(secret); + const rdcrypto = require('./rdcrypto')(secret, salt); const encrypted = "-CRYPT-broken-39698c8dee76099a15e754368f1833ae62b837bf0c1709481cac859baa"; expect(rdcrypto.decrypt(encrypted)).toBe(encrypted); }); +test('previosly encrypted value successfully decrypts', () => { + const rdcrypto = require('./rdcrypto')(secret, salt); + const encrypted = "-CRYPT-V2-1deedb92c859bb0f9153efc11130b0dcd83bc4e11452baecc0fe48fbb3d3696b45327ce447"; + expect(rdcrypto.decrypt(encrypted)).toBe(value); +}); + test('encrypted value without -CRYPT- marker won\'t be decrypted', () => { - const rdcrypto = require('./rdcrypto')(secret); + const rdcrypto = require('./rdcrypto')(secret, salt); const encrypted = "-FAKE-d248d6dcc04b8b0786cdd1505f91eb2f014684f9406f2da42502bd5cbf3cf4ad"; expect(rdcrypto.decrypt(encrypted)).toBe(encrypted); }); diff --git a/src/Readdle/Crypt/Crypto.php b/src/Readdle/Crypt/Crypto.php index df288f5..419ebaf 100644 --- a/src/Readdle/Crypt/Crypto.php +++ b/src/Readdle/Crypt/Crypto.php @@ -6,8 +6,8 @@ final class Crypto implements CryptoInterface { - private const CRYPTO_MARKER = "-CRYPT-"; - private const CRYPT_METHOD = "AES-128-CBC"; + private const CRYPTO_MARKER = "-CRYPT-V2-"; + private const CRYPT_METHOD = "aes-256-gcm"; private const BLOCK_SIZE = 16; private $secret; @@ -18,10 +18,11 @@ public function __construct(Secret $secret) public function encrypt(string $value): string { + $tag = ""; $iv = \openssl_random_pseudo_bytes(self::BLOCK_SIZE); - $encrypted = \bin2hex(\openssl_encrypt($value, self::CRYPT_METHOD, (string)$this->secret, OPENSSL_RAW_DATA, $iv)); + $encrypted = \openssl_encrypt($value, self::CRYPT_METHOD, (string)$this->secret, OPENSSL_RAW_DATA, $iv, $tag); - return self::CRYPTO_MARKER . \bin2hex($iv) . $encrypted; + return self::CRYPTO_MARKER . \bin2hex($iv) . \bin2hex($encrypted) . \bin2hex($tag); } public function decrypt(string $value): string @@ -30,13 +31,25 @@ public function decrypt(string $value): string return $value; } + if (\strlen($value) < self::BLOCK_SIZE * 4) { + return $value; + } + $original = $value; try { $value = \substr($value, \strlen(self::CRYPTO_MARKER)); - $iv = \hex2bin(\substr($value, 0, self::BLOCK_SIZE * 2)); - $decrypted = \hex2bin(\substr($value, self::BLOCK_SIZE * 2)); + $iv = \substr($value, 0, self::BLOCK_SIZE * 2); + $decrypted = \substr($value, self::BLOCK_SIZE * 2, -(self::BLOCK_SIZE * 2)); + $tag = \substr($value, -(self::BLOCK_SIZE * 2)); - return \openssl_decrypt($decrypted, self::CRYPT_METHOD, (string)$this->secret, OPENSSL_RAW_DATA, $iv); + return \openssl_decrypt( + \hex2bin($decrypted), + self::CRYPT_METHOD, + (string)$this->secret, + OPENSSL_RAW_DATA, + \hex2bin($iv), + \hex2bin($tag) + ); } catch (\Throwable $e) { return $original; } diff --git a/src/Readdle/Crypt/CryptoTest.php b/src/Readdle/Crypt/CryptoTest.php index a275e82..772de65 100644 --- a/src/Readdle/Crypt/CryptoTest.php +++ b/src/Readdle/Crypt/CryptoTest.php @@ -14,8 +14,8 @@ class CryptoTest extends TestCase public function invalidDecryptedValues(): array { return [ - ["-CRYPT-qw215ea85900cf7f9e41a28facfaed8409209aa44b4e4eefb33b5a9929ea2e498"], - ["-CRYPT-1115ea85900cf7f9e41a28facfaed8409209aa44b4e4eefb33b5a9929ea2e498"], + ["-CRYPT-V2-qw215ea85900cf7f9e41a28facfaed8409209aa44b4e4eefb33b5a9929ea2e498"], + ["-CRYPT-V2-1115ea85900cf7f9e41a28facfaed8409209aa44b4e4eefb33b5a9929ea2e498"], ["-FAKE-1115ea85900cf7f9e41a28facfaed8409209aa44b4e4eefb33b5a9929ea2e498"], ]; } @@ -34,9 +34,22 @@ public function testSuccessFullEncodeDecode() self::assertEquals("value", $decrypted); } + public function testCompatibilityWithJs() + { + $this->rdcrypto = new Crypto(Secret::fromString("NGYR4rBcywrVLqON")->salty("08c51bfa2b2a4812b1a8582a")); + $decrypted = $this->rdcrypto->decrypt("-CRYPT-V2-1deedb92c859bb0f9153efc11130b0dcd83bc4e11452baecc0fe48fbb3d3696b45327ce447"); + self::assertEquals("value", $decrypted); + } + + public function testPreviouslyEncrypted() + { + $decrypted = $this->rdcrypto->decrypt("-CRYPT-V2-227effcfccf3115e1b7c4725426efb0f66d1dcd58864863faeecca884dfa90f7e9b8f977cc"); + self::assertEquals("value", $decrypted); + } + protected function setUp(): void { - $this->secret = "NGYR4rBcywrVLqON"; + $this->secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; $this->rdcrypto = new Crypto(Secret::fromString($this->secret)); } } diff --git a/src/Readdle/Crypt/Secret.php b/src/Readdle/Crypt/Secret.php index c7da4f2..3e80047 100644 --- a/src/Readdle/Crypt/Secret.php +++ b/src/Readdle/Crypt/Secret.php @@ -6,18 +6,15 @@ final class Secret { - const LENGTH = 16; + const MIN_LENGTH = 16; private $value; private function __construct(string $value) { - if (self::LENGTH !== \strlen($value)) { + if (self::MIN_LENGTH > \strlen($value)) { throw new InvalidArgumentException( - \sprintf( - "Secret length should be exactly %s characters long", - self::LENGTH - ) + \sprintf("Secret length should be greater than %s characters", self::MIN_LENGTH) ); } $this->value = $value; @@ -28,6 +25,11 @@ public static function fromString(string $value): self return new self($value); } + public function salty(string $salt): self + { + return new self(\hash_pbkdf2("sha3-256", $this->value, $salt, 100, 32, true)); + } + public function __toString(): string { return $this->value; diff --git a/src/Readdle/Crypt/SecretTest.php b/src/Readdle/Crypt/SecretTest.php index 4cc90df..eb868ae 100644 --- a/src/Readdle/Crypt/SecretTest.php +++ b/src/Readdle/Crypt/SecretTest.php @@ -16,7 +16,22 @@ public function testInvalidSecret() public function testValidSecret() { - $value = \str_pad("", Secret::LENGTH, "a"); + $value = \str_pad("", Secret::MIN_LENGTH, "a"); self::assertSame($value, (string)Secret::fromString($value)); + + $value = \str_pad("", Secret::MIN_LENGTH * 2, "a"); + self::assertSame($value, (string)Secret::fromString($value)); + } + + public function testSaltedSecret() + { + $value = \str_pad("", Secret::MIN_LENGTH, "a"); + $secret = Secret::fromString($value); + $salty = $secret->salty(""); + + self::assertNotEquals(\spl_object_id($secret), \spl_object_id($salty)); + + self::assertSame($value, (string)$secret); + self::assertNotSame((string)$secret, (string)$salty); } }