Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration backup with AES-256-GCM #5665

Closed
wants to merge 12 commits into from
Closed

Conversation

oittaa
Copy link
Contributor

@oittaa oittaa commented Apr 1, 2022

As discussed a little bit in #5661

AES-256-GCM mode provides proper integrity checking. Additional authentication data can be added easily if something needs to be authenticated, but made available prior to decryption some time in the future.

This pull request also removes openssl shell commands and implements the needed legacy key derivation function in pure PHP.

Example below: I wrapped the three new methods (keyAndIV, opensslDecrypt, opensslEncrypt) in a test class if people want to test easily on their own machines without modifying the whole OPNsense installation.

<?php

class Encryption
{
    private function keyAndIV(string $cipher, string $hashAlgo, string $password, string $salt, ?int $iterations): array
    {
        /* AES-256 key size is 32 bytes */
        $keyLength = 32;
        $ivLength = openssl_cipher_iv_length($cipher);

        if (!is_null($iterations)) {
            $key = hash_pbkdf2($hashAlgo, $password, $salt, $iterations, $keyLength + $ivLength, true);
        } else {
            /* pre-21.7 */
            $key = $temp = '';
            while (strlen($key) < $keyLength + $ivLength) {
                $temp = hash($hashAlgo, $temp . $password . $salt, true);
                $key .= $temp;
            }
        }

        $iv = substr($key, $keyLength, $ivLength);
        $key = substr($key, 0, $keyLength);
        return [$key, $iv];
    }

    private function opensslDecrypt(string $data, string $password, string $cipher, string $hashAlgo, ?int $iterations): ?string
    {
        /* aes-256-gcm defaults */
        $saltOffset = 0;
        $saltLength = 16;
        $tagLength = 16;

        if (!in_array($cipher, openssl_get_cipher_methods()) || !in_array($hashAlgo, hash_algos())) {
            /* ciher or hash not supported */
            return null;
        }
        if ($cipher === 'aes-256-cbc') {
            /* pre- XXX */
            $saltOffset = 8; // skip 'Salted__'
            $saltLength = 8;
            $tagLength = 0;
        }

        $data = base64_decode($data);
        if (strlen($data) < $saltOffset + $saltLength + $tagLength) {
            /* not enough data */
            return null;
        }
        $salt = substr($data, $saltOffset, $saltLength);
        $tag = substr($data, $saltOffset + $saltLength, $tagLength);
        $data = substr($data, $saltOffset + $saltLength + $tagLength);
        [$key, $iv] = $this->keyAndIV($cipher, $hashAlgo, $password, $salt, $iterations);

        $result = openssl_decrypt(
            $data,
            $cipher,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );
        return $result === false ? null : $result;
    }

    private function opensslEncrypt(string $data, string $password, string $cipher, string $hashAlgo, int $iterations): ?string
    {
        $salt = random_bytes(16);
        [$key, $iv] = $this->keyAndIV($cipher, $hashAlgo, $password, $salt, $iterations);

        $result = openssl_encrypt(
            $data,
            $cipher,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            '',
            16
        );
        return $result === false ? null : base64_encode($salt . $tag . $result);
    }

    public function decrypt($data, $pass, $cipher = 'aes-256-gcm', $hash = 'sha512', $pbkdf2 = 100000)
    {
        $output = $this->opensslDecrypt($data, $pass, $cipher, $hash, $pbkdf2);
        return $output;
    }

    public function encrypt($data, $pass)
    {
        $cipher = 'aes-256-gcm';
        $hash = 'sha512';
        $pbkdf2 = '100000';
        $output = $this->opensslEncrypt($data, $pass, $cipher, $hash, $pbkdf2);
        return $output;
    }
}

$e = new Encryption();

/*
AES-256-CBC MD5

$ echo "my data to be encrypted" | openssl enc -aes-256-cbc -a -md md5 -p -pass pass:'my super complex password !"#$€éä'
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
salt=0BF40F5988DB9F8A
key=AEEA7AC5A93457EF2EE6CA70DBA2A7B0B5DDF7DDE15514B3B3AD0F2E62D2D66B
iv =A2D112F283F15DD112AC2537BDCFCFAF
U2FsdGVkX18L9A9ZiNufihFGyhh0+oyT7B3KDGFd9RoNyDv2VCMDF9Kc3gNSwVG+
*/
echo "### AES-256-CBC MD5\n";
$data = 'U2FsdGVkX18L9A9ZiNufihFGyhh0+oyT7B3KDGFd9RoNyDv2VCMDF9Kc3gNSwVG+';
$pass = 'my super complex password !"#$€éä';
var_dump($e->decrypt($data, $pass, 'aes-256-cbc', 'md5', null));


/*
AES-256-CBC SHA512

$ echo "my data to be encrypted" | openssl enc -aes-256-cbc -a -md sha512 -salt -pbkdf2 -iter 100000 -p -pass pass:'my super complex password !"#$€éä'
salt=7B71AEE703562A1E
key=B990D67A81487531372E9A0FEDE221D9217FFC1F69378D8CA0C257E4B8C65B35
iv =0ED0182EC2BF5BB2AAAFDC6F0EEBF021
U2FsdGVkX197ca7nA1YqHlSOkg0ZTJJ6EBThFdnTV4IzUadvh4+TieKOY/4gCYNw
*/
echo "### AES-256-CBC SHA512\n";
$data = 'U2FsdGVkX197ca7nA1YqHlSOkg0ZTJJ6EBThFdnTV4IzUadvh4+TieKOY/4gCYNw';
$pass = 'my super complex password !"#$€éä';
var_dump($e->decrypt($data, $pass, 'aes-256-cbc', 'sha512', 100000));


/*

AES-256-GCM SHA512

*/
echo "### AES-256-GCM SHA512\n";
$output = $e->encrypt("my data to be encrypted\n", 'my super complex password !"#$€éä');
var_dump($output);
var_dump($e->decrypt($output, 'my super complex password !"#$€éä'));

@oittaa
Copy link
Contributor Author

oittaa commented Apr 2, 2022

100 000 rounds is quite low number of rounds in year 2022. Approximate one second duration (but at least 100 000 rounds in every case) could be calculated with:

<?php
$timer = hrtime(true);
$temp = hash_pbkdf2('sha512', 'password', random_bytes(16), 100_000, 44, true);
$timer = (hrtime(true) - $timer) / 10 ** 9;
$iterations = max(100_000, ceil(100_000 / $timer));

var_dump($iterations);

I think at least one second should be spent here since it is quite rare occurrence unlike login which should be relatively fast. What do you think?

@oittaa
Copy link
Contributor Author

oittaa commented Apr 2, 2022

It would be great if someone with older AES-256-CBC MD5 based backups could test this. I have only tested with newer AES-256-CBC SHA512 files since that's all I had.

@AdSchellevis
Copy link
Member

@oittaa you could always try on our forum if someone would like to test your code.

@oittaa
Copy link
Contributor Author

oittaa commented Apr 2, 2022

@oittaa
Copy link
Contributor Author

oittaa commented Apr 2, 2022

I actually installed OPNsense version 21.1, took an encrypted backup, and restored it on a recent OPNsense with this patch. It worked as expected.

Encrypted config:


---- BEGIN config.xml ----
Version: OPNsense 21.1
Cipher: AES-256-CBC
Hash: MD5

U2FsdGVkX182jFXrGnrSrD16ybfZY0nkFsxG0ng65E/U5f/P0pL0N2FdCCr4w12443ujm2BS7KZe

and so on...

@oittaa
Copy link
Contributor Author

oittaa commented Apr 2, 2022

Here's an example why integrity checks matter and what can happen when a single bit flips.

$ echo -n 'ATTACK AT DAWN' | openssl enc -aes-256-cbc -a -md sha512 -salt -pbkdf2 -iter 100000 -pass pass:'password'
U2FsdGVkX19+WSFvvM2d+S2Y2JBWIhKzdnyHlGX7eAs=
$ echo 'U2FsdGVkX19+WSFvvM2d+S2Y2JBWIhKzdnyHlGX7eAr=' | openssl enc -aes-256-cbc -d -a -md sha512 -salt -pbkdf2 -iter 100000 -pass pass:'password'#*{;֐�}KyRC�

Only the last s changed to r: 0b1010011 -> 0b1010010, and openssl didn't return any errors.

@oittaa
Copy link
Contributor Author

oittaa commented Apr 3, 2022

$ phpunit --configuration PHPunit.xml --filter Backup
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.4.28
Configuration: PHPunit.xml

.....                                                               5 / 5 (100%)

Time: 00:00.913, Memory: 8.00 MB

OK (5 tests, 7 assertions)

@AdSchellevis
Copy link
Member

closing, not enough traction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants