Skip to content

Commit

Permalink
added auth tag & salt like 3ncr.org
Browse files Browse the repository at this point in the history
  • Loading branch information
melya committed Mar 23, 2020
1 parent b795582 commit 4144a58
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 37 deletions.
38 changes: 22 additions & 16 deletions lib/rdcrypto.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,55 @@
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) {
if (0 !== value.indexOf(this.cryptoMarker)) {
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);
};
15 changes: 11 additions & 4 deletions lib/rdcrypto.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
Expand All @@ -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);
});
27 changes: 20 additions & 7 deletions src/Readdle/Crypt/Crypto.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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;
}
Expand Down
19 changes: 16 additions & 3 deletions src/Readdle/Crypt/CryptoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
];
}
Expand All @@ -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));
}
}
14 changes: 8 additions & 6 deletions src/Readdle/Crypt/Secret.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
17 changes: 16 additions & 1 deletion src/Readdle/Crypt/SecretTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

0 comments on commit 4144a58

Please sign in to comment.