-
-
Save ayende/d5e0c2392bec98c73f580dc1182380ff to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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