Skip to content

Commit

Permalink
Improved overall security of keystores
Browse files Browse the repository at this point in the history
  • Loading branch information
Aeliux committed Apr 18, 2024
1 parent 940318e commit e3577af
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 83 deletions.
2 changes: 1 addition & 1 deletion src/Kryptor.Tool/Arguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal class Arguments
[Option('g', "generate", SetName = "generate", Required = true, HelpText = "Generates a new keystore file.")]
public bool Generate { get; set; }

[Option('k', "keystore-size", SetName = "generate", Default = 256, HelpText = "Usable with -g, Specifies the number of keys to be generated for new keystore.")]
[Option('k', "keystore-size", SetName = "generate", HelpText = "Usable with -g, Specifies the number of keys to be generated for the new keystore.")]
public int KeyStoreSize { get; set; }

[Option('c', "continuous", HelpText = "Usable with -e or -d, Use more secure continuous encryption/decryption method.")]
Expand Down
13 changes: 9 additions & 4 deletions src/Kryptor.Tool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
if (opt.Encrypt || opt.Decrypt)
{
KESKeyStore ks = default;
KESProvider kp = default;
KESProvider kp = KESProvider.Empty;

if (opt.Decrypt || !opt.CreateKey)
{
Expand Down Expand Up @@ -160,7 +160,7 @@ KESKeyStore ReadKeystore(string keystore)
try
{
Echo(new Colorize($"Reading keystore: [{Path.GetFileName(keystore)}]", ConsoleColor.DarkYellow));
KESKeyStore ks = KESKeyStore.FromString(File.ReadAllText(keystore));
KESKeyStore ks = new KESKeyStore(File.ReadAllBytes(keystore));

Echo(new Colorize($"Keystore Fingerprint: [{ks.Fingerprint.FormatFingerprint()}]", ConsoleColor.Blue));
return ks;
Expand Down Expand Up @@ -241,15 +241,20 @@ string GetVersionString(Assembly assembly)
return string.Join('.', ver.Major, ver.Minor, ver.Build);
}

KESKeyStore GenerateKeystore(string name = "", int keystoreSize = 256)
KESKeyStore GenerateKeystore(string name = "", int keystoreSize = 0)
{
if (keystoreSize == 0)
{
keystoreSize = KESKeyStore.GetRandomOddNumber();
}

Echo(new Colorize($"Generating keystore with [{keystoreSize}] keys", ConsoleColor.Cyan));
KESKeyStore ks = KESKeyStore.Generate(keystoreSize);

Echo(new Colorize($"Keystore Fingerprint: [{ks.Fingerprint.FormatFingerprint()}]", ConsoleColor.Blue));

var fName = !string.IsNullOrEmpty(name) ? name : BitConverter.ToString(ks.Fingerprint).Replace("-", "").ToLower() + ".kks";
File.WriteAllText(fName, ks.ToString());
File.WriteAllBytes(fName, ks.Raw);

Echo(new Colorize($"Keystore is saved to [{fName}]", ConsoleColor.Green));
return ks;
Expand Down
80 changes: 80 additions & 0 deletions src/Kryptor/Chunk.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;

namespace System.Linq
{
/// <inheritdoc/>
public static partial class Enumerable
{
/// <summary>
/// Split the elements of a sequence into chunks of size at most <paramref name="size"/>.
/// </summary>
/// <remarks>
/// Every chunk except the last will be of size <paramref name="size"/>.
/// The last chunk will contain the remaining elements and may be of a smaller size.
/// </remarks>
/// <param name="source">
/// An <see cref="IEnumerable{T}"/> whose elements to chunk.
/// </param>
/// <param name="size">
/// Maximum size of each chunk.
/// </param>
/// <typeparam name="TSource">
/// The type of the elements of source.
/// </typeparam>
/// <returns>
/// An <see cref="IEnumerable{T}"/> that contains the elements the input sequence split into chunks of size <paramref name="size"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="source"/> is null.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="size"/> is below 1.
/// </exception>
public static IEnumerable<TSource[]> Chunk<TSource>(this IEnumerable<TSource> source, int size)
{
if (source == null)
{
throw new ArgumentNullException("source");
}

if (size < 1)
{
throw new ArgumentOutOfRangeException("size");
}

return ChunkIterator(source, size);
}

private static IEnumerable<TSource[]> ChunkIterator<TSource>(IEnumerable<TSource> source, int size)
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource[] chunk = new TSource[size];
chunk[0] = e.Current;

int i = 1;
for (; i < chunk.Length && e.MoveNext(); i++)
{
chunk[i] = e.Current;
}

if (i == chunk.Length)
{
yield return chunk;
}
else
{
Array.Resize(ref chunk, i);
yield return chunk;
yield break;
}
}
}
}
}
}
23 changes: 0 additions & 23 deletions src/Kryptor/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,6 @@ namespace SAPTeam.Kryptor
/// </summary>
public static class Extensions
{
/// <summary>
/// Divides a string into chunks of a specified size.
/// </summary>
/// <param name="source">
/// The string to divide.
/// </param>
/// <param name="maxChunkSize">
/// The maximum size of each chunk.
/// </param>
/// <returns>
/// An <see cref="IEnumerable{T}"/> of strings, each containing a chunk of the original string.
/// </returns>
static public IEnumerable<T[]> Slice<T>(this Array source, int maxChunkSize)
{
for (int i = 0; i < source.Length; i += maxChunkSize)
{
var actualSize = Math.Min(source.Length - i, maxChunkSize);
T[] slice = new T[actualSize];
Array.Copy(source, i, slice, 0, actualSize);
yield return slice;
}
}

/// <summary>
/// SHA256 encrypt
/// </summary>
Expand Down
107 changes: 59 additions & 48 deletions src/Kryptor/KESKeyStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ namespace SAPTeam.Kryptor
/// </summary>
public struct KESKeyStore
{
private static Random random;
readonly int keystoreLength;
private static Random random = new Random();
readonly int count;

/// <summary>
/// Gets the key at the specified index.
Expand All @@ -27,9 +27,16 @@ public byte[] this[int index]
{
get
{
if (index >= keystoreLength)
if (index < 0)
{
index -= (index / keystoreLength) * keystoreLength;
index = count + index;
}

index = Math.Abs(index);

if (index >= count)
{
index -= (index / count) * count;
}

return Keys.ElementAt(index);
Expand All @@ -41,6 +48,11 @@ public byte[] this[int index]
/// </summary>
public IEnumerable<byte[]> Keys { get; }

/// <summary>
/// Gets the raw flat bytes array of data.
/// </summary>
public byte[] Raw { get; }

/// <summary>
/// Gets the unique fingerprint of this keystore.
/// </summary>
Expand All @@ -49,26 +61,16 @@ public byte[] this[int index]
/// <summary>
/// Initializes a new instance of the <see cref="KESKeyStore"/> struct.
/// </summary>
/// <param name="keys">
/// <param name="bytes">
/// The keys to store.
/// </param>
public KESKeyStore(IEnumerable<byte[]> keys)
public KESKeyStore(byte[] bytes)
{
Keys = keys;
keystoreLength = Keys.Count();

Fingerprint = new MD5CryptoServiceProvider().ComputeHash(YieldKeys(keys).ToArray());
}
Raw = bytes;
Keys = Raw.Chunk(32);
count = Keys.Count();

static IEnumerable<byte> YieldKeys(IEnumerable<byte[]> keys)
{
foreach (var key in keys)
{
foreach (var value in key)
{
yield return value;
}
}
Fingerprint = new MD5CryptoServiceProvider().ComputeHash(Raw);
}

/// <summary>
Expand All @@ -80,49 +82,58 @@ static IEnumerable<byte> YieldKeys(IEnumerable<byte[]> keys)
/// <returns>
/// The new <see cref="KESKeyStore"/> instance.
/// </returns>
public static KESKeyStore Generate(int count = 128)
public static KESKeyStore Generate(int count = 0)
{
byte[][] keys = new byte[count][];
if (count <= 0)
{
count = GetRandomOddNumber();
}

List<byte[]> result = new List<byte[]>();
int tries = 0;

for (int i = 0; i < count; i++)
{
keys[i] = GetRandomKey(32);
var k = GetRandomKey(32);

// Ignore kets with 10 or more duplicated items.
if (result.All((b) => b.Intersect(k).Count() < 10) || tries > 100)
{
result.Add(k);
tries = 0;
}
else
{
tries++;
i--;
}
}

return new KESKeyStore(keys);
return new KESKeyStore(result.SelectMany((k) => k).ToArray());
}

private static byte[] GetRandomKey(int length)
/// <summary>
/// Returns a random odd number between 257 and 2047.
/// </summary>
/// <returns></returns>
public static int GetRandomOddNumber()
{
byte[] buffer = new byte[length];

if (random == null)
int count = random.Next(257, 2047);
if (count % 2 == 0)
{
random = new Random();
count++;
}

random.NextBytes(buffer);

return buffer;
return count;
}

/// <inheritdoc/>
public override string ToString()
private static byte[] GetRandomKey(int length)
{
return string.Join(";", Keys.Select(x => Convert.ToBase64String(x)));
}
byte[] buffer = new byte[length];

/// <summary>
/// Converts a string to a <see cref="KESKeyStore"/> instance.
/// </summary>
/// <param name="s">
/// The string to convert.
/// </param>
/// <returns>
/// The <see cref="KESKeyStore"/> instance.
/// </returns>
public static KESKeyStore FromString(string s)
{
return new KESKeyStore(s.Trim(new char[] { '\n', '\r', '\0' }).Split(';').Select(x => Convert.FromBase64String(x)));
random.NextBytes(buffer);

return buffer;
}
}
}
11 changes: 8 additions & 3 deletions src/Kryptor/KESProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ public class KESProvider
const int DecBlockSize = 1048576;
const int EncBlockSize = (DecBlockSize / DecChunkSize - 1) * EncChunkSize;

static byte[] HeaderPattern = new byte[] { 59, 197, 2, 46, 83 };
static readonly byte[] HeaderPattern = new byte[] { 59, 197, 2, 46, 83 };

/// <summary>
/// Gets an empty provider.
/// </summary>
static public KESProvider Empty { get; }

/// <summary>
/// Delegate for OnProgress event.
Expand Down Expand Up @@ -225,7 +230,7 @@ public async Task<byte[]> EncryptBlockAsync(byte[] bytes)

List<byte> result = new List<byte>(bytes.Sha256());

foreach (var chunk in bytes.Slice<byte>(EncChunkSize))
foreach (var chunk in bytes.Chunk(EncChunkSize))
{
result.AddRange(await AESEncryptProvider.EncryptAsync(chunk, keystore[index++]));
}
Expand Down Expand Up @@ -258,7 +263,7 @@ public async Task<byte[]> DecryptBlockAsync(byte[] bytes)
throw new ArgumentException($"Max allowed size for input buffer is :{DecryptionBlockSize}");
}

var chunks = bytes.Slice<byte>(DecChunkSize).ToArray();
var chunks = bytes.Chunk(DecChunkSize).ToArray();
var hash = chunks[0];

List<byte> result = new List<byte>();
Expand Down
1 change: 1 addition & 0 deletions src/Kryptor/Kryptor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.133" PrivateAssets="All" />
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion src/Kryptor/version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.5-alpha",
"version": "0.6-alpha",
"pathFilters": ["."],
"publicReleaseRefSpec": [
"^refs/heads/master$", // we release out of master
Expand Down
Loading

0 comments on commit e3577af

Please sign in to comment.