233 lines
9.6 KiB
C#
233 lines
9.6 KiB
C#
|
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 + ")");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|