Skip to content

Instantly share code, notes, and snippets.

@ctigeek
Last active February 4, 2018 23:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ctigeek/3826731d0db661f224ab015463f18c30 to your computer and use it in GitHub Desktop.
Save ctigeek/3826731d0db661f224ab015463f18c30 to your computer and use it in GitHub Desktop.
Save name-object pairs securely.
public static class Config
{
public const int MaxValueLength = 7990; //If you change the size of the [secret] column then this needs to be changed.
public static string ConnectionString { get; set; }
public static string AesKey { get; set; }
public static int SecretPaddingSize { get; set; } = 1000; //e.g. any secret less than 1000 bytes will be padded so that it is at least 1000 bytes. This is to prevent information leaking regarding the size of the secret. Set to 0 to disable padding.
internal static readonly Lazy<byte[]> HashKeyLazy = new Lazy<byte[]>(() =>
{
using (var sha512 = new SHA512Managed())
{
if (AesKey == null) throw new InvalidOperationException("AesKey has not been set.");
return sha512.ComputeHash(Convert.FromBase64String(AesKey));
}
});
}
-- The code does not require Sql Server... use whatever db you like.
CREATE TABLE [secret1].[secrets](
[identifier] [varchar](500) NOT NULL,
[IV] [binary](16) NOT NULL,
[secret] [varbinary](8000) NOT NULL,
[hash] [varbinary] (500) NOT NULL,
CONSTRAINT [PK_secret1] PRIMARY KEY CLUSTERED
( [identifier] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
public class SecureRepo
{
public string GetString(string name)
{
var bytes = GetBytes(name);
return bytes == null ? null : Encoding.UTF8.GetString(bytes);
}
public void SaveString(string name, string secret)
{
if (secret == null)
{
throw new ArgumentNullException(nameof(secret));
}
SaveBytes(name, Encoding.UTF8.GetBytes(secret));
}
public void Delete(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var hash = HashName(name);
var conn = new SqlConnection(Config.ConnectionString);
using (conn)
{
conn.Open();
var cmd = new SqlCommand("delete secret1.secrets where identifier = @identifier;", conn);
cmd.Parameters.AddWithValue("identifier", hash);
cmd.ExecuteNonQuery();
}
}
public void SaveBytes(string name, byte[] secret)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (secret == null)
{
throw new ArgumentNullException(nameof(secret));
}
if (secret.Length > Config.MaxValueLength)
{
throw new ArgumentException("Value is too big. Max length is " + Config.MaxValueLength + " bytes.");
}
byte[] IV;
var encryptedBytes = EncryptValue(secret, out IV);
var encryptedHash = HmacHash(encryptedBytes);
var nameHash = HashName(name);
var conn = new SqlConnection(Config.ConnectionString);
using (conn)
{
conn.Open();
var cmd = new SqlCommand("update secret1.secrets set secret = @secret, IV=@IV, hash=@hash where identifier = @identifier;", conn);
cmd.Parameters.AddWithValue("identifier", nameHash);
cmd.Parameters.AddWithValue("secret", encryptedBytes);
cmd.Parameters.AddWithValue("hash", encryptedHash);
cmd.Parameters.Add("IV", SqlDbType.Binary).Value = IV;
var count = cmd.ExecuteNonQuery();
if (count == 1) return;
cmd.Dispose();
cmd = new SqlCommand("insert into secret1.secrets (identifier,IV,secret,hash) values (@identifier, @IV, @secret,@hash);", conn);
cmd.Parameters.AddWithValue("identifier", nameHash);
cmd.Parameters.AddWithValue("secret", encryptedBytes);
cmd.Parameters.AddWithValue("hash", encryptedHash);
cmd.Parameters.Add("IV", SqlDbType.Binary).Value = IV;
cmd.ExecuteNonQuery();
}
}
public byte[] GetBytes(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var hashedName = HashName(name);
var conn = new SqlConnection(Config.ConnectionString);
using (conn)
{
conn.Open();
var cmd = new SqlCommand("select IV,secret,hash from secret1.secrets where identifier = @identifier;", conn);
cmd.Parameters.AddWithValue("identifier", hashedName);
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
var secret = reader["secret"] as byte[];
var IV = reader["IV"] as byte[];
var hash = reader["hash"] as byte[];
if (!ValidateSecretHash(secret, hash))
{
throw new CryptographicException("Hash does not match for id " + hashedName);
}
var decrypted = DecryptValue(secret, IV);
return decrypted;
}
}
}
return null;
}
private string HashName(string name)
{
return Convert.ToBase64String(HmacHash(Encoding.UTF8.GetBytes(name)));
}
private byte[] HmacHash(byte[] hashThis)
{
using (var sha2 = new HMACSHA512(Config.HashKeyLazy.Value))
{
return sha2.ComputeHash(hashThis);
}
}
private bool ValidateSecretHash(byte[] secret, byte[] hash)
{
var validateThisHash = HmacHash(secret);
return validateThisHash.SequenceEqual(hash);
}
private byte[] DecryptValue(byte[] value, byte[] IV)
{
using (var aes = CreateAes(IV))
{
using (var memStream = new MemoryStream())
using (var encryptor = aes.CreateDecryptor())
using (var cryptoStream = new CryptoStream(memStream, encryptor, CryptoStreamMode.Write))
{
cryptoStream.Write(value, 0, value.Length);
cryptoStream.FlushFinalBlock();
cryptoStream.Close();
cryptoStream.Dispose();
return UnpadValue(memStream.ToArray());
}
}
}
private byte[] EncryptValue(byte[] value, out byte[] IV)
{
var padded = PadValue(value);
using (var aes = CreateAes(null))
{
IV = aes.IV;
using (var memStream = new MemoryStream())
using (var encryptor = aes.CreateEncryptor())
using (var cryptoStream = new CryptoStream(memStream, encryptor, CryptoStreamMode.Write))
{
cryptoStream.Write(padded, 0, padded.Length);
cryptoStream.FlushFinalBlock();
cryptoStream.Close();
cryptoStream.Dispose();
return memStream.ToArray();
}
}
}
private AesManaged CreateAes(byte[] IV)
{
var aes = new AesManaged();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.ISO10126;
aes.BlockSize = 128;
aes.Key = Convert.FromBase64String(Config.AesKey);
if (IV != null)
{
aes.IV = IV;
}
return aes;
}
private byte[] PadValue(byte[] value)
{
var newLength = (value.Length > Config.SecretPaddingSize ? value.Length : Config.SecretPaddingSize) + 2;
var dest = new byte[newLength];
Array.Copy(value, dest, value.Length);
var padLength = BitConverter.GetBytes((ushort)(newLength - value.Length));
dest[newLength - 2] = padLength[0];
dest[newLength - 1] = padLength[1];
return dest;
}
private byte[] UnpadValue(byte[] value)
{
var padding = BitConverter.ToUInt16(value, value.Length - 2);
var dest = new byte[value.Length - padding];
Array.Copy(value, dest, dest.Length);
return dest;
}
}
[TestFixture]
public class RepoTests
{
[SetUp]
public void Setup()
{
Config.ConnectionString = "server=localhost;database=Secrets;Trusted_Connection=True;";
Config.AesKey = "lKDEmf8UC48c3a3iNS1O/CvKPMOuHazcZfvSN/7r4yU=";
}
[Test]
public void Test1()
{
var repo = new SecureRepo();
repo.SaveString("name", "some value");
var result = repo.GetString("name");
Console.WriteLine(result);
}
[Test]
public void DeleteTest()
{
var repo = new SecureRepo();
repo.Delete("name");
}
[Test]
public void CreateAesKey()
{
var aes = new AesManaged();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.Zeros;
aes.BlockSize = 128;
aes.KeySize = 256;
aes.GenerateKey();
Console.WriteLine(Convert.ToBase64String(aes.Key));
Console.WriteLine(Convert.ToBase64String(aes.IV));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment