iOS內(nèi)購IAP(十四) —— IAP的收據(jù)驗證(一)

版本記錄

版本號 時間
V1.0 2019.01.09

前言

大家都知道踪栋,iOS虛擬商品如寶石、金幣等都需要走內(nèi)購图毕,和蘋果三七分成夷都,如果這類商品不走內(nèi)購那么上不去架或者上架以后被發(fā)現(xiàn)而被下架。最近有一個項目需要增加內(nèi)購支付功能予颤,所以最近又重新集成并整理了下囤官,希望對大家有所幫助。感興趣的可以參考上面幾篇蛤虐。
1. iOS內(nèi)購IAP(一) —— 基礎配置篇(一)
2. iOS內(nèi)購IAP(二) —— 工程實踐(一)
3. iOS內(nèi)購IAP(三) —— 編程指南之關于內(nèi)購(一)
4. iOS內(nèi)購IAP(四) —— 編程指南之設計您的應用程序的產(chǎn)品(一)
5. iOS內(nèi)購IAP(五) —— 編程指南之檢索產(chǎn)品信息(一)
6. iOS內(nèi)購IAP(六) —— 編程指南之請求支付(一)
7. iOS內(nèi)購IAP(七) —— 編程指南之促進應用內(nèi)購買(一)
8. iOS內(nèi)購IAP(八) —— 編程指南之提供產(chǎn)品(一)
9. iOS內(nèi)購IAP(九) —— 編程指南之處理訂閱(一)
10. iOS內(nèi)購IAP(十) —— 編程指南之恢復購買的產(chǎn)品(一)
11. iOS內(nèi)購IAP(十一) —— 編程指南之準備App審核(一)
12. iOS內(nèi)購IAP(十二) —— 一個詳細的內(nèi)購流程(一)
13. iOS內(nèi)購IAP(十三) —— 一個詳細的內(nèi)購流程(二)

開始

首先看下寫作環(huán)境

Swift 4.2, iOS 12, Xcode 10

在本教程中党饮,您將了解應用內(nèi)購買的收據(jù)如何工作以及如何驗證它們,以確保您的用戶已為您提供的商品付款驳庭。

付費軟件一直存在一個問題刑顺,即某些用戶試圖在不購買軟件的情況下使用該軟件或欺詐性地訪問應用內(nèi)購買。 收據(jù)提供了確認這些購買的工具。 他們通過提供銷售記錄來實現(xiàn)這一目標蹲堂。 每當用戶購買應用程序荞驴,進行應用內(nèi)購買或更新應用程序時,App Store都會在應用程序包中生成收據(jù)贯城。

在本教程中熊楼,您將了解這些收據(jù)的工作原理以及它們在設備上的驗證方式。 在本教程中能犯,您應該熟悉應用內(nèi)購買和StoreKit鲫骗。 您將需要一個iOS開發(fā)人員帳戶,一個用于測試的真實設備踩晶,訪問iOS開發(fā)人員中心和App Store Connect执泰。


What Is a Receipt?

收據(jù)包含應用程序包中的單個文件。 該文件采用稱為PKCS#7的格式渡蜻。 這是應用了加密技術的數(shù)據(jù)的標準格式术吝。 容器包含有效負載(payload),證書鏈(chain of certificates)和數(shù)字簽名(digital signature)茸苇。 您使用證書鏈和數(shù)字簽名來驗證Apple是否生成了收據(jù)排苍。

有效負載(payload)由一組稱為ASN.1的跨平臺格式的憑據(jù)屬性組成。 這些屬性中的每一個都包含類型学密,版本和值(type, version and value)淘衙。 這些代表收據(jù)的內(nèi)容。 您的應用使用這些屬性來確定收據(jù)對設備有效以及用戶購買了什么腻暮。


Loading the Receipt

打開入門項目彤守。入門項目是支持StoreKit和應用內(nèi)購買的iPhone應用程序。

要測試收據(jù)驗證哭靖,您必須在真實設備上運行該應用程序具垫,因為它在模擬器中不起作用。您需要開發(fā)證書和沙盒帳戶试幽。通過XCode測試應用程序時筝蚕,默認情況下應用程序不會有收據(jù)。如果不存在抡草,則starter app會實現(xiàn)請求刷新的證書饰及。

加密代碼很復雜蔗坯,很容易出錯康震。最好使用已知且經(jīng)過驗證的庫,而不是嘗試編寫自己的庫宾濒。本教程使用OpenSSL庫來完成驗證加密和解碼收據(jù)中提供的ASN.1數(shù)據(jù)的大部分工作腿短。 OpenSSL不是非常Swift友好的,所以在本教程中你將創(chuàng)建一個Swift包裝器。

為iPhone編譯OpenSSL并不是一個簡單的過程橘忱。如果您想自己動手赴魁,可以在GitHub上找到腳本和說明。入門項目包括OpenSSL文件夾中最新版本的OpenSSL 1.1.1钝诚。它被編譯為靜態(tài)庫颖御,使修改更加困難。這包括文件夾以及C頭文件凝颇。該項目還包括使用Swift的OpenSSL庫的橋接頭潘拱。

注意:您可能想知道為什么使用OpenSSL而不是iOS內(nèi)置的CommonCrypto框架,而且靜態(tài)OpenSSL庫為您的應用程序包添加了大約40MB拧略。 原因是如果用戶越獄他們的設備芦岂,使用黑客版本替換CommonCrypto將很容易解決這些問題。 bundle中的靜態(tài)庫是一個更難攻擊的目標垫蛆。

入門項目包括一個起始的Receipt類禽最。 它還包含一個靜態(tài)方法:isReceiptPresent()。 此方法確定是否存在收據(jù)文件袱饭。 如果沒有川无,它會使用StoreKit在嘗試驗證之前請求刷新收據(jù)。 如果收據(jù)不存在虑乖,您的應用應該做類似的事情舀透。

打開Receipt.swift。 在類聲明結束時為類添加新的自定義初始值設定項:

init() {
  guard let payload = loadReceipt() else {
    return
  }
}

要開始驗證决左,您需要將收據(jù)作為Data對象愕够。 將以下新方法添加到init()下面的Receipt以加載收據(jù)并返回PKCS#7數(shù)據(jù)結構:

private func loadReceipt() -> UnsafeMutablePointer<PKCS7>? {
  // Load the receipt into a Data object
  guard 
    let receiptUrl = Bundle.main.appStoreReceiptURL,
    let receiptData = try? Data(contentsOf: receiptUrl) 
    else {
      receiptStatus = .noReceiptPresent
      return nil
  }
}

此代碼獲取收據(jù)的位置,并嘗試將其作為Data對象加載佛猛。 如果不存在收據(jù)或收據(jù)不會作為Data對象加載惑芭,則驗證失敗。 如果在驗證收據(jù)期間的任何時候檢查失敗继找,則整個驗證失敗遂跟。 代碼將原因存儲在類的receiptStatus屬性中。

現(xiàn)在您在Data對象中有了收據(jù)婴渡,您可以使用OpenSSL處理內(nèi)容幻锁。 OpenSSL函數(shù)是用C語言編寫的,通常使用指針和其他底層方法边臼。 在loadReceipt()的末尾添加以下代碼:

// 1
let receiptBIO = BIO_new(BIO_s_mem())
let receiptBytes: [UInt8] = .init(receiptData)
BIO_write(receiptBIO, receiptBytes, Int32(receiptData.count))
// 2
let receiptPKCS7 = d2i_PKCS7_bio(receiptBIO, nil)
BIO_free(receiptBIO)
// 3
guard receiptPKCS7 != nil else {
  receiptStatus = .unknownReceiptFormat
  return nil
}

這段代碼的工作原理:

  • 1) 要在OpenSSL中使用envelope哄尔,首先必須將其轉(zhuǎn)換為BIO,這是OpenSSL使用的抽象I / O結構柠并。要創(chuàng)建一個新的BIO對象岭接,OpenSSL需要一個指向C中原始數(shù)據(jù)字節(jié)的指針富拗。C字節(jié)是一個Swift UInt8。由于您可以將任何SequenceData表示的數(shù)組初始化為UInt8序列鸣戴,因此只需傳入Data實例即可創(chuàng)建[UInt8]數(shù)組啃沪。然后,您將該數(shù)組作為原始字節(jié)指針傳遞窄锅。這是可能的创千,因為Swift隱式橋接函數(shù)參數(shù),創(chuàng)建指向數(shù)組元素的指針入偷。然后签餐,OpenSSL調(diào)用將收據(jù)寫入BIO結構。
  • 2) 您將BIO對象轉(zhuǎn)換為名為receiptPKCS7OpenSSL PKCS7數(shù)據(jù)結構盯串。完成后氯檐,您不再需要BIO對象并可以釋放先前為其分配的內(nèi)存。
  • 3) 如果出現(xiàn)任何問題体捏,那么receiptPKCS7將是一個沒有指向或指向nil的指針冠摄。在這種情況下,請設置狀態(tài)以反映驗證失敗几缭。

接下來河泳,您需要確保容器包含簽名和數(shù)據(jù)。將以下代碼添加到loadReceipt()方法的末尾以執(zhí)行這些檢查:

// Check that the container has a signature
guard OBJ_obj2nid(receiptPKCS7!.pointee.type) == NID_pkcs7_signed else {
  receiptStatus = .invalidPKCS7Signature
  return nil
}

// Check that the container contains data
let receiptContents = receiptPKCS7!.pointee.d.sign.pointee.contents
guard OBJ_obj2nid(receiptContents?.pointee.type) == NID_pkcs7_data else {
  receiptStatus = .invalidPKCS7Type
  return nil
}

return receiptPKCS7

C通常使用結構體處理復雜數(shù)據(jù)年栓。 與Swift結構不同拆挥,C結構僅包含沒有方法或其他元素的數(shù)據(jù)。 對C中結構的引用是對內(nèi)存位置的引用 - 指向數(shù)據(jù)結構的指針某抓。

存在各種UnsafePointer類型以允許混合Swift和C代碼纸兔。 OpenSSL函數(shù)需要一個指針,而不是您可能更熟悉的Swift類和結構否副。 receiptPKCS7是指向保存PKCS#7包絡的數(shù)據(jù)結構的指針汉矿。 UnsafePointerpointee屬性遵循指向數(shù)據(jù)結構的指針。

引用C中指針指向的過程通常足以擁有一個特殊的運算符- >备禀。 指針的pointee屬性在Swift中執(zhí)行此引用迄损。

如果檢查成功瑞驱,則該方法返回指向結構體的指針。 現(xiàn)在您的envelope格式正確且包含數(shù)據(jù)辆亏,您應該驗證Apple是否已對其進行簽名逞姿。


Validating Apple Signed the Receipt

PKCS#7容器使用具有兩個組件的公鑰加密袜啃。 一個組件是與每個人共享的公鑰盏袄。 第二個是私人安全密鑰芳绩。 Apple可以使用私鑰對數(shù)據(jù)進行數(shù)字簽名,因此任何擁有相應公鑰的人都可以確保擁有私鑰的人進行簽名柴淘。

對于收據(jù)迫淹,Apple使用其私鑰對收據(jù)進行簽名,并使用Apple的公鑰進行驗證为严。 證書包含有關這些密鑰的信息敛熬。

通常使用證書來簽署構成證書鏈的其他證書。 這樣做可以降低損害任何一個證書的風險第股,因為它只影響鏈中較低的證書应民。 這允許鏈頂部的單個根證書驗證簽名和中間證書,而無需由根證書直接簽名夕吻。

OpenSSL可以為您處理此檢查诲锹。 在init()的末尾添加以下調(diào)用:

guard validateSigning(payload) else {
  return
}

現(xiàn)在在Receipt末尾添加一個新方法來時執(zhí)行檢查:

private func validateSigning(_ receipt: UnsafeMutablePointer<PKCS7>?) -> Bool {
  guard 
    let rootCertUrl = Bundle.main
      .url(forResource: "AppleIncRootCertificate", withExtension: "cer"),
    let rootCertData = try? Data(contentsOf: rootCertUrl) 
    else {
      receiptStatus = .invalidAppleRootCertificate
      return false
  }
  
  let rootCertBio = BIO_new(BIO_s_mem())
  let rootCertBytes: [UInt8] = .init(rootCertData)
  BIO_write(rootCertBio, rootCertBytes, Int32(rootCertData.count))
  let rootCertX509 = d2i_X509_bio(rootCertBio, nil)
  BIO_free(rootCertBio)
}

此代碼從bundle加載Apple的根證書并將其轉(zhuǎn)換為BIO對象。 請注意涉馅,不同的函數(shù)調(diào)用反映您正在加載X.509格式證書而不是PKCS容器归园。 添加以下代碼以完成validateSigning(_ :)

// 1
let store = X509_STORE_new()
X509_STORE_add_cert(store, rootCertX509)

// 2
OPENSSL_init_crypto(UInt64(OPENSSL_INIT_ADD_ALL_DIGESTS), nil)

// 3
let verificationResult = PKCS7_verify(receipt, nil, store, nil, nil, 0)
guard verificationResult == 1  else {
  receiptStatus = .failedAppleSignature
  return false
}

return true

這段代碼的工作原理:

  • 1) 使用OpenSSL創(chuàng)建X.509證書庫。 該庫是用于驗證的證書的容器稚矿。 代碼將加載的根證書添加到庫庸诱。
  • 2) 初始化OpenSSL以進行證書驗證。
  • 3) 使用PKCS7_verify(_:_:_:_:_:_ :)從簽署收據(jù)的根證書中驗證鏈中的證書晤揣。 如果是桥爽,則該函數(shù)返回1。任何其他值表示該envelope未由Apple簽名昧识,因此驗證失敗钠四。

Reading Data in the Receipt

驗證Apple簽署了收據(jù)后,您現(xiàn)在可以閱讀收據(jù)內(nèi)容跪楞。 如前所述缀去,有效載荷(payload)的內(nèi)容是一組ASN.1值。 您將使用讀取此格式的OpenSSL函數(shù)甸祭。

Receipt已包含存儲payload內(nèi)容的屬性朵耕。 在init()的末尾添加以下代碼:

readReceipt(payload)

loadReceipt()之后添加以下方法以開始讀取收據(jù)數(shù)據(jù):

private func readReceipt(_ receiptPKCS7: UnsafeMutablePointer<PKCS7>?) {
  // Get a pointer to the start and end of the ASN.1 payload
  let receiptSign = receiptPKCS7?.pointee.d.sign
  let octets = receiptSign?.pointee.contents.pointee.d.data
  var ptr = UnsafePointer(octets?.pointee.data)
  let end = ptr!.advanced(by: Int(octets!.pointee.length))
}

此代碼從PKCS7結構獲取指向有效負載起點的指針 - 作為ptr。 然后淋叶,您將指針放在有效負載的末尾阎曹。 將以下代碼添加到readReceipt(_ :)以開始解析有效內(nèi)容:

var type: Int32 = 0
var xclass: Int32 = 0
var length: Int = 0

ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
guard type == V_ASN1_SET else {
  receiptStatus = .unexpectedASN1Type
  return
}

存儲有關每個ASN.1對象的信息有三個變量。 ASN1_get_object(_:_:_:_:_ :)讀取緩沖區(qū)以獲取第一個對象煞檩。指針更新到下一個對象处嫌。

C函數(shù)通常使用指向變量的指針從函數(shù)返回多個值,并直接更新這些對象斟湃。這類似于Swift中的inout參數(shù)熏迹。 符號獲取指向?qū)ο蟮闹羔槨T摵瘮?shù)返回數(shù)據(jù)的長度(length)凝赛,ASN.1對象類型(type)和ASN.1 tag值(xclass)注暗。

最后一個參數(shù)是要讀取的最長長度坛缕。提供此功能可防止因讀取超出存儲區(qū)末尾而導致的安全問題。

然后驗證有效內(nèi)容中第一個項的類型是否為ASN.1集捆昏。如果不是赚楚,則有效載荷無效。否則骗卜,您可以開始閱讀該集的內(nèi)容宠页。您將對ASN1_get_object(_:_:_:_:_ :)使用類似的調(diào)用來讀取有效負載中的所有數(shù)據(jù)。 ASN1Helpers.swift包含幾個輔助方法寇仓,它們將收據(jù)中的ASN.1數(shù)據(jù)類型讀取為可以為空的Swift值举户。在readReceipt(_ :)的末尾添加此代碼:

// 1
while ptr! < end {
  // 2
  ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
  guard type == V_ASN1_SEQUENCE else {
    receiptStatus = .unexpectedASN1Type
    return
  }
  
  // 3
  guard let attributeType = readASN1Integer(ptr: &ptr, maxLength: length) else {
    receiptStatus = .unexpectedASN1Type
    return
  }
  
  // 4
  guard let _ = readASN1Integer(ptr: &ptr, maxLength: ptr!.distance(to: end)) else {
    receiptStatus = .unexpectedASN1Type
    return
  }
  
  // 5
  ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
  guard type == V_ASN1_OCTET_STRING else {
    receiptStatus = .unexpectedASN1Type
    return
  }

  // Insert attribute reading code
}

這段代碼的作用:

  • 1) 創(chuàng)建一個循環(huán),直到指針到達有效負載的末尾遍烦。 那時你已經(jīng)處理了整個有效載荷俭嘁。
  • 2) 檢查對象是否為序列。 每個屬性都是三個字段的序列:type, version, data服猪。
  • 3) 獲取您將很快使用的屬性類型 - 整數(shù)兄淫。
  • 4) 讀取屬性版本,整數(shù)蔓姚。 您不需要它進行收據(jù)驗證捕虽。
  • 5) 檢查下一個值是否為字節(jié)序列。

和以前一樣坡脐,如果任何值不符合預期泄私,則設置狀態(tài)代碼并且驗證失敗。

您現(xiàn)在擁有有關當前屬性的信息备闲。 您還具有數(shù)據(jù)類型和指向此屬性的數(shù)據(jù)的指針晌端。 Appledocuments the attributes in a receipt

您將使用switch語句來處理收據(jù)中找到的屬性類型恬砂。 使用以下內(nèi)容替換// Insert attribute reading code here注釋:

switch attributeType {
case 2: // The bundle identifier
  var stringStartPtr = ptr
  bundleIdString = readASN1String(ptr: &stringStartPtr, maxLength: length)
  bundleIdData = readASN1Data(ptr: ptr!, length: length)
  
case 3: // Bundle version
  var stringStartPtr = ptr
  bundleVersionString = readASN1String(ptr: &stringStartPtr, maxLength: length)
  
case 4: // Opaque value
  let dataStartPtr = ptr!
  opaqueData = readASN1Data(ptr: dataStartPtr, length: length)
  
case 5: // Computed GUID (SHA-1 Hash)
  let dataStartPtr = ptr!
  hashData = readASN1Data(ptr: dataStartPtr, length: length)
  
case 12: // Receipt Creation Date
  var dateStartPtr = ptr
  receiptCreationDate = readASN1Date(ptr: &dateStartPtr, maxLength: length)

case 17: // IAP Receipt
  print("IAP Receipt.")
  
case 19: // Original App Version
  var stringStartPtr = ptr
  originalAppVersion = readASN1String(ptr: &stringStartPtr, maxLength: length)
  
case 21: // Expiration Date
  var dateStartPtr = ptr
  expirationDate = readASN1Date(ptr: &dateStartPtr, maxLength: length)
  
default: // Ignore other attributes in receipt
  print("Not processing attribute type: \(attributeType)")
}

// Advance pointer to the next item
ptr = ptr!.advanced(by: length)

此代碼使用每個屬性的類型來調(diào)用適當?shù)妮o助函數(shù)咧纠,該函數(shù)將值放入類的屬性中。 讀取每個值后泻骤,最后一行將指針前進到下一個屬性的開頭漆羔,然后繼續(xù)循環(huán)。


Reading In-App Purchases

應用內(nèi)購買的屬性需要更復雜的處理狱掂。 應用內(nèi)購買不是單個整數(shù)或字符串演痒,而是此集合中的另一個ASN.1集。 IAPReceipt.swift包含一個用于存儲內(nèi)容的IAPReceipt趋惨。 該集的格式與包含它的格式相同鸟顺,并且讀取它的代碼非常相似。 將以下初始化程序添加到IAPReceipt

init?(with pointer: inout UnsafePointer<UInt8>?, payloadLength: Int) {
  let endPointer = pointer!.advanced(by: payloadLength)
  var type: Int32 = 0
  var xclass: Int32 = 0
  var length = 0
  
  ASN1_get_object(&pointer, &length, &type, &xclass, payloadLength)
  guard type == V_ASN1_SET else {
    return nil
  }
  
  while pointer! < endPointer {
    ASN1_get_object(&pointer, &length, &type, &xclass, pointer!.distance(to: endPointer))
    guard type == V_ASN1_SEQUENCE else {
      return nil
    }
    guard let attributeType = readASN1Integer(ptr: &pointer,
                                maxLength: pointer!.distance(to: endPointer)) 
      else {
        return nil
    }
    // Attribute version must be an integer, but not using the value
    guard let _ = readASN1Integer(ptr: &pointer,
                    maxLength: pointer!.distance(to: endPointer)) 
      else {
        return nil
    }
    ASN1_get_object(&pointer, &length, &type, &xclass, pointer!.distance(to: endPointer))
    guard type == V_ASN1_OCTET_STRING else {
      return nil
    }
    
    switch attributeType {
    case 1701:
      var p = pointer
      quantity = readASN1Integer(ptr: &p, maxLength: length)
    case 1702:
      var p = pointer
      productIdentifier = readASN1String(ptr: &p, maxLength: length)
    case 1703:
      var p = pointer
      transactionIdentifer = readASN1String(ptr: &p, maxLength: length)
    case 1705:
      var p = pointer
      originalTransactionIdentifier = readASN1String(ptr: &p, maxLength: length)
    case 1704:
      var p = pointer
      purchaseDate = readASN1Date(ptr: &p, maxLength: length)
    case 1706:
      var p = pointer
      originalPurchaseDate = readASN1Date(ptr: &p, maxLength: length)
    case 1708:
      var p = pointer
      subscriptionExpirationDate = readASN1Date(ptr: &p, maxLength: length)
    case 1712:
      var p = pointer
      subscriptionCancellationDate = readASN1Date(ptr: &p, maxLength: length)
    case 1711:
      var p = pointer
      webOrderLineId = readASN1Integer(ptr: &p, maxLength: length)
    default:
      break
    }
    
    pointer = pointer!.advanced(by: length)
  }
}

與讀取初始集的代碼的唯一區(qū)別來自應用內(nèi)購買中發(fā)現(xiàn)的不同類型值器虾。 如果在初始化的任何時刻它發(fā)現(xiàn)了一個意外的值讯嫂,它返回nil并停止蹦锋。

回到Receipt.swift,用以下內(nèi)容替換case 17: // IAP Receipt in readReceipt(_:)以使用新對象:

case 17: // IAP Receipt
  var iapStartPtr = ptr
  let parsedReceipt = IAPReceipt(with: &iapStartPtr, payloadLength: length)
  if let newReceipt = parsedReceipt {
    inAppReceipts.append(newReceipt)
  }

您將當前指針傳遞給init()以讀取包含IAP的集合欧芽。 如果返回有效的收據(jù)項莉掂,則會將其添加到陣列中。 請注意渐裸,對于耗材和非續(xù)訂訂閱(consumable and non-renewing subscriptions)巫湘,應用內(nèi)購買僅在購買時出現(xiàn)一次装悲。 它們未包含在將來的收據(jù)更新中昏鹃。 非消費品和自動續(xù)訂訂閱(Non-consumable and auto-renewing subscriptions)將始終顯示在收據(jù)中。


Validating the Receipt

讀取收據(jù)有效負載后诀诊,您可以完成驗證收據(jù)洞渤。 將此代碼添加到Receipt中的init()

validateReceipt()

添加一個新方法到Receipt

private func validateReceipt() {
  guard 
    let idString = bundleIdString,
    let version = bundleVersionString,
    let _ = opaqueData,
    let hash = hashData 
    else {
      receiptStatus = .missingComponent
      return
  }
}

此代碼確保收據(jù)包含驗證所需的元素。 如果缺少任何內(nèi)容属瓣,則驗證失敗载迄。 在validateReceipt()的末尾添加以下代碼:

// Check the bundle identifier
guard let appBundleId = Bundle.main.bundleIdentifier else {
    receiptStatus = .unknownFailure 
    return
}

guard idString == appBundleId else {
  receiptStatus = .invalidBundleIdentifier
  return
}

此代碼獲取應用程序的包標識符,并將其與收據(jù)中的包標識符進行比較抡蛙。 如果它們不匹配护昧,則收據(jù)可能是從另一個應用程序復制而無效。

驗證標識符后添加以下代碼:

// Check the version
guard let appVersionString = 
  Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String else {
  receiptStatus = .unknownFailure
  return
}
guard version == appVersionString else {
  receiptStatus = .invalidVersionIdentifier
  return
}

您可以將收據(jù)中存儲的版本與應用的當前版本進行比較粗截。 如果值不匹配惋耙,則收據(jù)可能是從應用程序的其他版本復制的,因此應使用應用程序更新收據(jù)熊昌。

最終驗證檢查驗證是否為當前設備創(chuàng)建了收據(jù)绽榛。 要執(zhí)行此操作,您需要設備標識符婿屹,這是一個字母數(shù)字字符串灭美,可為您的應用唯一標識設備。

將以下方法添加到Receipt

private func getDeviceIdentifier() -> Data {
  let device = UIDevice.current
  var uuid = device.identifierForVendor!.uuid
  let addr = withUnsafePointer(to: &uuid) { (p) -> UnsafeRawPointer in
    UnsafeRawPointer(p)
  }
  let data = Data(bytes: addr, count: 16)
  return data
}

此方法將設備標識符作為Data對象獲取昂利。

您使用hash函數(shù)驗證設備届腐。 哈希函數(shù)很容易在一個方向上計算,但很難逆轉(zhuǎn)蜂奸。 哈希通常用于允許確認值而無需存儲值本身梯捕。 例如,密碼通常存儲為hash值而不是實際密碼窝撵。 可以將多個值一起散列傀顾,如果最終結果相同,您可以確信原始值是相同的碌奉。

Receipt類的末尾添加以下方法:

private func computeHash() -> Data {
  let identifierData = getDeviceIdentifier()
  var ctx = SHA_CTX()
  SHA1_Init(&ctx)
  
  let identifierBytes: [UInt8] = .init(identifierData)
  SHA1_Update(&ctx, identifierBytes, identifierData.count)
  
  let opaqueBytes: [UInt8] = .init(opaqueData!)
  SHA1_Update(&ctx, opaqueBytes, opaqueData!.count)
  
  let bundleBytes: [UInt8] = .init(bundleIdData!)
  SHA1_Update(&ctx, bundleBytes, bundleIdData!.count)
  
  var hash: [UInt8] = .init(repeating: 0, count: 20)
  SHA1_Final(&hash, &ctx)
  return Data(bytes: hash, count: 20)
}

您計算SHA-1哈希以驗證設備短曾。 OpenSSL庫再次可以計算您需要的SHA-1哈希值寒砖。 您可以組合收據(jù)中的不透明值,收據(jù)中的包標識符和設備標識符嫉拐。 Apple在購買時了解這些值哩都,您的應用在驗證時就知道這些值。 通過計算哈希并檢查收據(jù)中的哈希婉徘,您驗證是否為當前設備創(chuàng)建了收據(jù)漠嵌。

將以下代碼添加到validateReceipt()的末尾:

// Check the GUID hash
let guidHash = computeHash()
guard hash == guidHash else {
  receiptStatus = .invalidHash
  return
}

此代碼將計算的哈希值與收據(jù)中的值進行比較。 如果它們不匹配盖呼,則收據(jù)可能是從其他設備復制的儒鹿,并且無效。

收據(jù)的最終檢查僅適用于允許批量購買計劃(Volume Purchase Program - VPP)購買的應用程序几晤。 這些購買包括收據(jù)中的到期日期约炎。 添加以下代碼以完成validateReceipt()

// Check the expiration attribute if it's present
let currentDate = Date()
if let expirationDate = expirationDate {
  if expirationDate < currentDate {
    receiptStatus = .invalidExpired
    return
  }
}

// All checks passed so validation is a success
receiptStatus = .validationSuccess

如果存在非nil的到期日期,那么您的應用應檢查到期日是否在當前日期之后蟹瘾。如果它在當前日期之前圾浅,則收據(jù)不再有效。如果不存在過期日期憾朴,則驗證不會失敗狸捕。

最后,在完成所有這些檢查而沒有任何失敗的情況下众雷,您可以將收據(jù)標記為有效灸拍。


Running the App

運行該應用程序。您必須在真實設備上運行此項目报腔。存儲相關代碼在模擬器中不起作用株搔。您還需要一個沙盒帳戶設置。在App Store購買的應用程序中纯蛾,將出現(xiàn)收據(jù)纤房。但是在從XCode進行測試時,您需要刷新才能獲得收據(jù)翻诉。教程應用程序已經(jīng)這樣做了炮姨。您需要登錄。然后碰煌,應用程序?qū)⑹褂帽窘坛讨械拇a驗證收據(jù)并顯示收據(jù)舒岸。

完成此操作后,添加應用內(nèi)購買芦圾。確保還使用產(chǎn)品標識符(product identifiers)更新ViewController.swift蛾派。使用Buy IAP按鈕和沙盒帳戶。您會看到table view列出了這些應用內(nèi)購買。還可以嘗試消費品購買洪乍,并記下刷新收據(jù)后它們是如何消失的眯杏。


Protecting Receipt Validation Code

攻擊者將努力繞過您的收據(jù)驗證碼(receipt validation code)。 使用此或任何其他收據(jù)驗證碼無需更改會產(chǎn)生風險壳澳。 如果攻擊者可以在一個使用此確切代碼的應用程序中繞過檢查岂贩,則攻擊者可以使用相同的代碼更輕松地為另一個應用程序重復此過程。 對于高價值或高盈利的應用程序巷波,您需要在保持相同工作的同時修改本教程的代碼萎津。

為了防止繞過驗證過程,您可以重復執(zhí)行驗證而不是一次抹镊。 避免顯式錯誤消息(例如“收據(jù)驗證失敗”)會使攻擊者的工作更加困難锉屈。 將失敗代碼放置在遠離驗證檢查的應用程序部分中也會使攻擊者的工作更加困難。

最后髓考,您需要平衡未經(jīng)授權訪問您的應用程序的風險與額外的時間和復雜性部念,代碼的額外混淆會增加您的開發(fā)過程弃酌。

Apple的Receipt Validation Programming Guide提供了有關收據(jù)的最佳文檔氨菇,以及關于 Preventing Unauthorized Purchases with Receipts的WWDC 2014會議。 兩者都討論了本教程中未涉及的服務器驗證方法妓湘。 來自WWDC 2016的會議查蓉, Using Store Kit for In-App Purchases with Swift 3,還討論了與訂閱特別相關的收據(jù)榜贴。

后記

本篇主要講述了收據(jù)驗證豌研,感興趣的給個贊或者關注~~~

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市唬党,隨后出現(xiàn)的幾起案子鹃共,更是在濱河造成了極大的恐慌,老刑警劉巖驶拱,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霜浴,死亡現(xiàn)場離奇詭異,居然都是意外死亡蓝纲,警方通過查閱死者的電腦和手機阴孟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來税迷,“玉大人永丝,你說我怎么就攤上這事〖” “怎么了慕嚷?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我喝检,道長砂心,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任蛇耀,我火速辦了婚禮辩诞,結果婚禮上,老公的妹妹穿的比我還像新娘纺涤。我一直安慰自己译暂,他們只是感情好,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布撩炊。 她就那樣靜靜地躺著外永,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拧咳。 梳的紋絲不亂的頭發(fā)上伯顶,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音骆膝,去河邊找鬼祭衩。 笑死,一個胖子當著我的面吹牛阅签,可吹牛的內(nèi)容都是我干的掐暮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼政钟,長吁一口氣:“原來是場噩夢啊……” “哼路克!你這毒婦竟也來了?” 一聲冷哼從身側響起养交,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤精算,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后碎连,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灰羽,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年破花,在試婚紗的時候發(fā)現(xiàn)自己被綠了谦趣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡座每,死狀恐怖前鹅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情峭梳,我是刑警寧澤舰绘,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布蹂喻,位于F島的核電站,受9級特大地震影響捂寿,放射性物質(zhì)發(fā)生泄漏口四。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一秦陋、第九天 我趴在偏房一處隱蔽的房頂上張望蔓彩。 院中可真熱鬧,春花似錦驳概、人聲如沸赤嚼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽更卒。三九已至,卻和暖如春稚照,著一層夾襖步出監(jiān)牢的瞬間蹂空,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工果录, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留上枕,地道東北人。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓雕憔,卻偏偏與公主長得像姿骏,于是被迫代替她去往敵國和親糖声。 傳聞我的和親對象是個殘疾皇子斤彼,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容