nsec
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."
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! 😁
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!)
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? :)
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.
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.
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.
Thanks @samuel-lucas6!