初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Utils.Encryption;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using PlainToolkit.CngProtectedData;
|
||||
using DataProtectionScope = System.Security.Cryptography.DataProtectionScope;
|
||||
using CngDataProtectionScope = PlainToolkit.CngProtectedData.DataProtectionScope;
|
||||
|
||||
|
||||
namespace PCL.Core.Utils.Secret;
|
||||
|
||||
public static class EncryptHelper
|
||||
{
|
||||
private static readonly byte[] Key = "PCL CE Encryption Key"u8.ToArray();
|
||||
public static (IEncryptionProvider Provider, uint Version) DefaultProvider => _DefaultProvider.Value;
|
||||
private static readonly Lazy<(IEncryptionProvider Provider, uint Version)> _DefaultProvider = new(_SelectBestEncryption);
|
||||
|
||||
private static (IEncryptionProvider Provider, uint Version) _SelectBestEncryption()
|
||||
{
|
||||
var aesHardwareSupport = System.Runtime.Intrinsics.X86.Aes.IsSupported ||
|
||||
System.Runtime.Intrinsics.Arm.Aes.IsSupported;
|
||||
if (aesHardwareSupport && AesGcmProvider.Instance.IsSupported) return (AesGcmProvider.Instance, 2);
|
||||
if (ChaCha20Poly1305Provider.Instance.IsSupported) return (ChaCha20Poly1305Provider.Instance, 1);
|
||||
return (ChaCha20SoftwareProvider.Instance, 0);
|
||||
}
|
||||
|
||||
public static string SecretEncrypt(string? data)
|
||||
{
|
||||
if (data.IsNullOrEmpty()) return string.Empty;
|
||||
var rawData = Encoding.UTF8.GetBytes(data);
|
||||
|
||||
return Convert.ToBase64String(EncryptionData.ToBytes(new EncryptionData
|
||||
{ Version = DefaultProvider.Version, Data = DefaultProvider.Provider.Encrypt(rawData, EncryptionKey) }));
|
||||
}
|
||||
|
||||
public static string SecretDecrypt(string? data)
|
||||
{
|
||||
if (data.IsNullOrEmpty()) return string.Empty;
|
||||
var rawData = Convert.FromBase64String(data);
|
||||
Exception? decryptError;
|
||||
if (EncryptionData.IsValid(rawData))
|
||||
{
|
||||
try
|
||||
{
|
||||
var encryptionData = EncryptionData.FromBytes(rawData);
|
||||
IEncryptionProvider provider = encryptionData.Version switch
|
||||
{
|
||||
0 => ChaCha20SoftwareProvider.Instance,
|
||||
1 => ChaCha20Poly1305Provider.Instance,
|
||||
2 => AesGcmProvider.Instance,
|
||||
_ => throw new NotSupportedException("Unsupported encryption version")
|
||||
};
|
||||
var decryptedData = provider.Decrypt(encryptionData.Data, EncryptionKey);
|
||||
return Encoding.UTF8.GetString(decryptedData);
|
||||
}
|
||||
catch (Exception ex) { decryptError = ex; }
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
#pragma warning disable CS0612,CS0618 // Type or member is obsolete
|
||||
var decryptedData = AesCbcProvider.Instance.Decrypt(rawData, Encoding.UTF8.GetBytes(IdentifyOld.EncryptKey));
|
||||
#pragma warning restore CS0612,CS0618 // Type or member is obsolete
|
||||
return Encoding.UTF8.GetString(decryptedData);
|
||||
}
|
||||
catch (Exception ex) { decryptError = ex; }
|
||||
}
|
||||
|
||||
throw new Exception($"Unknown Encryption data, the data may broken", decryptError);
|
||||
}
|
||||
|
||||
#region "加密存储信息数据"
|
||||
|
||||
|
||||
public struct EncryptionData
|
||||
{
|
||||
public uint Version;
|
||||
public byte[] Data;
|
||||
|
||||
private const uint MagicNumber = 0x454E4321;
|
||||
|
||||
public static EncryptionData FromBase64(string base64)
|
||||
{
|
||||
return FromBytes(Convert.FromBase64String(base64));
|
||||
}
|
||||
|
||||
public static EncryptionData FromBytes(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
// 0 - 4 MagicNumber | 4 - 8 version || 8 - 12 bytes rData length | n bytes rData
|
||||
if (bytes.Length < 12)
|
||||
throw new ArgumentException("No enough data for EncryptionData", nameof(bytes));
|
||||
|
||||
if (BinaryPrimitives.ReadUInt32BigEndian(bytes[..4]) != MagicNumber)
|
||||
throw new ArgumentException("Unknown data for EncryptionData", nameof(bytes));
|
||||
|
||||
var dataLength = BinaryPrimitives.ReadInt32BigEndian(bytes[8..12]);
|
||||
if (dataLength > bytes.Length - 12)
|
||||
throw new ArgumentException("No enough data for EncryptionData", nameof(bytes));
|
||||
if (dataLength < 0)
|
||||
throw new ArgumentException("Invalid data length for EncryptionData", nameof(bytes));
|
||||
|
||||
var rData = bytes[12..(12 + dataLength)];
|
||||
|
||||
return new EncryptionData
|
||||
{
|
||||
Version = BinaryPrimitives.ReadUInt32BigEndian(bytes[4..8]),
|
||||
Data = rData.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public static byte[] ToBytes(EncryptionData encryptionData)
|
||||
{
|
||||
var length = 12 + encryptionData.Data.Length;
|
||||
var bytes = new byte[length];
|
||||
var bytesSpan = bytes.AsSpan();
|
||||
BinaryPrimitives.WriteUInt32BigEndian(bytesSpan[..4], MagicNumber);
|
||||
BinaryPrimitives.WriteUInt32BigEndian(bytesSpan[4..8], encryptionData.Version);
|
||||
BinaryPrimitives.WriteInt32BigEndian(bytesSpan[8..12], encryptionData.Data.Length);
|
||||
encryptionData.Data.CopyTo(bytesSpan[12..]);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static bool IsValid(ReadOnlySpan<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return data.Length >= 12 && BinaryPrimitives.ReadUInt32BigEndian(data[..4]) == MagicNumber;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region "密钥存储和获取"
|
||||
|
||||
internal static byte[] EncryptionKey { get => _EncryptionKey.Value; }
|
||||
private static readonly Lazy<byte[]> _EncryptionKey = new(_GetKey);
|
||||
|
||||
private static byte[] _GetKey()
|
||||
{
|
||||
var keyFile = Path.Combine(Paths.SharedData, "UserKey.bin");
|
||||
if (File.Exists(keyFile))
|
||||
{
|
||||
var buf = File.ReadAllBytes(keyFile);
|
||||
var data = EncryptionData.FromBytes(buf);
|
||||
return data.Version switch
|
||||
{
|
||||
1 => ProtectedData.Unprotect(data.Data, Key, DataProtectionScope.CurrentUser),
|
||||
2 => CngProtectedData.Unprotect(data.Data, Key, CngDataProtectionScope.CurrentUser),
|
||||
_ => throw new NotSupportedException("Unsupported key version")
|
||||
};
|
||||
}
|
||||
|
||||
var randomKey = new byte[32];
|
||||
RandomNumberGenerator.Fill(randomKey);
|
||||
var storeData = EncryptionData.ToBytes(new EncryptionData
|
||||
{
|
||||
Version = 2,
|
||||
Data = CngProtectedData.Protect(randomKey, Key, CngDataProtectionScope.CurrentUser)
|
||||
});
|
||||
|
||||
var tmpFile = $"{keyFile}.tmp{RandomUtils.NextInt(10000, 99999)}";
|
||||
using (var fs = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
|
||||
{
|
||||
fs.Write(storeData);
|
||||
fs.Flush(true);
|
||||
}
|
||||
|
||||
File.Move(tmpFile, keyFile, true);
|
||||
|
||||
return randomKey;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Management;
|
||||
using System.Text;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using PCL.Core.Utils.Hash;
|
||||
|
||||
namespace PCL.Core.Utils.Secret;
|
||||
|
||||
public class Identify
|
||||
{
|
||||
public static byte[] RawId { get => field ??= _GetRawId(); } = null!;
|
||||
public static string LauncherId { get => field ??= _getLauncherId(); } = null!;
|
||||
|
||||
private static byte[] _GetRawId()
|
||||
{
|
||||
var code = new StringBuilder();
|
||||
try
|
||||
{
|
||||
code.Append("UUID:").Append(_GetWmiProperty("Win32_ComputerSystemProduct", "UUID"))
|
||||
.Append("|MB_Prod:").Append(_GetWmiProperty("Win32_BaseBoard", "Product"))
|
||||
.Append("|MB_SN:").Append(_GetWmiProperty("Win32_BaseBoard", "SerialNumber"))
|
||||
.Append("|CPU:").Append(_GetWmiProperty("Win32_Processor", "ProcessorId"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", "获取设备基础信息失败");
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetBytes(SHA512Provider.Instance.ComputeHash(code.ToString()).ToHexString());
|
||||
}
|
||||
|
||||
private static string _GetWmiProperty(string className, string propertyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var searcher =
|
||||
new ManagementObjectSearcher($"SELECT {propertyName} FROM {className}");
|
||||
using var results = searcher.Get();
|
||||
foreach (var obj in results)
|
||||
{
|
||||
if (obj[propertyName] is not null)
|
||||
return (obj[propertyName].ToString() ?? string.Empty).Trim();
|
||||
}
|
||||
}
|
||||
catch { /* Ignore */ }
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string _getLauncherId()
|
||||
{
|
||||
try
|
||||
{
|
||||
var prefix = "PCL-CE|"u8.ToArray();
|
||||
var ctx = RawId;
|
||||
var suffix = "|LauncherId"u8.ToArray();
|
||||
|
||||
var buffer = new byte[prefix.Length + ctx.Length + suffix.Length];
|
||||
var bufferSpan = buffer.AsSpan();
|
||||
prefix.CopyTo(bufferSpan[..prefix.Length]);
|
||||
ctx.CopyTo(bufferSpan.Slice(prefix.Length, ctx.Length));
|
||||
suffix.CopyTo(bufferSpan.Slice(prefix.Length + ctx.Length, suffix.Length));
|
||||
|
||||
Array.Clear(ctx);
|
||||
var sample = SHA512Provider.Instance.ComputeHash(bufferSpan).ToHexString();
|
||||
bufferSpan.Clear();
|
||||
|
||||
// 16 in length, 8 bytes, 64 bits, enough for us
|
||||
return sample.Substring(64, 16)
|
||||
.ToUpper()
|
||||
.Insert(4, "-")
|
||||
.Insert(9, "-")
|
||||
.Insert(14, "-");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", "无法获取识别码");
|
||||
return "PCL2-CECE-GOOD-2025";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Management;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Hash;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Utils.Secret;
|
||||
|
||||
[Obsolete("Use PCL.Core.Utils.Secret.Identify instead")]
|
||||
public static class IdentifyOld
|
||||
{
|
||||
private const string DefaultRawCode = "B09675A9351CBD1FD568056781FE3966DD936CC9B94E51AB5CF67EEB7E74C075";
|
||||
private static readonly Lazy<string?> _LazyCpuId = new(_GetCpuId);
|
||||
|
||||
private static readonly Lazy<string> _LazyRawCode =
|
||||
new(() => CpuId is null ? DefaultRawCode : SHA256Provider.Instance.ComputeHash(CpuId).ToHexString().ToUpper());
|
||||
|
||||
private static readonly Lazy<string> _LaunchId = new(_GetLaunchId);
|
||||
|
||||
private static readonly Lazy<string> _LazyEncryptKey =
|
||||
new(() => SHA512Provider.Instance.ComputeHash(RawCode).ToHexString().Substring(4, 32).ToUpper());
|
||||
|
||||
public static string GetGuid() => Guid.NewGuid().ToString();
|
||||
[Obsolete]
|
||||
public static string? CpuId => _LazyCpuId.Value;
|
||||
[Obsolete]
|
||||
public static string RawCode => _LazyRawCode.Value;
|
||||
[Obsolete]
|
||||
public static string LaunchId => _LaunchId.Value;
|
||||
[Obsolete]
|
||||
public static string EncryptKey => _LazyEncryptKey.Value;
|
||||
|
||||
private static string? _GetCpuId()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor");
|
||||
using var collection = searcher.Get();
|
||||
|
||||
foreach (var item in collection)
|
||||
{
|
||||
try
|
||||
{
|
||||
return item["ProcessorId"]?.ToString();
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
{
|
||||
LogWrapper.Warn("Identify", $"WMI属性读取失败: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
LogWrapper.Warn("Identify", "未找到有效的CPU ID");
|
||||
return null;
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", $"WMI查询失败");
|
||||
}
|
||||
catch (System.Runtime.InteropServices.COMException ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", $"COM异常,请确保WMI服务正在运行");
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", "访问被拒绝,请以管理员权限运行");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", $"意外的系统异常");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetMachineId(string randomId)
|
||||
{
|
||||
return SHA512Provider.Instance.ComputeHash($"{randomId}|{CpuId}").ToHexString().ToUpper();
|
||||
}
|
||||
|
||||
private static string _GetLaunchId()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(States.System.LaunchUuid)) States.System.LaunchUuid = GetGuid();
|
||||
var hashCode = GetMachineId(States.System.LaunchUuid)
|
||||
.Substring(6, 16)
|
||||
.Insert(4, "-")
|
||||
.Insert(9, "-")
|
||||
.Insert(14, "-");
|
||||
return hashCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Identify", "无法获取短识别码");
|
||||
return "PCL2-CECE-GOOD-2025";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user