diff --git a/csplayready.sln b/csplayready.sln new file mode 100644 index 0000000..71ce3e6 --- /dev/null +++ b/csplayready.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csplayready", "csplayready\csplayready.csproj", "{8480D9A6-2F92-4913-BCF1-6DD2A0814457}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8480D9A6-2F92-4913-BCF1-6DD2A0814457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8480D9A6-2F92-4913-BCF1-6DD2A0814457}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8480D9A6-2F92-4913-BCF1-6DD2A0814457}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8480D9A6-2F92-4913-BCF1-6DD2A0814457}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/csplayready/Cdm.cs b/csplayready/Cdm.cs new file mode 100644 index 0000000..a478df1 --- /dev/null +++ b/csplayready/Cdm.cs @@ -0,0 +1,248 @@ +using System.Security.Cryptography; +using System.Text; +using System.Xml.Linq; +using csplayready.crypto; +using csplayready.device; +using csplayready.license; +using csplayready.system; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using ECPoint = Org.BouncyCastle.Math.EC.ECPoint; + +namespace csplayready; + +public class Cdm +{ + public static readonly int MaxNumOfSessions = 16; + + private static readonly byte[] RawWmrmEcc256PubKey = + "c8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562" + .HexToBytes(); + + private readonly ECPoint _wmrmEcc256PubKey; + + private readonly Dictionary _sessions = []; + + private CertificateChain _certificateChain; + private EccKey _encryptionKey; + private EccKey _signingKey; + + public Cdm(CertificateChain certificateChain, EccKey encryptionKey, EccKey signingKey) + { + _certificateChain = certificateChain; + _encryptionKey = encryptionKey; + _signingKey = signingKey; + + var curve = ECNamedCurveTable.GetByName("secp256r1").Curve; + _wmrmEcc256PubKey = curve.CreatePoint( + new BigInteger(1, RawWmrmEcc256PubKey[..32]), + new BigInteger(1, RawWmrmEcc256PubKey[32..]) + ); + } + + public static Cdm FromDevice(Device device) + { + return new Cdm(device.GroupCertificate!, device.EncryptionKey!, device.SigningKey!); + } + + public int Open() + { + if (_sessions.Count > MaxNumOfSessions) + throw new TooManySessions($"Too many Sessions open ({MaxNumOfSessions})."); + + var session = new Session(_sessions.Count + 1); + _sessions[session.Id] = session; + + return session.Id; + } + + public void Close(int sessionId) + { + if (!_sessions.Remove(sessionId)) + throw new InvalidSession($"Session identifier {sessionId} is invalid."); + } + + private byte[] GetKeyData(Session session) + { + (ECPoint point1, ECPoint point2) = Crypto.Ecc256Encrypt(session.XmlKey.GetPoint(), _wmrmEcc256PubKey); + return point1.ToBytes().Concat(point2.ToBytes()).ToArray(); + } + + private byte[] GetCipherData(Session session) + { + var b64Chain = Convert.ToBase64String(_certificateChain.Dumps()); + var body = $"{b64Chain}"; + + var ciphertext = Crypto.AesCbcEncrypt(session.XmlKey.AesKey, session.XmlKey.AesIv, Encoding.UTF8.GetBytes(body)); + return session.XmlKey.AesIv.Concat(ciphertext).ToArray(); + } + + private static string GetDigestContent(string wrmHeader, string nonce, string encryptedKey, string encryptedCert) + { + TimeSpan t = DateTime.UtcNow - new DateTime(1970, 1, 1); + var secondsSinceEpoch = (int)t.TotalSeconds; + + return + "" + + "1" + + $"{wrmHeader}" + + "" + + "10.0.16384.10011" + + "" + + $"{nonce}" + + $"{secondsSinceEpoch}" + + "" + + "" + + "" + + "" + + "" + + "" + + "WMRMServer" + + "" + + "" + + $"{encryptedKey}" + + "" + + "" + + "" + + "" + + $"{encryptedCert}" + + "" + + "" + + ""; + } + + private static string GetSignedInfo(string digestValue) + { + return + "" + + "" + + "" + + "" + + "" + + $"{digestValue}" + + "" + + ""; + } + + public string GetLicenseChallenge(int sessionId, string wrmHeader) + { + if (!_sessions.TryGetValue(sessionId, out Session? session)) + throw new InvalidSession($"Session identifier {sessionId} is invalid."); + + session.SigningKey = _signingKey; + session.EncryptionKey = _encryptionKey; + + SecureRandom secureRandom = new SecureRandom(); + + var randomBytes = new byte[16]; + secureRandom.NextBytes(randomBytes); + + var laContent = GetDigestContent( + wrmHeader, + Convert.ToBase64String(randomBytes), + Convert.ToBase64String(GetKeyData(session)), + Convert.ToBase64String(GetCipherData(session)) + ); + + var laHash = SHA256.HashData(Encoding.UTF8.GetBytes(laContent)); + var signedInfo = GetSignedInfo(Convert.ToBase64String(laHash)); + var signature = Crypto.Ecc256Sign(session.SigningKey.PrivateKey, Encoding.UTF8.GetBytes(signedInfo)); + + return + "" + + "" + + "" + + "" + + "" + + "" + + laContent + + "" + + signedInfo + + $"{Convert.ToBase64String(signature)}" + + "" + + "" + + "" + + $"{Convert.ToBase64String(session.SigningKey.PublicBytes())}" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + } + + private bool VerifyEncryptionKey(Session session, XmrLicense license) + { + var eccKeys = license.GetObject(42); + if (eccKeys == null) + throw new InvalidLicense("No ECC public key in license"); + + var encryptionKey = (byte[])eccKeys.First()["key"]; + return encryptionKey.SequenceEqual(session.EncryptionKey!.PublicBytes()); + } + + public void ParseLicense(int sessionId, string xmrLicense) + { + if (!_sessions.TryGetValue(sessionId, out Session? session)) + throw new InvalidSession($"Session identifier {sessionId} is invalid"); + + if (session.EncryptionKey == null || session.SigningKey == null) + throw new InvalidSession("Cannot parse a license message without first making a license request"); + + XDocument doc = XDocument.Parse(xmrLicense); + XNamespace drmNs = "http://schemas.microsoft.com/DRM/2007/03/protocols"; + + foreach (var b64License in doc.Descendants(drmNs + "License").Select(l => l.Value)) + { + var rawLicense = Convert.FromBase64String(b64License); + var license = XmrLicense.Loads(rawLicense); + + if (!VerifyEncryptionKey(session, license)) + throw new InvalidLicense("Public encryption key does not match"); + + var contentKeys = license.GetObject(10); + if (contentKeys == null) + throw new InvalidLicense("License does not contain any content keys"); + + foreach (var contentKey in contentKeys) + { + var keyId = (byte[])contentKey["key_id"]; + var keyType = (Key.KeyTypes)contentKey["key_type"]; + var cipherType = (Key.CipherTypes)contentKey["cipher_type"]; + var encryptedKey = (byte[])contentKey["encrypted_key"]; + + byte[] key; + byte[] integrityKey; + + switch (cipherType) + { + case Key.CipherTypes.Ecc256: + (ECPoint point1, ECPoint point2) = (Utils.FromBytes(encryptedKey[..64]), Utils.FromBytes(encryptedKey[64..])); + var decrypted = Crypto.Ecc256Decrypt(point1, point2, session.EncryptionKey.PrivateKey).ToBytes(); + integrityKey = decrypted[..16]; + key = decrypted[16..32]; + break; + default: + throw new InvalidLicense($"Cipher type {cipherType} is not supported"); + } + + if (!license.CheckSignature(integrityKey)) + throw new InvalidLicense("License integrity signature does not match"); + + session.Keys.Add(new Key(keyId, keyType, cipherType, key)); + } + } + } + + public List GetKeys(int sessionId) + { + if (!_sessions.TryGetValue(sessionId, out Session? session)) + throw new InvalidSession($"Session identifier {sessionId} is invalid"); + + return session.Keys; + } +} \ No newline at end of file diff --git a/csplayready/Exceptions.cs b/csplayready/Exceptions.cs new file mode 100644 index 0000000..fbdee55 --- /dev/null +++ b/csplayready/Exceptions.cs @@ -0,0 +1,38 @@ +namespace csplayready; + + +public class CsPlayreadyException : Exception +{ + public CsPlayreadyException(string message) : base(message) { } + public CsPlayreadyException(string message, Exception innerException) : base(message, innerException) { } +} + +public class InvalidCertificate : CsPlayreadyException +{ + public InvalidCertificate(string message) : base(message) { } + public InvalidCertificate(string message, Exception innerException) : base(message, innerException) { } +} + +public class InvalidCertificateChain : CsPlayreadyException +{ + public InvalidCertificateChain(string message) : base(message) { } + public InvalidCertificateChain(string message, Exception innerException) : base(message, innerException) { } +} + +public class TooManySessions : CsPlayreadyException +{ + public TooManySessions(string message) : base(message) { } + public TooManySessions(string message, Exception innerException) : base(message, innerException) { } +} + +public class InvalidSession : CsPlayreadyException +{ + public InvalidSession(string message) : base(message) { } + public InvalidSession(string message, Exception innerException) : base(message, innerException) { } +} + +public class InvalidLicense : CsPlayreadyException +{ + public InvalidLicense(string message) : base(message) { } + public InvalidLicense(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/csplayready/Program.cs b/csplayready/Program.cs new file mode 100644 index 0000000..7227c7a --- /dev/null +++ b/csplayready/Program.cs @@ -0,0 +1,150 @@ +using System.Text; +using System.Xml.Linq; +using csplayready.device; +using csplayready.system; + +namespace csplayready; + +class Program +{ + private static readonly XNamespace DrmNs = "http://schemas.microsoft.com/DRM/2007/03/protocols"; + + public static IEnumerable GetAllLicenses(XDocument doc) + { + return doc.Descendants(DrmNs + "License").Select(l => l.Value); + } + + public static void Main(string[] args) + { + + /*using Org.BouncyCastle.Math; + using Org.BouncyCastle.Math.EC; + using Org.BouncyCastle.Utilities.Encoders;*/ + /*EccKey key = EccKey.Generate(); + Console.WriteLine("Private key: " + key.PrivateKey); + + EccKey messagePoint = EccKey.Generate(); + Console.WriteLine("plaintext: " + messagePoint.PublicBytes().ToHex()); + + (ECPoint e1, ECPoint e2) = Crypto.Ecc256Encrypt(messagePoint.PublicKey, key.PublicKey); + Console.WriteLine("encrypted 1: " + e1.XCoord.ToBigInteger() + " " + e1.YCoord.ToBigInteger()); + Console.WriteLine("encrypted 2: " + e2.XCoord.ToBigInteger() + " " + e2.YCoord.ToBigInteger()); + + var bytes = e1.ToBytes().Concat(e2.ToBytes()).ToArray(); + Console.WriteLine("encrypted: " + bytes.ToHex()); + + var curve = ECNamedCurveTable.GetByName("secp256r1"); + var domainParams = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H); + + var l1 = Utils.FromBytes(bytes[..64], domainParams); + var l2 = Utils.FromBytes(bytes[64..], domainParams); + + ECPoint decryptedE = Crypto.Ecc256Decrypt(e1, e2, key.PrivateKey); + Console.WriteLine("decrypted e: " + decryptedE.ToBytes().ToHex()); + + ECPoint decryptedL = Crypto.Ecc256Decrypt(l1, l2, key.PrivateKey); + Console.WriteLine("decrypted l: " + decryptedL.ToBytes().ToHex());*/ + /* + using constructcs; + using static constructcs.ParserBuilder; + + string text = "AAADfHBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAA1xcAwAAAQABAFIDPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4" + + "AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC" + + "8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0A" + + "C4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAx" + + "ADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgB" + + "PAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgA0AFIAcABsAGIAKwBUAGIATgBFAFMAOAB0AEcAawBOAEYAVwBUAEUASA" + + "BBAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEsATABqADMAUQB6AFEAUAAvAE4AQQA9ADwALwBDAEgAR" + + "QBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcABzADoALwAvAHAAcgBvAGYAZgBpAGMAaQBhAGwAcwBpAHQA" + + "ZQAuAGsAZQB5AGQAZQBsAGkAdgBlAHIAeQAuAG0AZQBkAGkAYQBzAGUAcgB2AGkAYwBlAHMALgB3AGkAbgBkAG8AdwBzAC4" + + "AbgBlAHQALwBQAGwAYQB5AFIAZQBhAGQAeQAvADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAE" + + "IAVQBUAEUAUwA+ADwASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA4AC4AMQAuADIAMwAwADQALgAzADEAPAAvA" + + "EkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4APAAvAEMAVQBTAFQATwBNAEEAVABUAFIASQBCAFUAVABFAFMAPgA8" + + "AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA=="; + + var pssh = new Pssh(text); + Console.WriteLine(string.Join(", ", pssh.GetWrmHeaders()));*/ + /* + using csplayready.license; + + const string licenseString = "WE1SAAAAAANtHSY9EpvluoRHZaggdNEeAAMAAQAAAaYAAwACAAAAMgABAA0AAAAKAAEAAAAzAAAACgABAAEAMgAAAAwAAABMAAEANAAAAAoH0AACAAQAAABeAAEABQAAABIBkAEOAJYAZABkAAMABwAAACQAAQAIAAAAHJGhg9eD4K9Lstrmn5ELN3JA7wcAAAIANgAAACAAAAA5AAAAGB/ZIbbM7TVAjUvccXYNQ+kAAwAJAAAA8gABAAoAAACeHl06aWdatUKc9+vZTOADAQABAAMAgFpnAzpVEpVCWcpDHRv8K7dVTfDu1KVeLfpb4kvFWbD9hcNEDSpse946LHZRYsFw19sPnhs5sOnJe+Q/zy4EoX+BG9zZc6WCetrPhb/vKC2tGvwJrCqHFUE5DM82g5WjIV96cf61OQtSLMvrIT0dJmIV5YKfi5RTeAAb2kOj+AE7AAAAKgAAAEwAAQBA8yyUn9LQzBQonmbYcnuUQ3iZMVxdjP3VDDi5goFt3ofTWrFdOT4MXi0YKUE4G/zk8Xp6gPHkJjG8XKsM6mTbPQABAAsAAAAcAAEAELeiTV1WtdIiQPmFZnF1JN4="; + + var data = Convert.FromBase64String(licenseString); + var license = XmrLicense.Loads(data); + + Utils.PrintObject(license.GetObject(10));*/ + /* + using csplayready.crypto; + using csplayready.device; + using csplayready.system; + using Org.BouncyCastle.Security; + + var encryptionKey = EccKey.Load(@"C:\Users\titus\RiderProjects\csplayready\csplayready\hisense\encr.dat"); + var signingKey = EccKey.Load(@"C:\Users\titus\RiderProjects\csplayready\csplayready\hisense\sig.dat"); + + var groupKey = EccKey.Load(@"C:\Users\titus\RiderProjects\csplayready\csplayready\hisense\zgpriv.dat"); + var certificateChain = CertificateChain.Load(@"C:\Users\titus\RiderProjects\csplayready\csplayready\hisense\bgroupcert.dat"); + + if (!certificateChain.Get(0).GetIssuerKey()!.SequenceEqual(groupKey.PublicBytes())) + throw new InvalidCertificateChain("Group key does not match this certificate"); + + var random = new SecureRandom(); + var certId = random.GenerateSeed(16); + var clientId = random.GenerateSeed(16); + + var leafCert = Certificate.NewLeafCertificate(certId, (uint)certificateChain.GetSecurityLevel()!, clientId, signingKey, encryptionKey, groupKey, certificateChain); + certificateChain.Prepend(leafCert); + + Console.WriteLine("Valid: " + certificateChain.Verify()); + + var device = new Device(3, groupKey, encryptionKey, signingKey, certificateChain); + device.Dump("fourth_cs_device.prd");*/ + + // TODO: + // + make Utils class better + // + more exceptions + // + cli tool + + var device = Device.Load(args[0]); + var cdm = Cdm.FromDevice(device); + var sessionId = cdm.Open(); + + var pssh = new Pssh( + "AAADfHBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAA1xcAwAAAQABAFIDPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4Ac" + + "wA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANw" + + "AvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA" + + "8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABB" + + "AEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgA0A" + + "FIAcABsAGIAKwBUAGIATgBFAFMAOAB0AEcAawBOAEYAVwBUAEUASABBAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AE" + + "sATABqADMAUQB6AFEAUAAvAE4AQQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcABzADoALwAvAHA" + + "AcgBvAGYAZgBpAGMAaQBhAGwAcwBpAHQAZQAuAGsAZQB5AGQAZQBsAGkAdgBlAHIAeQAuAG0AZQBkAGkAYQBzAGUAcgB2AGkAYwBlAHMA" + + "LgB3AGkAbgBkAG8AdwBzAC4AbgBlAHQALwBQAGwAYQB5AFIAZQBhAGQAeQAvADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQ" + + "QBUAFQAUgBJAEIAVQBUAEUAUwA+ADwASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA4AC4AMQAuADIAMwAwADQALgAzADEAPA" + + "AvAEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4APAAvAEMAVQBTAFQATwBNAEEAVABUAFIASQBCAFUAVABFAFMAPgA8AC8ARAB" + + "BAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA=="); + + var wrmHeaders = pssh.GetWrmHeaders(); + var challenge = cdm.GetLicenseChallenge(sessionId, wrmHeaders[0]); + + Console.WriteLine(challenge); + + using HttpClient client = new HttpClient(); + const string url = "https://test.playready.microsoft.com/service/rightsmanager.asmx?cfg=(persist:false,sl:2000)"; + var content = new StringContent(challenge, Encoding.UTF8, "text/xml"); + + HttpResponseMessage response = client.PostAsync(url, content).Result; + response.EnsureSuccessStatusCode(); + var responseBody = response.Content.ReadAsStringAsync().Result; + + Console.WriteLine(responseBody); + + cdm.ParseLicense(sessionId, responseBody); + + foreach (var key in cdm.GetKeys(sessionId)) + { + Console.WriteLine($"{key.KeyId.ToHex()}:{key.RawKey.ToHex()}"); + } + + cdm.Close(sessionId); + } +} diff --git a/csplayready/Utils.cs b/csplayready/Utils.cs new file mode 100644 index 0000000..e69b657 --- /dev/null +++ b/csplayready/Utils.cs @@ -0,0 +1,122 @@ +using System.Collections; +using System.Text; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; + +namespace csplayready; + +public static class Utils +{ + private static readonly ECCurve Curve = ECNamedCurveTable.GetByName("secp256r1").Curve; + + public static byte[] ToRawByteArray(this BigInteger value) { + var bytes = value.ToByteArray(); + return bytes[0] == 0 ? bytes[1..] : bytes; + } + + public static byte[] ToBytes(this ECPoint point) + { + return point.XCoord.ToBigInteger().ToRawByteArray() + .Concat(point.YCoord.ToBigInteger().ToRawByteArray()) + .ToArray(); + } + + public static ECPoint FromBytes(byte[] bytes) + { + if (bytes.Length != 64) + throw new ArgumentException("Byte array must be exactly 64 bytes (32 bytes each for X and Y coordinates)"); + + ECPoint point = Curve.CreatePoint( + new BigInteger(1, bytes[..32]), + new BigInteger(1, bytes[32..]) + ); + if (!point.IsValid()) + throw new ArgumentException("Point is not valid for the given curve"); + + return point; + } + + public static ECPoint ToEcPoint(this byte[] data) + { + return ECNamedCurveTable.GetByName("secp256r1").Curve.CreatePoint( + new BigInteger(1, data[..32]), + new BigInteger(1, data[32..64]) + ); + } + + public static string ToHex(this byte[] bytes) => string.Concat(bytes.Select(b => b.ToString("x2"))); + + public static byte[] HexToBytes(this string hex) + { + if (string.IsNullOrWhiteSpace(hex)) + throw new ArgumentException("Hex string cannot be null or empty.", nameof(hex)); + + if (hex.Length % 2 != 0) + throw new FormatException("Hex string must have an even length."); + + var bytes = new byte[hex.Length / 2]; + for (var i = 0; i < bytes.Length; i++) + { + bytes[i] = (byte)((GetHexVal(hex[i * 2]) << 4) + GetHexVal(hex[i * 2 + 1])); + } + return bytes; + } + + private static int GetHexVal(char hex) + { + int val = hex; + return val - (val < 58 ? 48 : val < 97 ? 55 : 87); + } + + public static string FormatBytes(byte[] data) + { + StringBuilder builder = new StringBuilder(); + + foreach (byte b in data) + { + if (b >= 32 && b <= 126) + { + builder.Append((char)b); + } + else + { + builder.AppendFormat("\\x{0:X2}", b); + } + } + + return builder.ToString(); + } + + public static void PrintObject(object? obj, int indentLevel = 0) + { + var indent = new string(' ', indentLevel * 2); + + switch (obj) + { + case Dictionary dictionary: + Console.WriteLine($"{indent}Dictionary({dictionary.Count}):"); + foreach (var kvp in dictionary) + { + Console.WriteLine($"{indent} {kvp.Key}:"); + PrintObject(kvp.Value, indentLevel + 2); + } + break; + case byte[] bytes: + Console.WriteLine($"{indent}byte[{bytes.Length}]: \"{FormatBytes(bytes)}\""); + break; + case IList list: + Console.WriteLine($"{indent}List({list.Count}):"); + for (var i = 0; i < list.Count; i++) + { + Console.WriteLine($"{indent} --- {i} ---"); + PrintObject(list[i], indentLevel + 1); + } + break; + default: + Console.WriteLine($"{indent}{obj}"); + break; + } + } +} \ No newline at end of file diff --git a/csplayready/constructcs/BinaryParser.cs b/csplayready/constructcs/BinaryParser.cs new file mode 100644 index 0000000..bcf6dff --- /dev/null +++ b/csplayready/constructcs/BinaryParser.cs @@ -0,0 +1,613 @@ +using constructcs; + +namespace csplayready.constructcs; + +public abstract class Field(string name) +{ + public string Name { get; } = name; + + protected static void Assert(int length, int size) => _ = length < size ? throw new EndOfStreamException($"Expected {size}, got {length}") : true; + public abstract object Read(BinaryReader reader, Context context); + public abstract void Write(BinaryWriter writer, Context context, object value); +} + +public class Context +{ + private readonly Dictionary _values = new(); + + public void SetValue(string name, object value) => _values[name] = value; + public T GetValue(string fieldName) + { + if (_values.TryGetValue(fieldName, out var value)) + { + return (T)value; + } + throw new KeyNotFoundException($"Field '{fieldName}' not found in context"); + } +} + +public class ContextAccess +{ + private readonly Context _context; + + public ContextAccess(Context context) + { + _context = context; + } + + public object this[string fieldName] => _context.GetValue(fieldName); +} + +public class IntField : Field +{ + private readonly int _size; + private readonly bool _bigEndian; + private readonly bool _signed; + + public IntField(string name, int size, bool bigEndian, bool signed) : base(name) + { + _size = size; + _bigEndian = bigEndian; + _signed = signed; + } + + public override object Read(BinaryReader reader, Context context) + { + var bytes = reader.ReadBytes(_size); + Assert(bytes.Length, _size); + + if (_bigEndian) + Array.Reverse(bytes); + + object value = (_size, _signed) switch + { + (1, true) => (sbyte)bytes[0], + (1, false) => bytes[0], + (2, true) => BitConverter.ToInt16(bytes, 0), + (2, false) => BitConverter.ToUInt16(bytes, 0), + (4, true) => BitConverter.ToInt32(bytes, 0), + (4, false) => BitConverter.ToUInt32(bytes, 0), + (8, true) => BitConverter.ToInt64(bytes, 0), + (8, false) => BitConverter.ToUInt64(bytes, 0), + _ => throw new ArgumentException($"Unsupported byte count: {_size}") + }; + + context.SetValue(Name, value); + return value; + } + + public override void Write(BinaryWriter writer, Context context, object value) + { + byte[] bytes = (_size, _signed) switch + { + (1, true) => [(byte)Convert.ToSByte(value)], + (1, false) => [Convert.ToByte(value)], + (2, true) => BitConverter.GetBytes(Convert.ToInt16(value)), + (2, false) => BitConverter.GetBytes(Convert.ToUInt16(value)), + (4, true) => BitConverter.GetBytes(Convert.ToInt32(value)), + (4, false) => BitConverter.GetBytes(Convert.ToUInt32(value)), + (8, true) => BitConverter.GetBytes(Convert.ToInt64(value)), + (8, false) => BitConverter.GetBytes(Convert.ToUInt64(value)), + _ => throw new ArgumentException($"Unsupported byte count: {_size}") + }; + + if (_bigEndian) + Array.Reverse(bytes); + + writer.Write(bytes); + context.SetValue(Name, value); + } +} + + +public class BytesField : Field +{ + private protected readonly int? Length; + private protected readonly Func? LengthExpression; + + public BytesField(string name, int length) : base(name) + { + Length = length; + } + + public BytesField(string name, Func lengthExpression) : base(name) + { + LengthExpression = lengthExpression; + } + + public override object Read(BinaryReader reader, Context context) + { + var length = Length ?? Convert.ToInt32(LengthExpression!(new ContextAccess(context))); + var value = reader.ReadBytes(length); + + Assert(value.Length, length); + context.SetValue(Name, value); + + return value; + } + + public override void Write(BinaryWriter writer, Context context, object value) + { + var bytes = (byte[])value; + var length = Length ?? Convert.ToInt32(LengthExpression!(new ContextAccess(context))); + + if (bytes.Length != length) + throw new InvalidDataException($"Expected {length}, got {bytes.Length}"); + + writer.Write(bytes); + context.SetValue(Name, value); + } +} + +public class StringField : BytesField +{ + private readonly Encoding _encoding; + + public StringField(string name, int length, Encoding encoding) : base(name, length) + { + _encoding = encoding; + } + + public StringField(string name, Func lengthExpression, Encoding encoding) : base(name, lengthExpression) + { + _encoding = encoding; + } + + public override object Read(BinaryReader reader, Context context) + { + var bytes = (byte[])base.Read(reader, context); + + return _encoding switch + { + Encoding.Ascii => System.Text.Encoding.ASCII.GetString(bytes), + Encoding.Utf8 => System.Text.Encoding.UTF8.GetString(bytes), + Encoding.Utf16 => System.Text.Encoding.Unicode.GetString(bytes), + _ => throw new ArgumentOutOfRangeException() + }; + } + + public override void Write(BinaryWriter writer, Context context, object value) + { + var text = (string)value; + var length = Length ?? Convert.ToInt32(LengthExpression!(new ContextAccess(context))); + + byte[] bytes = _encoding switch + { + Encoding.Ascii => System.Text.Encoding.ASCII.GetBytes(text), + Encoding.Utf8 => System.Text.Encoding.UTF8.GetBytes(text), + Encoding.Utf16 => System.Text.Encoding.Unicode.GetBytes(text), + _ => throw new ArgumentOutOfRangeException() + }; + + if (bytes.Length != length) + throw new InvalidDataException($"Expected {length}, got {bytes.Length}"); + + writer.Write(bytes); + context.SetValue(Name, value); + } +} + + +public class ConstField : Field +{ + private readonly byte[] _expected; + + public ConstField(string name, byte[] expected) : base(name) + { + _expected = expected; + } + + public override object Read(BinaryReader reader, Context context) + { + var actual = reader.ReadBytes(_expected.Length); + + Assert(actual.Length, _expected.Length); + if (!actual.SequenceEqual(_expected)) + throw new InvalidDataException($"Expected constant {_expected.ToHex()} but got {actual.ToHex()}"); + + context.SetValue(Name, actual); + return actual; + } + + public override void Write(BinaryWriter writer, Context context, object value) + { + var bytes = (byte[])value; + + Assert(bytes.Length, _expected.Length); + if (!bytes.SequenceEqual(_expected)) + throw new InvalidDataException($"Expected constant {_expected.ToHex()} but got {bytes.ToHex()}"); + + writer.Write(bytes); + context.SetValue(Name, value); + } +} + +public class ArrayField : Field +{ + private readonly Field _field; + private readonly int? _count; + private readonly Func? _countExpression; + + public ArrayField(string name, Field field, int count) : base(name) + { + _field = field; + _count = count; + } + + public ArrayField(string name, Field field, Func countExpression) : base(name) + { + _field = field; + _countExpression = countExpression; + } + + public override object Read(BinaryReader reader, Context context) + { + var count = _count ?? Convert.ToInt32(_countExpression!(new ContextAccess(context))); + var results = new List(); + + for (var i = 0; i < count; i++) + { + var value = _field.Read(reader, context); + results.Add(value); + } + Assert(results.Count, count); + + context.SetValue(Name, results); + return results; + } + + public override void Write(BinaryWriter writer, Context context, object value) + { + var count = _count ?? Convert.ToInt32(_countExpression!(new ContextAccess(context))); + var results = new List(); + + for (var i = 0; i < count; i++) + { + var item = ((List)value)[i]; + _field.Write(writer, context, item); + results.Add(item); + } + Assert(results.Count, count); + + context.SetValue(Name, value); + } +} + + +public class StructField : Field +{ + private readonly Struct? _nestedStruct; + private readonly Func? _nestedStructExpression; + + public StructField(string name, Struct nestedStruct) : base(name) + { + _nestedStruct = nestedStruct; + } + + public StructField(string name, Func nestedStructExpression) : base(name) + { + _nestedStructExpression = nestedStructExpression; + } + + public override object Read(BinaryReader reader, Context context) + { + var nestedStruct = _nestedStruct ?? _nestedStructExpression!(); + + var nestedData = nestedStruct.Parse(reader, context); + context.SetValue(Name, nestedData); + return nestedData; + } + + public override void Write(BinaryWriter writer, Context context, object value) + { + var nestedStruct = _nestedStruct ?? _nestedStructExpression!(); + + nestedStruct.Build(writer, context, (Dictionary)value); + context.SetValue(Name, value); + } +} + + +public class SwitchField : Field +{ + private readonly int? _index; + private readonly Func? _indexExpression; + private readonly Func _switchExpression; + + public SwitchField(string name, int index, Func switchExpression) : base(name) + { + _index = index; + _switchExpression = switchExpression; + } + + public SwitchField(string name, Func indexExpression, Func switchExpression) : base(name) + { + _indexExpression = indexExpression; + _switchExpression = switchExpression; + } + + public override object Read(BinaryReader reader, Context context) + { + var index = _index ?? Convert.ToInt32(_indexExpression!(new ContextAccess(context))); + var field = _switchExpression(index); + + if(field == null) + throw new InvalidOperationException($"No case found for index {index} in Switch {Name}"); + + var value = field.Read(reader, context); + context.SetValue(Name, value); + return value; + } + + public override void Write(BinaryWriter writer, Context context, object value) + { + var index = _index ?? Convert.ToInt32(_indexExpression!(new ContextAccess(context))); + var field = _switchExpression(index); + + if(field == null) + throw new InvalidOperationException($"No case found for index {index} in Switch {Name}"); + + field.Write(writer, context, value); + context.SetValue(Name, value); + } +} + + +public class ConditionalField : Field +{ + private readonly Field _thenField; + private readonly Field _elseField; + private readonly bool? _condition; + private readonly Func? _conditionExpression; + + public ConditionalField(string name, bool condition, Field thenField, Field elseField) : base(name) + { + _condition = condition; + _thenField = thenField; + _elseField = elseField; + } + + public ConditionalField(string name, Func conditionExpression, Field thenField, Field elseField) : base(name) + { + _conditionExpression = conditionExpression; + _thenField = thenField; + _elseField = elseField; + } + + public override object Read(BinaryReader reader, Context context) + { + var condition = _condition ?? Convert.ToBoolean(_conditionExpression!(new ContextAccess(context))); + var value = condition ? _thenField.Read(reader, context) : _elseField.Read(reader, context); + + context.SetValue(Name, value); + return value; + } + + public override void Write(BinaryWriter writer, Context context, object value) + { + var condition = _condition ?? Convert.ToBoolean(_conditionExpression!(new ContextAccess(context))); + + if (condition) + { + _thenField.Write(writer, context, value); + } + else + { + _elseField.Write(writer, context, value); + } + + context.SetValue(Name, value); + } +} + + +public class RangeField : Field +{ + private readonly Field _field; + private readonly int? _min; + private readonly Func? _minExpression; + private readonly int? _max; + private readonly Func? _maxExpression; + + public RangeField(string name, Field field, int min, int max) : base(name) + { + _field = field; + _min = min; + _max = max; + } + + public RangeField(string name, Field field, Func minExpression, int max) : base(name) + { + _field = field; + _minExpression = minExpression; + _max = max; + } + + public RangeField(string name, Field field, int min, Func maxExpression) : base(name) + { + _field = field; + _min = min; + _maxExpression = maxExpression; + } + + public RangeField(string name, Field field, Func minExpression, Func maxExpression) : base(name) + { + _field = field; + _minExpression = minExpression; + _maxExpression = maxExpression; + } + + public override object Read(BinaryReader reader, Context context) + { + var min = _min ?? Convert.ToInt32(_minExpression!(new ContextAccess(context))); + var max = _max ?? Convert.ToInt32(_maxExpression!(new ContextAccess(context))); + + var results = new List(); + while (results.Count < max) + { + var pos = reader.BaseStream.Position; + try + { + var value = _field.Read(reader, context); + results.Add(value); + } + catch (Exception) + { + reader.BaseStream.Position = pos; + break; + } + } + + Assert(results.Count, min); + context.SetValue(Name, results); + + return results; + } + + public override void Write(BinaryWriter writer, Context context, object value) + { + var min = _min ?? Convert.ToInt32(_minExpression!(new ContextAccess(context))); + var max = _max ?? Convert.ToInt32(_maxExpression!(new ContextAccess(context))); + var items = (List)value; + + if (!(min <= items.Count && items.Count <= max)) + throw new Exception($"Expected from {min} to {max} elements, found {items.Count}"); + + var results = new List(); + for (var i = 0; i < items.Count; i++) + { + var item = items[results.Count]; + _field.Write(writer, context, item); + results.Add(value); + } + + Assert(results.Count, min); + context.SetValue(Name, results); + } +} + + +public class PassField(string name) : Field(name) +{ + public override object Read(BinaryReader reader, Context context) + { + return new object(); + } + + public override void Write(BinaryWriter writer, Context context, object value) { } +} + +public class Struct +{ + private readonly List _fields = []; + + public Struct(params Field[] fields) + { + _fields.AddRange(fields); + } + + public Dictionary Parse(byte[] data) + { + using var stream = new MemoryStream(data); + return Parse(stream); + } + + public Dictionary Parse(Stream stream) + { + using var reader = new BinaryReader(stream); + return Parse(reader, new Context()); + } + + public Dictionary Parse(BinaryReader reader, Context context) + { + var result = new Dictionary(); + + foreach (var field in _fields) + { + var value = field.Read(reader, context); + result[field.Name] = value; + } + + return result; + } + + public byte[] Build(Dictionary values) + { + using var memoryStream = new MemoryStream(); + Build(memoryStream, values); + return memoryStream.ToArray(); + } + + public void Build(Stream stream, Dictionary values) + { + using var writer = new BinaryWriter(stream); + Build(writer, new Context(), values); + } + + public void Build(BinaryWriter writer, Context context, Dictionary values) + { + foreach (Field field in _fields.Where(field => !string.IsNullOrEmpty(field.Name))) + { + if (!values.TryGetValue(field.Name, out var value)) + throw new KeyNotFoundException($"No value provided for Field '{field.Name}'"); + + field.Write(writer, context, value); + } + writer.Flush(); + } +} + +public static class ParserBuilder +{ + public static IntField Int8sb(string name) => new(name, 1, true, true); + public static IntField Int8sl(string name) => new(name, 1, false, true); + public static IntField Int16sb(string name) => new(name, 2, true, true); + public static IntField Int16sl(string name) => new(name, 2, false, true); + public static IntField Int32sb(string name) => new(name, 4, true, true); + public static IntField Int32sl(string name) => new(name, 4, false, true); + public static IntField Int64sb(string name) => new(name, 8, true, true); + public static IntField Int64sl(string name) => new(name, 8, false, true); + + public static IntField Int8ub(string name) => new(name, 1, true, false); + public static IntField Int8ul(string name) => new(name, 1, false, false); + public static IntField Int16ub(string name) => new(name, 2, true, false); + public static IntField Int16ul(string name) => new(name, 2, false, false); + public static IntField Int32ub(string name) => new(name, 4, true, false); + public static IntField Int32ul(string name) => new(name, 4, false, false); + public static IntField Int64ub(string name) => new(name, 8, true, false); + public static IntField Int64ul(string name) => new(name, 8, false, false); + + public static BytesField Bytes(string name, int length) => new(name, length); + public static BytesField Bytes(string name, Func lengthExpression) => new(name, lengthExpression); + + public static ConstField Const(string name, byte[] expected) => new(name, expected); + + public static StringField ASCIIString(string name, int length) => new(name, length, Encoding.Ascii); + public static StringField ASCIIString(string name, Func lengthExpression) => new(name, lengthExpression, Encoding.Ascii); + public static StringField UTF8String(string name, int length) => new(name, length, Encoding.Utf8); + public static StringField UTF8String(string name, Func lengthExpression) => new(name, lengthExpression, Encoding.Utf8); + public static StringField UTF16String(string name, int length) => new(name, length, Encoding.Utf16); + public static StringField UTF16String(string name, Func lengthExpression) => new(name, lengthExpression, Encoding.Utf16); + + public static ArrayField Array(string name, Field element, int count) => new(name, element, count); + public static ArrayField Array(string name, Field element, Func countExpression) => new(name, element, countExpression); + + public static StructField Child(string name, Struct nestedStruct) => new(name, nestedStruct); + public static StructField Child(string name, Func nestedStructExpression) => new(name, nestedStructExpression); + + public static SwitchField Switch(string name, int index, Func switchExpression) => new(name, index, switchExpression); + public static SwitchField Switch(string name, Func indexExpression, Func switchExpression) => new(name, indexExpression, switchExpression); + + public static ConditionalField IfThenElse(string name, bool condition, Field thenField, Field elseField) => new(name, condition, thenField, elseField); + public static ConditionalField IfThenElse(string name, Func conditionExpression, Field thenField, Field elseField) => new(name, conditionExpression, thenField, elseField); + + public static ConditionalField If(string name, bool condition, Field thenField) => new(name, condition, thenField, new PassField(thenField.Name)); + public static ConditionalField If(string name, Func conditionExpression, Field thenField) => new(name, conditionExpression, thenField, new PassField(thenField.Name)); + + public static RangeField Range(string name, Field field, int min, int max) => new(name, field, min, max); + public static RangeField Range(string name, Field field, Func minExpression, int max) => new(name, field, minExpression, max); + public static RangeField Range(string name, Field field, int min, Func maxExpression) => new(name, field, min, maxExpression); + public static RangeField Range(string name, Field field, Func minExpression, Func maxExpression) => new(name, field, minExpression, maxExpression); + + public static RangeField GreedyRange(string name, Field field) => new(name, field, 0, int.MaxValue); +} diff --git a/csplayready/constructcs/Enums.cs b/csplayready/constructcs/Enums.cs new file mode 100644 index 0000000..82a8b95 --- /dev/null +++ b/csplayready/constructcs/Enums.cs @@ -0,0 +1,8 @@ +namespace constructcs; + +public enum Encoding +{ + Utf8, + Utf16, + Ascii +} diff --git a/csplayready/crypto/Crypto.cs b/csplayready/crypto/Crypto.cs new file mode 100644 index 0000000..792faab --- /dev/null +++ b/csplayready/crypto/Crypto.cs @@ -0,0 +1,67 @@ +using System.Security.Cryptography; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using ECPoint = Org.BouncyCastle.Math.EC.ECPoint; + +namespace csplayready.crypto; + +public static class Crypto +{ + private static readonly X9ECParameters Curve = ECNamedCurveTable.GetByName("secp256r1"); + private static readonly ECDomainParameters DomainParams = new(Curve.Curve, Curve.G, Curve.N, Curve.H); + + public static byte[] AesCbcEncrypt(byte[] key, byte[] iv, byte[] data) + { + var cipher = CipherUtilities.GetCipher("AES/CBC/PKCS7"); + var keyParam = new KeyParameter(key); + var parameters = new ParametersWithIV(keyParam, iv); + + cipher.Init(true, parameters); + return cipher.DoFinal(data); + } + + public static byte[] AesCbcDecrypt(byte[] key, byte[] iv, byte[] data) + { + var cipher = CipherUtilities.GetCipher("AES/CBC/PKCS7"); + var keyParam = new KeyParameter(key); + var parameters = new ParametersWithIV(keyParam, iv); + + cipher.Init(false, parameters); + return cipher.DoFinal(data); + } + + public static (ECPoint point1, ECPoint point2) Ecc256Encrypt(ECPoint messagePoint, ECPoint publicKey) => ElGamal.Encrypt(messagePoint, publicKey); + public static ECPoint Ecc256Decrypt(ECPoint point1, ECPoint point2, BigInteger privateKey) => ElGamal.Decrypt(point1, point2, privateKey); + + public static byte[] Ecc256Sign(BigInteger privateKey, byte[] data) + { + var signer = new ECDsaSigner(); + + var hash = SHA256.HashData(data); + + var privateKeyParams = new ECPrivateKeyParameters(privateKey, DomainParams); + signer.Init(true, privateKeyParams); + + var signature = signer.GenerateSignature(hash); + return signature[0].ToRawByteArray() + .Concat(signature[1].ToRawByteArray()) + .ToArray(); + } + + public static bool Ecc256Verify(ECPoint publicKey, byte[] data, byte[] signature) + { + var signer = new ECDsaSigner(); + + var publicKeyParams = new ECPublicKeyParameters(publicKey, DomainParams); + signer.Init(false, publicKeyParams); + + var hash = SHA256.HashData(data); + var r = new BigInteger(1, signature[..32]); + var s = new BigInteger(1, signature[32..]); + + return signer.VerifySignature(hash, r, s); + } +} \ No newline at end of file diff --git a/csplayready/crypto/EccKey.cs b/csplayready/crypto/EccKey.cs new file mode 100644 index 0000000..10c3019 --- /dev/null +++ b/csplayready/crypto/EccKey.cs @@ -0,0 +1,94 @@ +using System.Security.Cryptography; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using ECPoint = Org.BouncyCastle.Math.EC.ECPoint; + +namespace csplayready.crypto; + +public class EccKey +{ + private static readonly X9ECParameters Curve = ECNamedCurveTable.GetByName("secp256r1"); + private static readonly ECDomainParameters DomainParams = new(Curve.Curve, Curve.G, Curve.N, Curve.H); + + public readonly BigInteger PrivateKey; + public readonly ECPoint PublicKey; + + public EccKey(BigInteger privateKey, ECPoint publicKey) + { + PrivateKey = privateKey; + PublicKey = publicKey; + } + + public EccKey(BigInteger privateKey) + { + PrivateKey = privateKey; + PublicKey = Curve.G.Multiply(PrivateKey).Normalize(); + } + + public static EccKey Generate() + { + var eccGenerator = GeneratorUtilities.GetKeyPairGenerator("EC"); + eccGenerator.Init(new ECKeyGenerationParameters(DomainParams, new SecureRandom())); + + var eccKeyPair = eccGenerator.GenerateKeyPair(); + return new EccKey(((ECPrivateKeyParameters)eccKeyPair.Private).D, ((ECPublicKeyParameters)eccKeyPair.Public).Q); + } + + public static EccKey Loads(byte[] data, bool verify = true) + { + if (data.Length != 32 && data.Length != 96) + { + throw new InvalidDataException($"Invalid data length. Expecting 96 or 32 bytes, got {data.Length}"); + } + + BigInteger privateKey = new BigInteger(1, data[..32]); + ECPoint publicKey = Curve.G.Multiply(privateKey).Normalize(); + + if (data.Length == 96) + { + ECPoint loadedPublicKey = DomainParams.Curve.CreatePoint( + new BigInteger(1, data[32..64]), + new BigInteger(1, data[64..96]) + ); + + if (verify) + { + if (!publicKey.XCoord.Equals(loadedPublicKey.XCoord) || !publicKey.YCoord.Equals(loadedPublicKey.YCoord)) + { + throw new InvalidDataException("Derived Public Key does not match loaded Public Key"); + } + } + + publicKey = loadedPublicKey; + } + + return new EccKey(privateKey, publicKey); + } + + public static EccKey Load(string path) => Loads(File.ReadAllBytes(path)); + + public byte[] Dumps(bool privateOnly = false) + { + if (privateOnly) + return PrivateBytes(); + return PrivateBytes() + .Concat(PublicBytes()).ToArray(); + } + + public void Dump(string path, bool privateOnly = false) => File.WriteAllBytes(path, Dumps(privateOnly)); + + public byte[] PrivateBytes() => PrivateKey.ToRawByteArray(); + + public byte[] PrivateSha256Digest() => SHA256.HashData(PrivateBytes()); + + public byte[] PublicBytes() + { + return PublicKey.XCoord.ToBigInteger().ToRawByteArray() + .Concat(PublicKey.YCoord.ToBigInteger().ToRawByteArray()) + .ToArray(); + } + + public byte[] PublicSha256Digest() => SHA256.HashData(PublicBytes()); +} \ No newline at end of file diff --git a/csplayready/crypto/ElGamal.cs b/csplayready/crypto/ElGamal.cs new file mode 100644 index 0000000..97b27c9 --- /dev/null +++ b/csplayready/crypto/ElGamal.cs @@ -0,0 +1,30 @@ +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; +using Org.BouncyCastle.Security; + +namespace csplayready.crypto; + +public class ElGamal +{ + private static readonly X9ECParameters Curve = ECNamedCurveTable.GetByName("secp256r1"); + private static readonly ECDomainParameters DomainParams = new(Curve.Curve, Curve.G, Curve.N, Curve.H); + + public static (ECPoint point1, ECPoint point2) Encrypt(ECPoint messagePoint, ECPoint publicKey) + { + var random = new SecureRandom(); + var ephemeralKey = new BigInteger(DomainParams.N.BitLength, random); + + var point1 = Curve.G.Multiply(ephemeralKey).Normalize(); + var point2 = messagePoint.Add(publicKey.Multiply(ephemeralKey)).Normalize(); + + return (point1, point2); + } + + public static ECPoint Decrypt(ECPoint point1, ECPoint point2, BigInteger privateKey) + { + var sharedSecret = point1.Multiply(privateKey); + return point2.Subtract(sharedSecret).Normalize(); + } +} diff --git a/csplayready/csplayready.csproj b/csplayready/csplayready.csproj new file mode 100644 index 0000000..474cebc --- /dev/null +++ b/csplayready/csplayready.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + enable + enable + true + true + + + + + + + diff --git a/csplayready/device/Device.cs b/csplayready/device/Device.cs new file mode 100644 index 0000000..67c95ee --- /dev/null +++ b/csplayready/device/Device.cs @@ -0,0 +1,95 @@ +using System.Text.RegularExpressions; +using csplayready.crypto; + +using csplayready.constructcs; +using csplayready.system; +using static csplayready.constructcs.ParserBuilder; + +namespace csplayready.device; + +public class Device(byte version, EccKey? groupKey, EccKey? encryptionKey, EccKey? signingKey, CertificateChain? groupCertificate) +{ + private static readonly Struct PrdV2 = new( + Int32ub("group_certificate_length"), + Bytes("group_certificate", ctx => ctx["group_certificate_length"]), + Bytes("encryption_key", 96), + Bytes("signing_key", 96) + ); + + private static readonly Struct PrdV3 = new( + Bytes("group_key", 96), + Bytes("encryption_key", 96), + Bytes("signing_key", 96), + Int32ub("group_certificate_length"), + Bytes("group_certificate", ctx => ctx["group_certificate_length"]) + ); + + private static readonly Struct Prd = new( + Const("signature", "PRD"u8.ToArray()), + Int8ub("version"), + Switch("data", ctx => ctx["version"], i => i switch + { + 2 => Child(string.Empty, PrdV2), + 3 => Child(string.Empty, PrdV3), + _ => throw new InvalidDataException($"Unknown PRD version {i}") + }) + ); + + public byte Version = version; + public EccKey? GroupKey = groupKey; + public EccKey? EncryptionKey = encryptionKey; + public EccKey? SigningKey = signingKey; + public CertificateChain? GroupCertificate = groupCertificate; + + public Device() : this(0, null, null, null, null) { } + + public static Device Loads(byte[] bytes) + { + var result = Prd.Parse(bytes); + var data = (Dictionary)result["data"]; + + Device device = new Device + { + Version = (byte)result["version"], + GroupKey = EccKey.Loads((byte[])data["group_key"]), + EncryptionKey = EccKey.Loads((byte[])data["encryption_key"]), + SigningKey = EccKey.Loads((byte[])data["signing_key"]), + GroupCertificate = CertificateChain.Loads((byte[])data["group_certificate"]) + }; + return device; + } + + public static Device Load(string path) + { + return Loads(File.ReadAllBytes(path)); + } + + public byte[] Dumps() + { + return Prd.Build(new Dictionary + { + { "signature", "PRD"u8.ToArray() }, + { "version", 3 }, + { "data", new Dictionary + { + { "group_key", GroupKey!.Dumps() }, + { "encryption_key", EncryptionKey!.Dumps() }, + { "signing_key", SigningKey!.Dumps() }, + { "group_certificate_length", GroupCertificate!.Dumps().Length }, + { "group_certificate", GroupCertificate!.Dumps() }, + }} + }); + } + + public void Dump(string path) + { + File.WriteAllBytes(path, Dumps()); + } + + public string? GetName() + { + if (GroupCertificate == null) return null; + var name = $"{GroupCertificate!.GetName()}_sl{GroupCertificate!.GetSecurityLevel()}"; + return Regex.Replace(name, @"[^a-zA-Z0-9_\- ]", "").Replace(" ", "_").ToLower(); + } +} diff --git a/csplayready/license/Key.cs b/csplayready/license/Key.cs new file mode 100644 index 0000000..7fbe9de --- /dev/null +++ b/csplayready/license/Key.cs @@ -0,0 +1,31 @@ +namespace csplayready.license; + +public class Key(byte[] keyId, Key.KeyTypes keyType, Key.CipherTypes cipherType, byte[] rawKey) +{ + public enum KeyTypes : ushort + { + Invalid = 0x0000, + Aes128Ctr = 0x0001, + Rc4Cipher = 0x0002, + Aes128Ecb = 0x0003, + Cocktail = 0x0004, + Aes128Cbc = 0x0005, + KeyExchange = 0x0006 + } + + public enum CipherTypes : ushort + { + Invalid = 0x0000, + Rsa1024 = 0x0001, + ChainedLicense = 0x0002, + Ecc256 = 0x0003, + Ecc256WithKz = 0x0004, + TeeTransient = 0x0005, + Ecc256ViaSymmetric = 0x0006 + } + + public readonly byte[] KeyId = keyId; + public readonly KeyTypes KeyType = keyType; + public readonly CipherTypes CipherType = cipherType; + public readonly byte[] RawKey = rawKey; +} \ No newline at end of file diff --git a/csplayready/license/XmlKey.cs b/csplayready/license/XmlKey.cs new file mode 100644 index 0000000..a9628a7 --- /dev/null +++ b/csplayready/license/XmlKey.cs @@ -0,0 +1,33 @@ +using csplayready.crypto; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; + +namespace csplayready.license; + +public class XmlKey +{ + private static readonly ECCurve Curve = ECNamedCurveTable.GetByName("secp256r1").Curve; + + private readonly BigInteger _sharedX; + private readonly BigInteger _sharedY; + + public readonly byte[] AesIv; + public readonly byte[] AesKey; + + public XmlKey() + { + var sharedPoint = EccKey.Generate(); + _sharedX = sharedPoint.PublicKey.XCoord.ToBigInteger(); + _sharedY = sharedPoint.PublicKey.YCoord.ToBigInteger(); + + var sharedXBytes = _sharedX.ToRawByteArray(); + AesIv = sharedXBytes[..16]; + AesKey = sharedXBytes[16..]; + } + + public ECPoint GetPoint() + { + return Curve.CreatePoint(_sharedX, _sharedY); + } +} diff --git a/csplayready/license/XmrLicense.cs b/csplayready/license/XmrLicense.cs new file mode 100644 index 0000000..9cecdcc --- /dev/null +++ b/csplayready/license/XmrLicense.cs @@ -0,0 +1,255 @@ +using csplayready.constructcs; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Macs; +using Org.BouncyCastle.Crypto.Parameters; +using static csplayready.constructcs.ParserBuilder; + +namespace csplayready.license; + +public class XmrLicenseStructs +{ + private static readonly Struct PlayEnablerType = new( + Bytes("player_enabler_type", 16) + ); + + private static readonly Struct DomainRestrictionObject = new( + Bytes("account_id", 16), + Int32ub("revision") + ); + + private static readonly Struct IssueDateObject = new( + Int32ub("issue_date") + ); + + private static readonly Struct RevInfoVersionObject = new( + Int32ub("sequence") + ); + + private static readonly Struct SecurityLevelObject = new( + Int16ub("minimum_security_level") + ); + + private static readonly Struct EmbeddedLicenseSettingsObject = new( + Int16ub("indicator") + ); + + private static readonly Struct EccKeyObject = new( + Int16ub("curve_type"), + Int16ub("key_length"), + Bytes("key", ctx => ctx["key_length"]) + ); + + private static readonly Struct SignatureObject = new( + Int16ub("signature_type"), + Int16ub("signature_data_length"), + Bytes("signature_data", ctx => ctx["signature_data_length"]) + ); + + private static readonly Struct ContentKeyObject = new( + Bytes("key_id", 16), + Int16ub("key_type"), + Int16ub("cipher_type"), + Int16ub("key_length"), + Bytes("encrypted_key", ctx => ctx["key_length"]) + ); + + private static readonly Struct RightsSettingsObject = new( + Int16ub("rights") + ); + + private static readonly Struct OutputProtectionLevelRestrictionObject = new( + Int16ub("minimum_compressed_digital_video_opl"), + Int16ub("minimum_uncompressed_digital_video_opl"), + Int16ub("minimum_analog_video_opl"), + Int16ub("minimum_digital_compressed_audio_opl"), + Int16ub("minimum_digital_uncompressed_audio_opl") + ); + + private static readonly Struct ExpirationRestrictionObject = new( + Int32ub("begin_date"), + Int32ub("end_date") + ); + + private static readonly Struct RemovalDateObject = new( + Int32ub("removal_date") + ); + + private static readonly Struct UplinkKidObject = new( + Bytes("uplink_kid", 16), + Int16ub("chained_checksum_type"), + Int16ub("chained_checksum_length"), + Bytes("chained_checksum", ctx => ctx["chained_checksum_length"]) + ); + + private static readonly Struct AnalogVideoOutputConfigurationRestriction = new( + Bytes("video_output_protection_id", 16), + Bytes("binary_configuration_data", ctx => (uint)ctx["length"] - 24) + ); + + private static readonly Struct DigitalVideoOutputRestrictionObject = new( + Bytes("video_output_protection_id", 16), + Bytes("binary_configuration_data", ctx => (uint)ctx["length"] - 24) + ); + + private static readonly Struct DigitalAudioOutputRestrictionObject = new( + Bytes("audio_output_protection_id", 16), + Bytes("binary_configuration_data", ctx => (uint)ctx["length"] - 24) + ); + + private static readonly Struct PolicyMetadataObject = new( + Bytes("metadata_type", 16), + Bytes("policy_data", ctx => (uint)ctx["length"] - 24) + ); + + private static readonly Struct SecureStopRestrictionObject = new( + Bytes("metering_id", 16) + ); + + private static readonly Struct MeteringRestrictionObject = new( + Bytes("metering_id", 16) + ); + + private static readonly Struct ExpirationAfterFirstPlayRestrictionObject = new( + Int32ub("seconds") + ); + + private static readonly Struct GracePeriodObject = new( + Int32ub("grace_period") + ); + + private static readonly Struct SourceIdObject = new( + Int32ub("source_id") + ); + + private static readonly Struct AuxiliaryKey = new( + Int32ub("location"), + Bytes("key", 16) + ); + + private static readonly Struct AuxiliaryKeysObject = new( + Int16ub("count"), + Array("auxiliary_keys", Child(string.Empty, AuxiliaryKey), ctx => ctx["count"]) + ); + + private static readonly Struct UplinkKeyObject3 = new( + Bytes("uplink_key_id", 16), + Int16ub("chained_length"), + Bytes("checksum", ctx => ctx["chained_length"]), + Int16ub("count"), + Array("entries", Int32ub(string.Empty), ctx => ctx["count"]) + ); + + private static readonly Struct CopyEnablerObject = new( + Bytes("copy_enabler_type", 16) + ); + + private static readonly Struct CopyCountRestrictionObject = new( + Int32ub("count") + ); + + private static readonly Struct MoveObject = new( + Int32ub("minimum_move_protection_level") + ); + + private static readonly Struct XmrObject = new( + Int16ub("flags"), + Int16ub("type"), + Int32ub("length"), + Switch("data", ctx => ctx["type"], i => i switch + { + 0x0005 => Child(string.Empty, OutputProtectionLevelRestrictionObject), + 0x0008 => Child(string.Empty, AnalogVideoOutputConfigurationRestriction), + 0x000a => Child(string.Empty, ContentKeyObject), + 0x000b => Child(string.Empty, SignatureObject), + 0x000d => Child(string.Empty, RightsSettingsObject), + 0x0012 => Child(string.Empty, ExpirationRestrictionObject), + 0x0013 => Child(string.Empty, IssueDateObject), + 0x0016 => Child(string.Empty, MeteringRestrictionObject), + 0x001a => Child(string.Empty, GracePeriodObject), + 0x0022 => Child(string.Empty, SourceIdObject), + 0x002a => Child(string.Empty, EccKeyObject), + 0x002c => Child(string.Empty, PolicyMetadataObject), + 0x0029 => Child(string.Empty, DomainRestrictionObject), + 0x0030 => Child(string.Empty, ExpirationAfterFirstPlayRestrictionObject), + 0x0031 => Child(string.Empty, DigitalAudioOutputRestrictionObject), + 0x0032 => Child(string.Empty, RevInfoVersionObject), + 0x0033 => Child(string.Empty, EmbeddedLicenseSettingsObject), + 0x0034 => Child(string.Empty, SecurityLevelObject), + 0x0037 => Child(string.Empty, MoveObject), + 0x0039 => Child(string.Empty, PlayEnablerType), + 0x003a => Child(string.Empty, CopyEnablerObject), + 0x003b => Child(string.Empty, UplinkKidObject), + 0x003d => Child(string.Empty, CopyCountRestrictionObject), + 0x0050 => Child(string.Empty, RemovalDateObject), + 0x0051 => Child(string.Empty, AuxiliaryKeysObject), + 0x0052 => Child(string.Empty, UplinkKeyObject3), + 0x005a => Child(string.Empty, SecureStopRestrictionObject), + 0x0059 => Child(string.Empty, DigitalVideoOutputRestrictionObject), + _ => Child(string.Empty, () => XmrObject!) + }) + ); + + protected static readonly Struct XmrLicense = new( + Const("signature", "XMR\0"u8.ToArray()), + Int32ub("xmr_version"), + Bytes("rights_id", 16), + GreedyRange("containers", Child(string.Empty, XmrObject)) + ); +} + +public class XmrLicense(Dictionary data) : XmrLicenseStructs +{ + public static XmrLicense Loads(byte[] data) + { + return new XmrLicense(XmrLicense.Parse(data)); + } + + public byte[] Dumps() + { + return XmrLicense.Build(data); + } + + private static Dictionary Locate(Dictionary container) + { + while (true) + { + var flags = (ushort)container["flags"]; + if (flags != 2 && flags != 3) return container; + + container = (Dictionary)container["data"]; + } + } + + public IEnumerable> GetObject(ushort type) + { + foreach (Dictionary obj in (List)data["containers"]) + { + var container = Locate(obj); + if ((ushort)container["type"] == type) + yield return (Dictionary)container["data"]; + } + } + + public bool CheckSignature(byte[] integrityKey) + { + var cmac = new CMac(new AesEngine()); + cmac.Init(new KeyParameter(integrityKey)); + + var signatureObject = GetObject(11).FirstOrDefault(); + if (signatureObject == null) + throw new InvalidLicense("License does not contain a signature object"); + + var message = Dumps()[..^((ushort)signatureObject["signature_data_length"] + 12)]; + cmac.BlockUpdate(message, 0, message.Length); + + var result = new byte[cmac.GetMacSize()]; + cmac.DoFinal(result, 0); + + return result.SequenceEqual((byte[])signatureObject["signature_data"]); + } + + public Dictionary GetData() + { + return data; + } +} \ No newline at end of file diff --git a/csplayready/system/BCert.cs b/csplayready/system/BCert.cs new file mode 100644 index 0000000..cb2effe --- /dev/null +++ b/csplayready/system/BCert.cs @@ -0,0 +1,505 @@ +using System.Text; +using csplayready.constructcs; +using csplayready.crypto; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Math; +using static csplayready.constructcs.ParserBuilder; + +namespace csplayready.system; + +public class BCertStructs +{ + protected static readonly Struct DrmBCertBasicInfo = new( + Bytes("cert_id", 16), + Int32ub("security_level"), + Int32ub("flags"), + Int32ub("cert_type"), + Bytes("public_key_digest", 32), + Int32ub("expiration_date"), + Bytes("client_id", 16) + ); + + protected static readonly Struct DrmBCertDomainInfo = new( + Bytes("service_id", 16), + Bytes("account_id", 16), + Int32ub("revision_timestamp"), + Int32ub("domain_url_length"), + Bytes("domain_url", ctx => ((uint)ctx["domain_url_length"] + 3) & 0xfffffffc) // TODO: use string + ); + + protected static readonly Struct DrmBCertPcInfo = new( + Int32ub("security_version") + ); + + protected static readonly Struct DrmBCertDeviceInfo = new( + Int32ub("max_license"), + Int32ub("max_header"), + Int32ub("max_chain_depth") + ); + + protected static readonly Struct DrmBCertFeatureInfo = new( + Int32ub("feature_count"), + Array("features", Int32ub(string.Empty), ctx => ctx["feature_count"]) + ); + + protected static readonly Struct DrmBCertKeyInfo = new( + Int32ub("key_count"), + Array("cert_keys", Child(string.Empty, new Struct( + Int16ub("type"), + Int16ub("length"), + Int32ub("flags"), + Bytes("key", ctx => (ushort)ctx["length"] / 8), + Int32ub("usages_count"), + Array("usages", Int32ub(string.Empty), ctx => ctx["usages_count"]) + )), ctx => ctx["key_count"]) + ); + + protected static readonly Struct DrmBCertManufacturerInfo = new( + Int32ub("flags"), + Int32ub("manufacturer_name_length"), + Bytes("manufacturer_name", ctx => ((uint)ctx["manufacturer_name_length"] + 3) & 0xfffffffc), // TODO: use string + Int32ub("model_name_length"), + Bytes("model_name", ctx => ((uint)ctx["model_name_length"] + 3) & 0xfffffffc), // TODO: use string + Int32ub("model_number_length"), + Bytes("model_number", ctx => ((uint)ctx["model_number_length"] + 3) & 0xfffffffc) // TODO: use string + ); + + protected static readonly Struct DrmBCertSignatureInfo = new( + Int16ub("signature_type"), + Int16ub("signature_size"), + Bytes("signature", ctx => ctx["signature_size"]), + Int32ub("signature_key_size"), + Bytes("signature_key", ctx => (uint)ctx["signature_key_size"] / 8) + ); + + protected static readonly Struct DrmBCertSilverlightInfo = new( + Int32ub("security_version"), + Int32ub("platform_identifier") + ); + + protected static readonly Struct DrmBCertMeteringInfo = new( + Bytes("metering_id", 16), + Int32ub("metering_url_length"), + Bytes("metering_url", ctx => ((uint)ctx["metering_url_length"] + 3) & 0xfffffffc) // TODO: use string + ); + + protected static readonly Struct DrmBCertExtDataSignKeyInfo = new( + Int16ub("key_type"), + Int16ub("key_length"), + Int32ub("flags"), + Bytes("key", ctx => (ushort)ctx["key_length"] / 8) + ); + + protected static readonly Struct BCertExtDataRecord = new( + Int32ub("data_size"), + Bytes("data", ctx => ctx["data_size"]) + ); + + protected static readonly Struct DrmBCertExtDataSignature = new( + Int16ub("signature_type"), + Int16ub("signature_size"), + Bytes("signature", ctx => ctx["signature_size"]) + ); + + protected static readonly Struct BCertExtDataContainer = new( + Int32ub("record_count"), + Array("records", Child(string.Empty, BCertExtDataRecord), ctx => ctx["record_count"]), + Child("signature", DrmBCertExtDataSignature) + ); + + protected static readonly Struct DrmBCertServerInfo = new( + Int32ub("warning_days") + ); + + protected static readonly Struct DrmBcertSecurityVersion = new( + Int32ub("security_version"), + Int32ub("platform_identifier") + ); + + protected static readonly Struct Attribute = new( + Int16ub("flags"), + Int16ub("tag"), + Int32ub("length"), + Switch("attribute", ctx => ctx["tag"], i => i switch + { + 1 => Child(string.Empty, DrmBCertBasicInfo), + 2 => Child(string.Empty, DrmBCertDomainInfo), + 3 => Child(string.Empty, DrmBCertPcInfo), + 4 => Child(string.Empty, DrmBCertDeviceInfo), + 5 => Child(string.Empty, DrmBCertFeatureInfo), + 6 => Child(string.Empty, DrmBCertKeyInfo), + 7 => Child(string.Empty, DrmBCertManufacturerInfo), + 8 => Child(string.Empty, DrmBCertSignatureInfo), + 9 => Child(string.Empty, DrmBCertSilverlightInfo), + 10 => Child(string.Empty, DrmBCertMeteringInfo), + 11 => Child(string.Empty, DrmBCertExtDataSignKeyInfo), + 12 => Child(string.Empty, BCertExtDataContainer), + 13 => Child(string.Empty, DrmBCertExtDataSignature), + 14 => Bytes(string.Empty, ctx => (uint)ctx["length"] - 8), + 15 => Child(string.Empty, DrmBCertServerInfo), + 16 => Child(string.Empty, DrmBcertSecurityVersion), + 17 => Child(string.Empty, DrmBcertSecurityVersion), + _ => Bytes(string.Empty, ctx => (uint)ctx["length"] - 8) + }) + ); + + protected static readonly Struct BCert = new( + Const("signature", "CERT"u8.ToArray()), + Int32ub("version"), + Int32ub("total_length"), + Int32ub("certificate_length"), + GreedyRange("attributes", Child(string.Empty, Attribute)) + ); + + protected static readonly Struct BCertChain = new( + Const("signature", "CHAI"u8.ToArray()), + Int32ub("version"), + Int32ub("total_length"), + Int32ub("flags"), + Int32ub("certificate_count"), + GreedyRange("certificates", Child(string.Empty, BCert)) + ); +} + +public class Certificate(Dictionary data) : BCertStructs +{ + public static Certificate Loads(byte[] data) + { + return new Certificate(BCert.Parse(data)); + } + + public byte[] Dumps() + { + return BCert.Build(data); + } + + public static Certificate NewLeafCertificate(byte[] certId, uint securityLevel, byte[] clientId, EccKey signingKey, EccKey encryptionKey, EccKey groupKey, CertificateChain parent, uint expiry = 0xFFFFFFFF) + { + var basicInfo = new Dictionary + { + { "cert_id", certId }, + { "security_level", securityLevel }, + { "flags", (uint)0 }, + { "cert_type", (uint)2 }, + { "public_key_digest", signingKey.PublicSha256Digest() }, + { "expiration_date", expiry }, + { "client_id", clientId } + }; + var basicInfoAttribute = new Dictionary + { + { "flags", (ushort)1 }, + { "tag", (ushort)1 }, + { "length", (uint)(DrmBCertBasicInfo.Build(basicInfo).Length + 8) }, + { "attribute", basicInfo } + }; + + var deviceInfo = new Dictionary + { + { "max_license", (uint)10240 }, + { "max_header", (uint)15360 }, + { "max_chain_depth", (uint)2 } + }; + var deviceInfoAttribute = new Dictionary + { + { "flags", (ushort)1 }, + { "tag", (ushort)4 }, + { "length", (uint)(DrmBCertDeviceInfo.Build(deviceInfo).Length + 8) }, + { "attribute", deviceInfo } + }; + + var feature = new Dictionary + { + { "feature_count", 3 }, + { "features", new List + { + // 1, // Transmitter + // 2, // Receiver + // 3, // SharedCertificate + 4, // SecureClock + // 5, // AntiRollBackClock + // 6, // ReservedMetering + // 7, // ReservedLicSync + // 8, // ReservedSymOpt + 9, // CRLS (Revocation Lists) + // 10, // ServerBasicEdition + // 11, // ServerStandardEdition + // 12, // ServerPremiumEdition + 13, // PlayReady3Features + // 14, // DeprecatedSecureStop + } }, + }; + var featureAttribute = new Dictionary + { + { "flags", (ushort)1 }, + { "tag", (ushort)5 }, + { "length", (uint)(DrmBCertFeatureInfo.Build(feature).Length + 8) }, + { "attribute", feature } + }; + + var certKeySign = new Dictionary + { + { "type", (ushort)1 }, + { "length", (ushort)512 }, + { "flags", (uint)0 }, + { "key", signingKey.PublicBytes() }, + { "usages_count", (uint)1 }, + { "usages", new List + { + (uint)1 // KEYUSAGE_SIGN + } }, + }; + var certKeyEncrypt = new Dictionary + { + { "type", (ushort)1 }, + { "length", (ushort)512 }, + { "flags", (uint)0 }, + { "key", encryptionKey.PublicBytes() }, + { "usages_count", (uint)1 }, + { "usages", new List + { + (uint)2 // KEYUSAGE_ENCRYPT_KEY + } }, + }; + var keyInfo = new Dictionary + { + { "key_count", (uint)2 }, + { "cert_keys", new List + { + certKeySign, + certKeyEncrypt + } }, + }; + var keyInfoAttribute = new Dictionary + { + { "flags", (ushort)1 }, + { "tag", (ushort)6 }, + { "length", (uint)(DrmBCertKeyInfo.Build(keyInfo).Length + 8) }, + { "attribute", keyInfo } + }; + + var manufacturerInfo = parent.Get(0).GetAttribute(7); + if (manufacturerInfo == null) + throw new InvalidCertificate("Parent's manufacturer info required for provisioning"); + + var newBCertContainer = new Dictionary + { + { "signature", "CERT"u8.ToArray() }, + { "version", (uint)1 }, + { "total_length", (uint)0 }, // filled at a later time + { "certificate_length", (uint)0 }, // filled at a later time + { "attributes", new List + { + basicInfoAttribute, + deviceInfoAttribute, + featureAttribute, + keyInfoAttribute, + manufacturerInfo + } }, + }; + + var payload = BCert.Build(newBCertContainer); + newBCertContainer["certificate_length"] = payload.Length; + newBCertContainer["total_length"] = payload.Length + 144; + + var signPayload = BCert.Build(newBCertContainer); + var signature = Crypto.Ecc256Sign(groupKey.PrivateKey, signPayload); + + var signatureInfo = new Dictionary + { + { "signature_type", (ushort)1 }, + { "signature_size", (ushort)signature.Length }, + { "signature", signature }, + { "signature_key_size", (uint)512 }, + { "signature_key", groupKey.PublicBytes() }, + }; + var signatureInfoAttribute = new Dictionary + { + { "flags", (ushort)1 }, + { "tag", (ushort)8 }, + { "length", (uint)(DrmBCertSignatureInfo.Build(signatureInfo).Length + 8) }, + { "attribute", signatureInfo } + }; + ((List)newBCertContainer["attributes"]).Add(signatureInfoAttribute); + + return new Certificate(newBCertContainer); + } + + public Dictionary? GetAttribute(ushort type) + { + foreach (Dictionary attribute in (List)data["attributes"]) + { + if ((ushort)attribute["tag"] == type) + return attribute; + } + return null; + } + + public uint? GetSecurityLevel() + { + var basicInfo = (Dictionary)GetAttribute(1)?["attribute"]!; + return (uint?)basicInfo["security_level"]; + } + + private static string UnPad(byte[] bytes, byte strip = 0) + { + var i = bytes.Length - 1; + for (; i >= 0; i--) + if(bytes[i] != strip) + break; + return Encoding.UTF8.GetString(bytes[..(i + 1)]); + } + + public string GetName() + { + var manufacturerInfo = (Dictionary)GetAttribute(7)?["attribute"]!; + return $"{UnPad((byte[])manufacturerInfo["manufacturer_name"])} {UnPad((byte[])manufacturerInfo["model_name"])} {UnPad((byte[])manufacturerInfo["model_number"])}"; + } + + public byte[]? GetIssuerKey() + { + var keyInfoObject = GetAttribute(6); + if (keyInfoObject == null) + return null; + + var keyInfoAttribute = (Dictionary)keyInfoObject["attribute"]; + return ((List)keyInfoAttribute["cert_keys"]) + .Cast>() + .Where(key => ((List)key["usages"]) + .Cast() + .Contains(6)) + .Select(key => (byte[]?)key["key"]) + .FirstOrDefault(); + } + + public byte[]? Verify(byte[] publicKey, int index) + { + var signatureObject = GetAttribute(8); + if (signatureObject == null) + throw new InvalidCertificate($"No signature object found in certificate {index}"); + + var signatureAttribute = (Dictionary)signatureObject["attribute"]; + + var rawSignatureKey = (byte[])signatureAttribute["signature_key"]; + if (!publicKey.SequenceEqual(rawSignatureKey)) + throw new InvalidCertificate($"Signature keys of certificate {index} do not match"); + + var signatureKey = ECNamedCurveTable.GetByName("secp256r1").Curve.CreatePoint( + new BigInteger(1, rawSignatureKey[..32]), + new BigInteger(1, rawSignatureKey[32..]) + ); + + var signPayload = Dumps()[..^(int)(uint)signatureObject["length"]]; + var signature = (byte[])signatureAttribute["signature"]; + + if (!Crypto.Ecc256Verify(signatureKey, signPayload, signature)) + throw new InvalidCertificate($"Signature of certificate {index} is not authentic"); + + return GetIssuerKey(); + } + + public Dictionary GetData() + { + return data; + } +} + +public class CertificateChain(Dictionary data) : BCertStructs +{ + private static readonly byte[] Ecc256MsbCertRootIssuerPubKey = + "864d61cff2256e422c568b3c28001cfb3e1527658584ba0521b79b1828d936de1d826a8fc3e6e7fa7a90d5ca2946f1f64a2efb9f5dcffe7e434eb44293fac5ab" + .HexToBytes(); + + public static CertificateChain Loads(byte[] data) + { + return new CertificateChain(BCertChain.Parse(data)); + } + + public static CertificateChain Load(string path) + { + var bytes = File.ReadAllBytes(path); + return Loads(bytes); + } + + public byte[] Dumps() + { + return BCertChain.Build(data); + } + + public void Dump(string path) + { + var bytes = Dumps(); + File.WriteAllBytes(path, bytes); + } + + public Certificate Get(int index) + { + var certificates = (List)data["certificates"]; + return new Certificate((Dictionary)certificates[index]); + } + + public uint Count() + { + return (uint)data["certificate_count"]; + } + + public void Append(Certificate bCert) + { + data["certificate_count"] = Count() + 1; + ((List)data["certificates"]).Add(bCert.GetData()); + data["total_length"] = (uint)data["total_length"] + bCert.Dumps().Length; + } + + public void Prepend(Certificate bCert) + { + data["certificate_count"] = Count() + 1; + ((List)data["certificates"]).Insert(0, bCert.GetData()); + data["total_length"] = (uint)data["total_length"] + bCert.Dumps().Length; + } + + public void Remove(int index) + { + data["certificate_count"] = Count() - 1; + data["total_length"] = (uint)data["total_length"] - Get(index).Dumps().Length; + ((List)data["certificates"]).RemoveAt(index); + } + + public uint? GetSecurityLevel() + { + return Get(0).GetSecurityLevel(); + } + + public string GetName() + { + return Get(0).GetName(); + } + + public bool Verify() + { + var issuerKey = Ecc256MsbCertRootIssuerPubKey; + + try + { + for (var i = (int)(Count() - 1); i >= 0; i--) + { + var certificate = Get(i); + issuerKey = certificate.Verify(issuerKey!, i); + + if (issuerKey == null && i != 0) + { + throw new InvalidCertificate($"Certificate {i} is not valid"); + } + } + } + catch (InvalidCertificate e) + { + throw new InvalidCertificateChain("CertificateChain is not valid", e); + } + + return true; + } + + public Dictionary GetData() + { + return data; + } +} \ No newline at end of file diff --git a/csplayready/system/PSSH.cs b/csplayready/system/PSSH.cs new file mode 100644 index 0000000..8bd06a7 --- /dev/null +++ b/csplayready/system/PSSH.cs @@ -0,0 +1,59 @@ +using csplayready.constructcs; +using static csplayready.constructcs.ParserBuilder; + +namespace csplayready.system; + +public class PsshStructs() +{ + private static readonly Struct PlayreadyObject = new( + Int16ul("type"), + Int16ul("length"), + Switch("data", ctx => ctx["type"], i => i switch + { + 1 => UTF16String(string.Empty, ctx => ctx["length"]), + 2 => Bytes(string.Empty, ctx => ctx["length"]), + 3 => Bytes(string.Empty, ctx => ctx["length"]), + _ => throw new ArgumentOutOfRangeException(nameof(i), i, null) + }) + ); + + private static readonly Struct PlayreadyHeader = new( + Int32ul("length"), + Int16ul("record_count"), + Array("records", Child("playreadyObject", PlayreadyObject), ctx => ctx["record_count"]) + ); + + protected static readonly Struct PsshBox = new( + Int32ub("length"), + Const("pssh", "pssh"u8.ToArray()), + Int32ub("fullbox"), + Bytes("system_id", 16), + Int32ub("data_length"), + Child("playreadyHeader", PlayreadyHeader) + ); +} + +public class Pssh : PsshStructs +{ + private readonly Dictionary _data; + + public Pssh(byte[] data) + { + _data = PsshBox.Parse(data); + } + + public Pssh(string b64Data) + { + var data = Convert.FromBase64String(b64Data); + _data = PsshBox.Parse(data); + } + + public string[] GetWrmHeaders() + { + var playreadyHeader = (Dictionary)_data["playreadyHeader"]; + var records = (List)playreadyHeader["records"]; + return records.Where(dict => (ushort)((Dictionary)dict)["type"] == 1) + .Select(dict => Convert.ToString(((Dictionary)dict)["data"])!) + .ToArray(); + } +} \ No newline at end of file diff --git a/csplayready/system/Session.cs b/csplayready/system/Session.cs new file mode 100644 index 0000000..acca5e9 --- /dev/null +++ b/csplayready/system/Session.cs @@ -0,0 +1,14 @@ +using csplayready.crypto; +using csplayready.license; + +namespace csplayready.system; + +public class Session(int number) +{ + public readonly int Number = number; + public readonly int Id = new Random().Next(1, int.MaxValue); + public readonly XmlKey XmlKey = new XmlKey(); + public EccKey? SigningKey = null; + public EccKey? EncryptionKey = null; + public List Keys = []; +}