123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.Linq;
- using LipingShare.LCLib.Asn1Processor;
- using System.Text;
- using System.Threading;
-
- namespace UnityEngine.Purchasing.Security
- {
- /// <summary>
- /// This class will validate the Apple receipt is signed with the correct certificate.
- /// </summary>
- public class AppleValidator
- {
- private X509Cert cert;
- private AppleReceiptParser parser = new AppleReceiptParser();
-
- /// <summary>
- /// Constructs an instance with Apple Certificate.
- /// </summary>
- /// <param name="appleRootCertificate">The apple certificate.</param>
- public AppleValidator(byte[] appleRootCertificate)
- {
- cert = new X509Cert(appleRootCertificate);
- }
-
- /// <summary>
- /// Validate that the Apple receipt is signed correctly.
- /// </summary>
- /// <param name="receiptData">The Apple receipt to validate.</param>
- /// <returns>The parsed AppleReceipt</returns>
- /// <exception cref="InvalidSignatureException">The exception thrown if the receipt is incorrectly signed.</exception>
- public AppleReceipt Validate(byte[] receiptData)
- {
- PKCS7 receipt;
- var result = parser.Parse(receiptData, out receipt);
- if (!receipt.Verify(cert, result.receiptCreationDate))
- {
- throw new InvalidSignatureException();
- }
- return result;
- }
- }
-
- /// <summary>
- /// This class with parse the Apple receipt data received in byte[] into a AppleReceipt object
- /// </summary>
- public class AppleReceiptParser
- {
- // Cache the AppleReceipt object, PKCS7, and raw data for the most recently parsed data.
- private static Dictionary<string, object> _mostRecentReceiptData = new Dictionary<string, object>();
- private const string k_AppleReceiptKey = "k_AppleReceiptKey";
- private const string k_PKCS7Key = "k_PKCS7Key";
- private const string k_ReceiptBytesKey = "k_ReceiptBytesKey";
-
- /// <summary>
- /// Parse the Apple receipt data into a AppleReceipt object
- /// </summary>
- /// <param name="receiptData">Apple receipt data</param>
- /// <returns>The converted AppleReceipt object from the Apple receipt data</returns>
- public AppleReceipt Parse(byte[] receiptData)
- {
- return Parse(receiptData, out _);
- }
-
- internal AppleReceipt Parse(byte[] receiptData, out PKCS7 receipt)
- {
- // Avoid Culture-sensitive parsing for the duration of this method
- CultureInfo originalCulture = PushInvariantCultureOnThread();
-
- try
- {
- // Check to see if this receipt has been parsed before.
- // If so, return the most recent AppleReceipt and PKCS7; do not parse it again.
- if (_mostRecentReceiptData.ContainsKey(k_AppleReceiptKey) &&
- _mostRecentReceiptData.ContainsKey(k_PKCS7Key) &&
- _mostRecentReceiptData.ContainsKey(k_ReceiptBytesKey) &&
- ArrayEquals<byte>(receiptData, (byte[])_mostRecentReceiptData[k_ReceiptBytesKey]))
- {
- receipt = (PKCS7)_mostRecentReceiptData[k_PKCS7Key];
- return (AppleReceipt)_mostRecentReceiptData[k_AppleReceiptKey];
- }
-
- using (var stm = new System.IO.MemoryStream(receiptData))
- {
- Asn1Parser parser = new Asn1Parser();
- parser.LoadData(stm);
- receipt = new PKCS7(parser.RootNode);
- var result = ParseReceipt(receipt.data);
-
- // Cache the receipt info
- _mostRecentReceiptData[k_AppleReceiptKey] = result;
- _mostRecentReceiptData[k_PKCS7Key] = receipt;
- _mostRecentReceiptData[k_ReceiptBytesKey] = receiptData;
- return result;
- }
- }
- finally
- {
- PopCultureOffThread(originalCulture);
- }
- }
-
- /// <summary>
- /// Use InvariantCulture on this thread to avoid provoking Culture-sensitive reactions.
- /// E.g. when using DateTime.Parse we might load the host's current Culture, and that may
- /// have been stripped, and so this non-default culture would cause a crash.
- /// (*) NOTE Culture stripping for IL2CPP will be reduced in future Unitys in 2021
- /// (unity/il2cpp@5d3712f).
- /// </summary>
- /// <returns></returns>
- private static CultureInfo PushInvariantCultureOnThread()
- {
- var originalCulture = Thread.CurrentThread.CurrentCulture;
-
- Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
-
- return originalCulture;
- }
-
- /// <summary>
- /// Restores the original culture to this thread.
- /// </summary>
- /// <param name="originalCulture"></param>
- private static void PopCultureOffThread(CultureInfo originalCulture)
- {
- // Undo our parser Culture-preparations, safely
- Thread.CurrentThread.CurrentCulture = originalCulture;
- }
-
- private AppleReceipt ParseReceipt(Asn1Node data)
- {
- if (data == null || data.ChildNodeCount != 1)
- {
- throw new InvalidPKCS7Data();
- }
-
- Asn1Node set = GetSetNode(data);
-
- var result = new AppleReceipt();
- var inApps = new List<AppleInAppPurchaseReceipt>();
-
- for (int t = 0; t < set.ChildNodeCount; t++)
- {
- var node = set.GetChildNode(t);
- // Each node should contain three children.
-
- if (node.ChildNodeCount == 3)
- {
- var type = Asn1Util.BytesToLong(node.GetChildNode(0).Data);
- var value = node.GetChildNode(2);
- // See https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
- switch (type)
- {
- case 2:
- result.bundleID = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
- break;
- case 3:
- result.appVersion = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
- break;
- case 4:
- result.opaque = value.Data;
- break;
- case 5:
- result.hash = value.Data;
- break;
- case 12:
- var dateString = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
- result.receiptCreationDate = DateTime.Parse(dateString).ToUniversalTime();
- break;
- case 17:
- inApps.Add(ParseInAppReceipt(value.GetChildNode(0)));
- break;
- case 19:
- result.originalApplicationVersion = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
- break;
- }
- }
- }
-
- result.inAppPurchaseReceipts = inApps.ToArray();
- return result;
- }
-
- private Asn1Node GetSetNode(Asn1Node data)
- {
- if (data.IsIndefiniteLength && data.ChildNodeCount == 1)
- {
- // Explanation: Receipts received from the iOS StoreKit Testing encodes the receipt data one layer deeper than expected.
- // It also has nodes with "Indeterminate" or "Undefined" length, including the node in question.
- // Failing to go one node deeper will result in an unparsed receipt.
- var intermediateNode = data.GetChildNode(0);
- return intermediateNode.GetChildNode(0);
- }
- else
- {
- return data.GetChildNode(0);
- }
- }
-
- private AppleInAppPurchaseReceipt ParseInAppReceipt(Asn1Node inApp)
- {
- var result = new AppleInAppPurchaseReceipt();
- for (int t = 0; t < inApp.ChildNodeCount; t++)
- {
- var node = inApp.GetChildNode(t);
- if (node.ChildNodeCount == 3)
- {
- var type = Asn1Util.BytesToLong(node.GetChildNode(0).Data);
- var value = node.GetChildNode(2);
- switch (type)
- {
- case 1701:
- result.quantity = (int)Asn1Util.BytesToLong(value.GetChildNode(0).Data);
- break;
- case 1702:
- result.productID = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
- break;
- case 1703:
- result.transactionID = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
- break;
- case 1705:
- result.originalTransactionIdentifier = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
- break;
- case 1704:
- result.purchaseDate = TryParseDateTimeNode(value);
- break;
- case 1706:
- result.originalPurchaseDate = TryParseDateTimeNode(value);
- break;
- case 1708:
- result.subscriptionExpirationDate = TryParseDateTimeNode(value);
- break;
- case 1712:
- result.cancellationDate = TryParseDateTimeNode(value);
- break;
-
- case 1707:
- // looks like possibly a type?
- result.productType = (int)Asn1Util.BytesToLong(value.GetChildNode(0).Data);
- break;
-
- case 1713:
- // looks like possibly is_trial?
- result.isFreeTrial = (int)Asn1Util.BytesToLong(value.GetChildNode(0).Data);
- break;
-
- case 1719:
- result.isIntroductoryPricePeriod = (int)Asn1Util.BytesToLong(value.GetChildNode(0).Data);
- break;
-
- default:
- break;
-
-
- }
-
- }
- }
- return result;
- }
-
- /// <summary>
- /// Try and parse a DateTime, returning the minimum DateTime on failure.
- /// </summary>
- private static DateTime TryParseDateTimeNode(Asn1Node node)
- {
- var dateString = Encoding.UTF8.GetString(node.GetChildNode(0).Data);
- if (!string.IsNullOrEmpty(dateString))
- {
- return DateTime.Parse(dateString).ToUniversalTime();
- }
-
- return DateTime.MinValue;
- }
-
- /// <summary>
- /// Indicates whether both arrays are the same or contains the same information.
- ///
- /// This method is used to validate if the receipts are different.
- /// </summary>
- /// <param name="a">First object to validate against second object.</param>
- /// <param name="b">Second object to validate against first object.</param>
- /// <typeparam name="T">Type of object to check.</typeparam>
- /// <returns>Returns true if they are the same length and contain the same information or else returns false.</returns>
- public static bool ArrayEquals<T>(T[] a, T[] b) where T : IEquatable<T>
- {
- if (a.Length != b.Length)
- {
- return false;
- }
- for (int i = 0; i < a.Length; i++)
- {
- if (!a[i].Equals(b[i]))
- {
- return false;
- }
- }
- return true;
- }
- }
- }
|