nsec icon indicating copy to clipboard operation
nsec copied to clipboard

What it means in practice: "Using the same nonce with the same key more than once leads to catastrophic loss of security."

Open igorgomeslima opened this issue 2 years ago • 5 comments

Hello,

It's is not an issue per si, but, this repo does not have Discussions tab enabled.


I'm try using the AeadAlgorithm.XChaCha20Poly1305, and I little bit confused when read:

Note

Using the same nonce with the same key more than once leads to catastrophic loss of security.

To prevent nonce reuse when encrypting multiple plaintexts with the same key, it is recommended to increment the previous nonce; a randomly generated nonce is not suitable.

In my scenario I need to encrypt some data and decrypt this data in another moment, like an Token:

>. Request: `my-web-api/token/generate`
>. Response: Some `ciphertext` generated by `XChaCha20Poly1305` in `Base64` format.

But when I tried to use the recommended practices in nonce value(increment) I can't Decrypt data. In my thinking I need the suitable nonce to do the correct data decryption, but, how will I know the appropriate one, if I auto-incremented the value? I need always extern/public the nonce I used? Like:

"Encrypt"

>. Request: `my-web-api/token/generate`
>. Response: Some `ciphertext` generated by `XChaCha20Poly1305` in `Base64` format + `nonce`.

"Decrypt"

>. Request: `my-web-api/token/validate`
>. Body: `ciphertext` in `Base64` format with `nonce`.

Some foo code example:

class Program
    {
        public static byte[] ExportedKey;
        
        static void Main(string[] args)
        {
            const string json =
                @"{'Id':'62fea73265676bb8c06749a7','TriadKey':'111155555555555555555555555555555555555555555151513321321321654654654897897654654321321112','IsValid':false,'ExpirationDate':'2022-08-19T20:55:14.6146798Z'}";

            Aes256Gcm(AeadAlgorithm.Aes256Gcm, "Z8c0kX9ATszSsKCAROHnZw==", json);
            
            Console.ReadLine();
        }

        private static void Aes256Gcm(AeadAlgorithm aeadAlgorithm, string nonceText, string dataText)
        {
            // create a new key pair
            var creationParameters = new KeyCreationParameters
            {
                ExportPolicy = KeyExportPolicies.AllowPlaintextArchiving
            };
            
            using var key = Key.Create(aeadAlgorithm, creationParameters);
            ExportedKey = key.Export(KeyBlobFormat.NSecSymmetricKey);
            File.WriteAllBytes("private-secret-key.nsec", ExportedKey);
        
            // generate some data to be signed
            var data = Encoding.UTF8.GetBytes(dataText);
            var nonce = Encoding.UTF8.GetBytes(nonceText);
            // increment nonce
            nonce[0]++;

            // sign the data using the private key
            var ciphertext = aeadAlgorithm.Encrypt(key: key, nonce: nonce, plaintext: data, associatedData: null);
            Console.WriteLine($"Encrypted Data:\n{Convert.ToBase64String(ciphertext)}\n");
            
            // DUMB: simulates "new request" `my-web-api/token/validate` = NEW NONCE/INCREMENTED
            nonce[0]++;
            
            Decrypt(aeadAlgorithm, nonce, ciphertext);
        }

        private static void Decrypt(AeadAlgorithm aeadAlgorithm, byte[] nonce, byte[] ciphertext)
        {
            // re-import it
            using var key = Key.Import(aeadAlgorithm, ExportedKey, KeyBlobFormat.NSecSymmetricKey);
            
            var decryptedPlaintext = aeadAlgorithm.Decrypt(key, nonce, default, ciphertext);

            Console.WriteLine
            (
                decryptedPlaintext is null
                    ? "Data decryption failed."
                    : $"Decrypted Data:\n{Encoding.UTF8.GetString(decryptedPlaintext)}\n"
            );
        }
    }

OUTPUT: "Data decryption failed." - Because use different nouce

What would be the proper way to deal with this scenario?

Cheers! 😁

igorgomeslima avatar Aug 23 '22 22:08 igorgomeslima

In short: Every time you encrypt, you must use a nonce that has not been used before (with the same key). When you decrypt, you must decrypt using exactly the nonce that was used for encryption. Shipping the nonce along with the ciphertext is indeed one way to do that.

(Note that nonce[0]++; only increments the first byte of the nonce, so after 256 increments it starts repeating!)

ektrah avatar Aug 23 '22 23:08 ektrah

In short: Every time you encrypt, you must use a nonce that has not been used before (with the same key). When you decrypt, you must decrypt using exactly the nonce that was used for encryption. Shipping the nonce along with the ciphertext is indeed one way to do that.

(Note that nonce[0]++; only increments the first byte of the nonce, so after 256 increments it starts repeating!)

Thanks @ektrah! Yes, about nouce[0]++ I already put a comment: // DUMB: simulates "new request"... etc.

I wrote something like this(dummy only for sample purposes):

Cryptography.cs

using Geralt;
...


public static class DummyCryptography
    {
        private static readonly Key NSecKey;
        private static readonly AeadAlgorithm Algorithm;
        
        // Bypass the BAD implementation to reuse a created `Key`...
        static Cryptography()
        {
            Algorithm = AeadAlgorithm.Aes256Gcm;
            
            var creationParameters = new KeyCreationParameters
            {
                ExportPolicy = KeyExportPolicies.AllowPlaintextArchiving
            };

            // create a new key
            using var key = Key.Create(Algorithm, creationParameters);
            
            var exportedKey = key.Export(KeyBlobFormat.NSecSymmetricKey);

            NSecKey = Key.Import(Algorithm, exportedKey, KeyBlobFormat.NSecSymmetricKey);
        }

        public static string Encrypt(string nonceText, string plainText)
        {
            // generate some data to be signed
            var data = Encoding.UTF8.GetBytes(plainText);
            
            // nouce with increment
            var nonce = Encoding.UTF8.GetBytes(nonceText);
            ConstantTime.Increment(nonce); // -> https://www.geralt.xyz/constant-time#increment

            // sign the data using the private key
            var ciphertext = Algorithm.Encrypt(key: NSecKey, nonce: nonce, plaintext: data, associatedData: default);

            return $"{Convert.ToBase64String(ciphertext)};{Encoding.UTF8.GetString(nonce)}";
        }

        public static string Decrypt(string nonceText, string cipherText)
        {
            var cipherTextBytes = Convert.FromBase64String(cipherText);
            
            var nonce = Encoding.UTF8.GetBytes(nonceText);
            
            var plainText = Algorithm.Decrypt(NSecKey, nonce, default, cipherTextBytes);

            return Encoding.UTF8.GetString(plainText!);
        }
    }

TokenController.cs

public class TokenController : ControllerBase
   {
       private readonly ILogger<TokenController> _logger;

       public TokenController(ILogger<TokenController> logger)
       {
           _logger = logger;
       }

       [HttpGet]
       public IActionResult Get()
       {
          const string plainText = @"{'Id':'62fea73265676bb8c06749a7','TriadKey':'111155555555555555555555555555555555555555555151513321321321654654654897897654654321321112','IsValid':false,'ExpirationDate':'2022-08-19T20:55:14.6146798Z'}";

           var token = DummyCryptography.Encrypt(Consts.CryptographyNonce, plainText);

           return Ok(token);
       }
       
       [HttpPost]
       public IActionResult Post(string token)
       {
           var splitedToken = token.Split(';');
           var encryptedToken = splitedToken[0];
           var cryptographyNonce = splitedToken[1];
           
           var tokenInfo = DummyCryptography.Decrypt(cryptographyNonce, encryptedToken);

           return Ok(tokenInfo);
       }
   }

It's is a good startup point? Do you have any other advice/method/format to returning the nonce value to the end user? :)

igorgomeslima avatar Aug 24 '22 01:08 igorgomeslima

That’s a good starting point but needs a bit more work. The nonce needs to be 12 bytes and is not a UTF-8 string. Have a look at RFC 5116, Section 3 on nonce generation. You’ll also likely want to change the key every once in a while (e.g., after a certain number of nonces have been generated and/or some time has passed).

Also, checkout JWT and PASETO.

ektrah avatar Aug 24 '22 10:08 ektrah

That’s a good starting point but needs a bit more work. The nonce needs to be 12 bytes and is not a UTF-8 string. Have a look at RFC 5116, Section 3 on nonce generation. You’ll also likely want to change the key every once in a while (e.g., after a certain number of nonces have been generated and/or some time has passed).

Also, checkout JWT and PASETO.

I will check the mentioned RFC. I already thinked about JWT, considering the data needs to be encrypted(no passwords are involved by the way)... maybe a JWE is more adequate in my scenario.


One last doubt...

When you says:

The nonce needs to be 12 bytes and is not a UTF-8 string

You refer this part of code?

return $"{Convert.ToBase64String(ciphertext)};{Encoding.UTF8.GetString(nonce)}";

Thank's to listen @ektrah.

igorgomeslima avatar Aug 24 '22 14:08 igorgomeslima

You refer this part of code?

@igorgomeslima Yes, you could prepend the nonce to the ciphertext (e.g. using Array.Copy(), the LINQ .Concat(), etc) before converting the whole thing to a Base64 string. Then you convert the Base64 to a byte array and retrieve the nonce as bytes from the beginning.

samuel-lucas6 avatar Aug 31 '22 18:08 samuel-lucas6

Thanks @samuel-lucas6!

igorgomeslima avatar Sep 27 '22 21:09 igorgomeslima