You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
156 lines
4.8 KiB
156 lines
4.8 KiB
1 year ago
|
using System.Runtime.InteropServices;
|
||
1 year ago
|
using System.Security;
|
||
|
using System.Security.Cryptography;
|
||
|
using System.Text.Json;
|
||
1 year ago
|
using DeviceId;
|
||
1 year ago
|
|
||
1 year ago
|
namespace StabilityMatrix.Core.Models;
|
||
1 year ago
|
|
||
|
internal record struct KeyInfo(byte[] Key, byte[] Salt, int Iterations);
|
||
|
|
||
|
/// <summary>
|
||
1 year ago
|
/// Encrypted MessagePack Serializer that uses a global key derived from the computer's SID.
|
||
|
/// Header contains additional random entropy as a salt that is used in decryption.
|
||
1 year ago
|
/// </summary>
|
||
1 year ago
|
public static class GlobalEncryptedSerializer
|
||
1 year ago
|
{
|
||
|
private const int KeySize = 32;
|
||
|
private const int Iterations = 300;
|
||
|
private const int SaltSize = 16;
|
||
|
|
||
1 year ago
|
public static T Deserialize<T>(ReadOnlySpan<byte> data)
|
||
|
{
|
||
|
// Get salt from start of file
|
||
|
var salt = data[..SaltSize].ToArray();
|
||
|
// Get encrypted json from rest of file
|
||
|
var encryptedJson = data[SaltSize..];
|
||
1 year ago
|
|
||
1 year ago
|
var json = DecryptBytes(encryptedJson, salt);
|
||
|
|
||
|
return JsonSerializer.Deserialize<T>(json)
|
||
|
?? throw new Exception("Deserialize returned null");
|
||
|
}
|
||
1 year ago
|
|
||
1 year ago
|
public static byte[] Serialize<T>(T obj)
|
||
|
{
|
||
|
var json = JsonSerializer.SerializeToUtf8Bytes(obj);
|
||
|
var (encrypted, salt) = EncryptBytes(json);
|
||
|
// Prepend salt to encrypted json
|
||
|
var fileBytes = salt.Concat(encrypted).ToArray();
|
||
|
|
||
|
return fileBytes;
|
||
|
}
|
||
|
|
||
1 year ago
|
private static string? GetComputerSid()
|
||
1 year ago
|
{
|
||
1 year ago
|
var deviceId = new DeviceIdBuilder()
|
||
|
.AddMachineName()
|
||
|
.AddOsVersion()
|
||
1 year ago
|
.OnWindows(
|
||
|
windows =>
|
||
|
windows
|
||
|
.AddProcessorId()
|
||
|
.AddMotherboardSerialNumber()
|
||
|
.AddSystemDriveSerialNumber()
|
||
|
)
|
||
|
.OnLinux(linux => linux.AddMotherboardSerialNumber().AddSystemDriveSerialNumber())
|
||
|
.OnMac(mac => mac.AddSystemDriveSerialNumber().AddPlatformSerialNumber())
|
||
1 year ago
|
.ToString();
|
||
|
|
||
|
return deviceId;
|
||
1 year ago
|
}
|
||
1 year ago
|
|
||
1 year ago
|
private static SecureString GetComputerKeyPhrase()
|
||
|
{
|
||
1 year ago
|
var keySource = GetComputerSid();
|
||
1 year ago
|
// If no sid, use username as fallback
|
||
|
keySource ??= Environment.UserName;
|
||
|
|
||
|
// XOR with fixed constant
|
||
|
const string keyPhrase = "StabilityMatrix";
|
||
|
var result = new SecureString();
|
||
|
|
||
|
for (var i = 0; i < keySource.Length; i++)
|
||
|
{
|
||
|
result.AppendChar((char)(keySource[i] ^ keyPhrase[i % keyPhrase.Length]));
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
1 year ago
|
private static KeyInfo DeriveKeyWithSalt(
|
||
|
SecureString password,
|
||
|
int saltLength,
|
||
|
int iterations,
|
||
|
int keyLength
|
||
|
)
|
||
1 year ago
|
{
|
||
|
var salt = RandomNumberGenerator.GetBytes(saltLength);
|
||
|
var key = DeriveKey(password, salt, iterations, keyLength);
|
||
|
return new KeyInfo(key, salt, iterations);
|
||
|
}
|
||
|
|
||
1 year ago
|
private static byte[] DeriveKey(
|
||
|
SecureString password,
|
||
|
byte[] salt,
|
||
|
int iterations,
|
||
|
int keyLength
|
||
|
)
|
||
1 year ago
|
{
|
||
|
var ptr = Marshal.SecureStringToBSTR(password);
|
||
|
try
|
||
|
{
|
||
|
var length = Marshal.ReadInt32(ptr, -4);
|
||
|
var passwordByteArray = new byte[length];
|
||
|
var handle = GCHandle.Alloc(passwordByteArray, GCHandleType.Pinned);
|
||
|
try
|
||
|
{
|
||
|
for (var i = 0; i < length; i++)
|
||
|
{
|
||
|
passwordByteArray[i] = Marshal.ReadByte(ptr, i);
|
||
|
}
|
||
1 year ago
|
|
||
|
using var rfc2898 = new Rfc2898DeriveBytes(passwordByteArray, salt, iterations, HashAlgorithmName.SHA512);
|
||
1 year ago
|
return rfc2898.GetBytes(keyLength);
|
||
|
}
|
||
|
finally
|
||
|
{
|
||
|
Array.Clear(passwordByteArray, 0, passwordByteArray.Length);
|
||
|
handle.Free();
|
||
|
}
|
||
|
}
|
||
|
finally
|
||
|
{
|
||
|
Marshal.ZeroFreeBSTR(ptr);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static (byte[], byte[]) EncryptBytes(byte[] data)
|
||
|
{
|
||
1 year ago
|
var keyInfo = DeriveKeyWithSalt(GetComputerKeyPhrase(), SaltSize, Iterations, KeySize);
|
||
|
|
||
1 year ago
|
using var aes = Aes.Create();
|
||
|
aes.Key = keyInfo.Key;
|
||
|
aes.IV = keyInfo.Salt;
|
||
|
aes.Padding = PaddingMode.PKCS7;
|
||
|
aes.Mode = CipherMode.CBC;
|
||
|
|
||
|
var transform = aes.CreateEncryptor();
|
||
|
return (transform.TransformFinalBlock(data, 0, data.Length), keyInfo.Salt);
|
||
|
}
|
||
1 year ago
|
|
||
1 year ago
|
private static byte[] DecryptBytes(ReadOnlySpan<byte> encryptedData, byte[] salt)
|
||
1 year ago
|
{
|
||
1 year ago
|
var key = DeriveKey(GetComputerKeyPhrase(), salt, Iterations, KeySize);
|
||
1 year ago
|
|
||
|
using var aes = Aes.Create();
|
||
|
aes.Key = key;
|
||
|
aes.IV = salt;
|
||
|
aes.Padding = PaddingMode.PKCS7;
|
||
|
aes.Mode = CipherMode.CBC;
|
||
|
|
||
|
var transform = aes.CreateDecryptor();
|
||
1 year ago
|
return transform.TransformFinalBlock(encryptedData.ToArray(), 0, encryptedData.Length);
|
||
1 year ago
|
}
|
||
|
}
|