No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AppleValidator.cs 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using LipingShare.LCLib.Asn1Processor;
  6. using System.Text;
  7. using System.Threading;
  8. namespace UnityEngine.Purchasing.Security
  9. {
  10. /// <summary>
  11. /// This class will validate the Apple receipt is signed with the correct certificate.
  12. /// </summary>
  13. public class AppleValidator
  14. {
  15. private X509Cert cert;
  16. private AppleReceiptParser parser = new AppleReceiptParser();
  17. /// <summary>
  18. /// Constructs an instance with Apple Certificate.
  19. /// </summary>
  20. /// <param name="appleRootCertificate">The apple certificate.</param>
  21. public AppleValidator(byte[] appleRootCertificate)
  22. {
  23. cert = new X509Cert(appleRootCertificate);
  24. }
  25. /// <summary>
  26. /// Validate that the Apple receipt is signed correctly.
  27. /// </summary>
  28. /// <param name="receiptData">The Apple receipt to validate.</param>
  29. /// <returns>The parsed AppleReceipt</returns>
  30. /// <exception cref="InvalidSignatureException">The exception thrown if the receipt is incorrectly signed.</exception>
  31. public AppleReceipt Validate(byte[] receiptData)
  32. {
  33. PKCS7 receipt;
  34. var result = parser.Parse(receiptData, out receipt);
  35. if (!receipt.Verify(cert, result.receiptCreationDate))
  36. {
  37. throw new InvalidSignatureException();
  38. }
  39. return result;
  40. }
  41. }
  42. /// <summary>
  43. /// This class with parse the Apple receipt data received in byte[] into a AppleReceipt object
  44. /// </summary>
  45. public class AppleReceiptParser
  46. {
  47. // Cache the AppleReceipt object, PKCS7, and raw data for the most recently parsed data.
  48. private static Dictionary<string, object> _mostRecentReceiptData = new Dictionary<string, object>();
  49. private const string k_AppleReceiptKey = "k_AppleReceiptKey";
  50. private const string k_PKCS7Key = "k_PKCS7Key";
  51. private const string k_ReceiptBytesKey = "k_ReceiptBytesKey";
  52. /// <summary>
  53. /// Parse the Apple receipt data into a AppleReceipt object
  54. /// </summary>
  55. /// <param name="receiptData">Apple receipt data</param>
  56. /// <returns>The converted AppleReceipt object from the Apple receipt data</returns>
  57. public AppleReceipt Parse(byte[] receiptData)
  58. {
  59. return Parse(receiptData, out _);
  60. }
  61. internal AppleReceipt Parse(byte[] receiptData, out PKCS7 receipt)
  62. {
  63. // Avoid Culture-sensitive parsing for the duration of this method
  64. CultureInfo originalCulture = PushInvariantCultureOnThread();
  65. try
  66. {
  67. // Check to see if this receipt has been parsed before.
  68. // If so, return the most recent AppleReceipt and PKCS7; do not parse it again.
  69. if (_mostRecentReceiptData.ContainsKey(k_AppleReceiptKey) &&
  70. _mostRecentReceiptData.ContainsKey(k_PKCS7Key) &&
  71. _mostRecentReceiptData.ContainsKey(k_ReceiptBytesKey) &&
  72. ArrayEquals<byte>(receiptData, (byte[])_mostRecentReceiptData[k_ReceiptBytesKey]))
  73. {
  74. receipt = (PKCS7)_mostRecentReceiptData[k_PKCS7Key];
  75. return (AppleReceipt)_mostRecentReceiptData[k_AppleReceiptKey];
  76. }
  77. using (var stm = new System.IO.MemoryStream(receiptData))
  78. {
  79. Asn1Parser parser = new Asn1Parser();
  80. parser.LoadData(stm);
  81. receipt = new PKCS7(parser.RootNode);
  82. var result = ParseReceipt(receipt.data);
  83. // Cache the receipt info
  84. _mostRecentReceiptData[k_AppleReceiptKey] = result;
  85. _mostRecentReceiptData[k_PKCS7Key] = receipt;
  86. _mostRecentReceiptData[k_ReceiptBytesKey] = receiptData;
  87. return result;
  88. }
  89. }
  90. finally
  91. {
  92. PopCultureOffThread(originalCulture);
  93. }
  94. }
  95. /// <summary>
  96. /// Use InvariantCulture on this thread to avoid provoking Culture-sensitive reactions.
  97. /// E.g. when using DateTime.Parse we might load the host's current Culture, and that may
  98. /// have been stripped, and so this non-default culture would cause a crash.
  99. /// (*) NOTE Culture stripping for IL2CPP will be reduced in future Unitys in 2021
  100. /// (unity/il2cpp@5d3712f).
  101. /// </summary>
  102. /// <returns></returns>
  103. private static CultureInfo PushInvariantCultureOnThread()
  104. {
  105. var originalCulture = Thread.CurrentThread.CurrentCulture;
  106. Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
  107. return originalCulture;
  108. }
  109. /// <summary>
  110. /// Restores the original culture to this thread.
  111. /// </summary>
  112. /// <param name="originalCulture"></param>
  113. private static void PopCultureOffThread(CultureInfo originalCulture)
  114. {
  115. // Undo our parser Culture-preparations, safely
  116. Thread.CurrentThread.CurrentCulture = originalCulture;
  117. }
  118. private AppleReceipt ParseReceipt(Asn1Node data)
  119. {
  120. if (data == null || data.ChildNodeCount != 1)
  121. {
  122. throw new InvalidPKCS7Data();
  123. }
  124. Asn1Node set = GetSetNode(data);
  125. var result = new AppleReceipt();
  126. var inApps = new List<AppleInAppPurchaseReceipt>();
  127. for (int t = 0; t < set.ChildNodeCount; t++)
  128. {
  129. var node = set.GetChildNode(t);
  130. // Each node should contain three children.
  131. if (node.ChildNodeCount == 3)
  132. {
  133. var type = Asn1Util.BytesToLong(node.GetChildNode(0).Data);
  134. var value = node.GetChildNode(2);
  135. // See https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
  136. switch (type)
  137. {
  138. case 2:
  139. result.bundleID = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
  140. break;
  141. case 3:
  142. result.appVersion = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
  143. break;
  144. case 4:
  145. result.opaque = value.Data;
  146. break;
  147. case 5:
  148. result.hash = value.Data;
  149. break;
  150. case 12:
  151. var dateString = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
  152. result.receiptCreationDate = DateTime.Parse(dateString).ToUniversalTime();
  153. break;
  154. case 17:
  155. inApps.Add(ParseInAppReceipt(value.GetChildNode(0)));
  156. break;
  157. case 19:
  158. result.originalApplicationVersion = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
  159. break;
  160. }
  161. }
  162. }
  163. result.inAppPurchaseReceipts = inApps.ToArray();
  164. return result;
  165. }
  166. private Asn1Node GetSetNode(Asn1Node data)
  167. {
  168. if (data.IsIndefiniteLength && data.ChildNodeCount == 1)
  169. {
  170. // Explanation: Receipts received from the iOS StoreKit Testing encodes the receipt data one layer deeper than expected.
  171. // It also has nodes with "Indeterminate" or "Undefined" length, including the node in question.
  172. // Failing to go one node deeper will result in an unparsed receipt.
  173. var intermediateNode = data.GetChildNode(0);
  174. return intermediateNode.GetChildNode(0);
  175. }
  176. else
  177. {
  178. return data.GetChildNode(0);
  179. }
  180. }
  181. private AppleInAppPurchaseReceipt ParseInAppReceipt(Asn1Node inApp)
  182. {
  183. var result = new AppleInAppPurchaseReceipt();
  184. for (int t = 0; t < inApp.ChildNodeCount; t++)
  185. {
  186. var node = inApp.GetChildNode(t);
  187. if (node.ChildNodeCount == 3)
  188. {
  189. var type = Asn1Util.BytesToLong(node.GetChildNode(0).Data);
  190. var value = node.GetChildNode(2);
  191. switch (type)
  192. {
  193. case 1701:
  194. result.quantity = (int)Asn1Util.BytesToLong(value.GetChildNode(0).Data);
  195. break;
  196. case 1702:
  197. result.productID = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
  198. break;
  199. case 1703:
  200. result.transactionID = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
  201. break;
  202. case 1705:
  203. result.originalTransactionIdentifier = Encoding.UTF8.GetString(value.GetChildNode(0).Data);
  204. break;
  205. case 1704:
  206. result.purchaseDate = TryParseDateTimeNode(value);
  207. break;
  208. case 1706:
  209. result.originalPurchaseDate = TryParseDateTimeNode(value);
  210. break;
  211. case 1708:
  212. result.subscriptionExpirationDate = TryParseDateTimeNode(value);
  213. break;
  214. case 1712:
  215. result.cancellationDate = TryParseDateTimeNode(value);
  216. break;
  217. case 1707:
  218. // looks like possibly a type?
  219. result.productType = (int)Asn1Util.BytesToLong(value.GetChildNode(0).Data);
  220. break;
  221. case 1713:
  222. // looks like possibly is_trial?
  223. result.isFreeTrial = (int)Asn1Util.BytesToLong(value.GetChildNode(0).Data);
  224. break;
  225. case 1719:
  226. result.isIntroductoryPricePeriod = (int)Asn1Util.BytesToLong(value.GetChildNode(0).Data);
  227. break;
  228. default:
  229. break;
  230. }
  231. }
  232. }
  233. return result;
  234. }
  235. /// <summary>
  236. /// Try and parse a DateTime, returning the minimum DateTime on failure.
  237. /// </summary>
  238. private static DateTime TryParseDateTimeNode(Asn1Node node)
  239. {
  240. var dateString = Encoding.UTF8.GetString(node.GetChildNode(0).Data);
  241. if (!string.IsNullOrEmpty(dateString))
  242. {
  243. return DateTime.Parse(dateString).ToUniversalTime();
  244. }
  245. return DateTime.MinValue;
  246. }
  247. /// <summary>
  248. /// Indicates whether both arrays are the same or contains the same information.
  249. ///
  250. /// This method is used to validate if the receipts are different.
  251. /// </summary>
  252. /// <param name="a">First object to validate against second object.</param>
  253. /// <param name="b">Second object to validate against first object.</param>
  254. /// <typeparam name="T">Type of object to check.</typeparam>
  255. /// <returns>Returns true if they are the same length and contain the same information or else returns false.</returns>
  256. public static bool ArrayEquals<T>(T[] a, T[] b) where T : IEquatable<T>
  257. {
  258. if (a.Length != b.Length)
  259. {
  260. return false;
  261. }
  262. for (int i = 0; i < a.Length; i++)
  263. {
  264. if (!a[i].Equals(b[i]))
  265. {
  266. return false;
  267. }
  268. }
  269. return true;
  270. }
  271. }
  272. }