lib_unity_purchase/Runtime/Security/CrossPlatformValidator.cs

233 lines
9.6 KiB
C#
Raw Normal View History

2024-01-29 18:49:33 +08:00
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine.Purchasing;
namespace UnityEngine.Purchasing.Security
{
/// <summary>
/// Security exception for when the store is not supported by the <c>CrossPlatformValidator</c>.
/// </summary>
public class StoreNotSupportedException : IAPSecurityException
{
/// <summary>
/// Constructs an instance with a message.
/// </summary>
/// <param name="message"> The message that describes the error. </param>
public StoreNotSupportedException(string message) : base(message)
{
}
}
/// <summary>
/// Security exception for an invalid App bundle ID.
/// </summary>
public class InvalidBundleIdException : IAPSecurityException { }
/// <summary>
/// Security exception for invalid receipt Data.
/// </summary>
public class InvalidReceiptDataException : IAPSecurityException { }
/// <summary>
/// Security exception for a missing store secret.
/// </summary>
public class MissingStoreSecretException : IAPSecurityException
{
/// <summary>
/// Constructs an instance with a message.
/// </summary>
/// <param name="message"> The message that describes the error. </param>
public MissingStoreSecretException(string message) : base(message)
{
}
}
/// <summary>
/// Security exception for an invalid public key.
/// </summary>
public class InvalidPublicKeyException : IAPSecurityException
{
/// <summary>
/// Constructs an instance with a message.
/// </summary>
/// <param name="message"> The message that describes the error. </param>
public InvalidPublicKeyException(string message) : base(message)
{
}
}
/// <summary>
/// A generic exception for <c>CrossPlatformValidator</c> issues.
/// </summary>
public class GenericValidationException : IAPSecurityException
{
/// <summary>
/// Constructs an instance with a message.
/// </summary>
/// <param name="message"> The message that describes the error. </param>
public GenericValidationException(string message) : base(message)
{
}
}
/// <summary>
/// Class that validates receipts on multiple platforms that support the Security module.
/// Note that this currently only supports GooglePlay and Apple platforms.
/// </summary>
public class CrossPlatformValidator
{
private GooglePlayValidator google;
private AppleValidator apple;
private string googleBundleId, appleBundleId;
/// <summary>
/// Constructs an instance and checks the validity of the certification keys
/// which only takes input parameters for the supported platforms and uses a common bundle ID for Apple and GooglePlay.
/// </summary>
/// <param name="googlePublicKey"> The GooglePlay public key. </param>
/// <param name="appleRootCert"> The Apple certification key. </param>
/// <param name="appBundleId"> The bundle ID for all platforms. </param>
public CrossPlatformValidator(byte[] googlePublicKey, byte[] appleRootCert,
string appBundleId) : this(googlePublicKey, appleRootCert, null, appBundleId, appBundleId, null)
{
}
/// <summary>
/// Constructs an instance and checks the validity of the certification keys
/// which uses a common bundle ID for Apple and GooglePlay.
/// </summary>
/// <param name="googlePublicKey"> The GooglePlay public key. </param>
/// <param name="appleRootCert"> The Apple certification key. </param>
/// <param name="unityChannelPublicKey_not_used"> The Unity Channel public key. Not used because Unity Channel is no longer supported. </param>
/// <param name="appBundleId"> The bundle ID for all platforms. </param>
public CrossPlatformValidator(byte[] googlePublicKey, byte[] appleRootCert, byte[] unityChannelPublicKey_not_used,
string appBundleId)
: this(googlePublicKey, appleRootCert, null, appBundleId, appBundleId, appBundleId)
{
}
/// <summary>
/// Constructs an instance and checks the validity of the certification keys
/// which only takes input parameters for the supported platforms.
/// </summary>
/// <param name="googlePublicKey"> The GooglePlay public key. </param>
/// <param name="appleRootCert"> The Apple certification key. </param>
/// <param name="googleBundleId"> The GooglePlay bundle ID. </param>
/// <param name="appleBundleId"> The Apple bundle ID. </param>
public CrossPlatformValidator(byte[] googlePublicKey, byte[] appleRootCert,
string googleBundleId, string appleBundleId)
: this(googlePublicKey, appleRootCert, null, googleBundleId, appleBundleId, null)
{
}
/// <summary>
/// Constructs an instance and checks the validity of the certification keys.
/// </summary>
/// <param name="googlePublicKey"> The GooglePlay public key. </param>
/// <param name="appleRootCert"> The Apple certification key. </param>
/// <param name="unityChannelPublicKey_not_used"> The Unity Channel public key. Not used because Unity Channel is no longer supported. </param>
/// <param name="googleBundleId"> The GooglePlay bundle ID. </param>
/// <param name="appleBundleId"> The Apple bundle ID. </param>
/// <param name="xiaomiBundleId_not_used"> The Xiaomi bundle ID. Not used because Xiaomi is no longer supported. </param>
public CrossPlatformValidator(byte[] googlePublicKey, byte[] appleRootCert, byte[] unityChannelPublicKey_not_used,
string googleBundleId, string appleBundleId, string xiaomiBundleId_not_used)
{
try
{
if (null != googlePublicKey)
{
google = new GooglePlayValidator(googlePublicKey);
}
if (null != appleRootCert)
{
apple = new AppleValidator(appleRootCert);
}
}
catch (Exception ex)
{
throw new InvalidPublicKeyException("Cannot instantiate self with an invalid public key. (" +
ex.ToString() + ")");
}
this.googleBundleId = googleBundleId;
this.appleBundleId = appleBundleId;
}
/// <summary>
/// Validates a receipt.
/// </summary>
/// <param name="unityIAPReceipt"> The receipt to be validated. </param>
/// <returns> An array of receipts parsed from the validation process </returns>
public IPurchaseReceipt[] Validate(string unityIAPReceipt)
{
try
{
var wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(unityIAPReceipt);
if (null == wrapper)
{
throw new InvalidReceiptDataException();
}
var store = (string)wrapper["Store"];
var payload = (string)wrapper["Payload"];
switch (store)
{
case "GooglePlay":
{
if (null == google)
{
throw new MissingStoreSecretException(
"Cannot validate a Google Play receipt without a Google Play public key.");
}
var details = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
var json = (string)details["json"];
var sig = (string)details["signature"];
var result = google.Validate(json, sig);
// [IAP-1696] Check googleBundleId if packageName is present inside the signed receipt.
// packageName can be missing when the GPB v1 getPurchaseHistory API is used to fetch.
if (!string.IsNullOrEmpty(result.packageName) &&
!googleBundleId.Equals(result.packageName))
{
throw new InvalidBundleIdException();
}
return new IPurchaseReceipt[] { result };
}
case "AppleAppStore":
case "MacAppStore":
{
if (null == apple)
{
throw new MissingStoreSecretException(
"Cannot validate an Apple receipt without supplying an Apple root certificate");
}
var r = apple.Validate(Convert.FromBase64String(payload));
if (!appleBundleId.Equals(r.bundleID))
{
throw new InvalidBundleIdException();
}
return r.inAppPurchaseReceipts.ToArray();
}
default:
{
throw new StoreNotSupportedException("Store not supported: " + store);
}
}
}
catch (IAPSecurityException ex)
{
throw ex;
}
catch (Exception ex)
{
throw new GenericValidationException("Cannot validate due to unhandled exception. (" + ex + ")");
}
}
}
}