Skip to content

Instantly share code, notes, and snippets.

@ayende
Created May 2, 2024 11:38
Show Gist options
  • Save ayende/d5e0c2392bec98c73f580dc1182380ff to your computer and use it in GitHub Desktop.
Save ayende/d5e0c2392bec98c73f580dc1182380ff to your computer and use it in GitHub Desktop.
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
Stopwatch sp = Stopwatch.StartNew();
var output = DeniableEncryption.Encrypt(
("P@ssw0rd", "Joey doesn't share food!"),
("swordfish", "Meet at dawn by the beach to toast the new year"),
("adm1n!str@t0r", "We were on a break!"),
("Qwerty!Asdf@2024", "Bitcoin seed: lonely ghost need apology spend shy festival funds")
);
var outputBase = Convert.ToBase64String(output, Base64FormattingOptions.InsertLineBreaks);
Console.WriteLine(outputBase);
Console.WriteLine("Encrypt: " + sp.ElapsedMilliseconds + " with " + output.Length);
Output("swordfish", output);
Output("P@ssw0rd", output);
Output("Qwerty!Asdf@2024", output);
Output("adm1n!str@t0r", output);
Output("other", output);
static void Output(string pwd, byte[] encrypted)
{
var sp = Stopwatch.StartNew();
Console.WriteLine(DeniableEncryption.Decrpyt(pwd, encrypted));
Console.WriteLine(sp.ElapsedMilliseconds);
}
public static class DeniableEncryption
{
public const int Iterations = 250_000; // https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Password_Storage_Cheat_Sheet.md#pbkdf2
public const int BlockSize = 64;
public const int DerivedKeySize = 32;
public const int MaxUserItems = 6;
public const int ItemsCount = 8;
public const int SaltSize = 32;
public const int OffsetsBlockSize = ItemsCount * sizeof(int);
static DeniableEncryption() => Debug.Assert(MaxUserItems < ItemsCount, "You must leave at least some really dummy items");
public static string? Decrpyt(string pwd, byte[] encrypted)
{
Span<byte> mem = encrypted;
var salt = mem.Slice(0, SaltSize);
ReadOnlySpan<byte> derived = Rfc2898DeriveBytes.Pbkdf2(pwd, salt, Iterations, HashAlgorithmName.SHA512, sizeof(int) + DerivedKeySize + sizeof(int));
var offsetMask = MemoryMarshal.Read<int>(derived.Slice(0, sizeof(int)));
var lenMask = MemoryMarshal.Read<int>(derived.Slice(sizeof(int), sizeof(int)));
var derivedKey = derived.Slice(sizeof(int) + sizeof(int), DerivedKeySize);
var offsetsBlock = MemoryMarshal.Cast<byte, int>(mem.Slice(SaltSize, OffsetsBlockSize));
for (int i = 0; i < ItemsCount; i++)
{
var offset = offsetsBlock[i] ^ offsetMask;
if (offset < SaltSize + OffsetsBlockSize || offset + sizeof(int) > mem.Length)
continue;
var maskedLen = MemoryMarshal.Read<int>(mem.Slice(offset, sizeof(int)));
var len = maskedLen ^ lenMask;
if (len < 0 || offset + len + sizeof(int) > mem.Length)
continue;
using var cipher = new AesGcm(derivedKey, AesGcm.TagByteSizes.MaxSize);
var outputBuf = new byte[len];
try
{
cipher.Decrypt(
nonce: mem.Slice(offset + sizeof(int), AesGcm.NonceByteSizes.MaxSize),
ciphertext: mem.Slice(offset + sizeof(int) + AesGcm.NonceByteSizes.MaxSize + AesGcm.TagByteSizes.MaxSize, len),
tag: mem.Slice(offset + sizeof(int) + AesGcm.NonceByteSizes.MaxSize, AesGcm.TagByteSizes.MaxSize),
outputBuf);
}
catch (CryptographicException)
{
// expected, we may hit a dummy value or wrong password
}
return Encoding.UTF8.GetString(outputBuf);
}
return null;
}
public static byte[] Encrypt(params (string Password, string Value)[] items)
{
if (items.Length > MaxUserItems)
throw new ArgumentException("You are allowed up to " + MaxUserItems + " items, but got: " + items.Length);
if (items.GroupBy(x => x.Password).Any(x => x.Count() != 1))
throw new ArgumentException("You are not allowed up to reuse passwords between messages, but got a duplicate password");
var totalSize = items.Max(x => Encoding.UTF8.GetByteCount(x.Value) + sizeof(int) + AesGcm.NonceByteSizes.MaxSize + AesGcm.TagByteSizes.MaxSize);
var sizeAlignedUp = (totalSize + BlockSize - 1) & -BlockSize;
var additionalSizeMixed = RandomNumberGenerator.GetInt32(1, 4) * RandomNumberGenerator.GetInt32(BlockSize / 2, BlockSize);
var outputBuffer = RandomNumberGenerator.GetBytes(ItemsCount * sizeAlignedUp + OffsetsBlockSize + SaltSize + additionalSizeMixed);
Span<byte> output = outputBuffer;
var salt = output.Slice(0, SaltSize);
var offsetsBlock = MemoryMarshal.Cast<byte, int>(output.Slice(SaltSize, OffsetsBlockSize));
int index = RandomNumberGenerator.GetInt32(ItemsCount);
foreach (var (pwd, val) in items)
{
ReadOnlySpan<byte> derived = Rfc2898DeriveBytes.Pbkdf2(pwd, salt, Iterations, HashAlgorithmName.SHA512, sizeof(int) + DerivedKeySize + sizeof(int));
var plaintext = Encoding.UTF8.GetBytes(val);
var requiredSize = sizeof(int) + AesGcm.NonceByteSizes.MaxSize + AesGcm.TagByteSizes.MaxSize + plaintext.Length;
var offset = sizeAlignedUp * index + SaltSize + OffsetsBlockSize +
RandomNumberGenerator.GetInt32(sizeAlignedUp - requiredSize);
var sizeMask = MemoryMarshal.Read<int>(derived.Slice(0, sizeof(int)));
offsetsBlock[index] = offset ^ sizeMask;
index = (index + 1) % ItemsCount;
Span<byte> mem = output.Slice(offset, requiredSize);
var lenMask = MemoryMarshal.Read<int>(derived.Slice(sizeof(int), sizeof(int)));
var mask = lenMask ^ plaintext.Length;
MemoryMarshal.Write(mem, mask);
var derivedKey = derived.Slice(sizeof(int) + sizeof(int), DerivedKeySize);
using var cipher = new AesGcm(derivedKey, AesGcm.TagByteSizes.MaxSize);
cipher.Encrypt(
nonce: mem.Slice(sizeof(int), AesGcm.NonceByteSizes.MaxSize),
plaintext: plaintext,
ciphertext: mem.Slice(sizeof(int) + AesGcm.NonceByteSizes.MaxSize + AesGcm.TagByteSizes.MaxSize, plaintext.Length),
tag: mem.Slice(sizeof(int) + AesGcm.NonceByteSizes.MaxSize, AesGcm.TagByteSizes.MaxSize));
}
return outputBuffer;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment