tikhop / TPInAppReceipt

Reading and Validating In App Purchase Receipt Locally.
MIT License
635 stars 95 forks source link

Will this update from Apple affect this library? #108

Closed ripperhe closed 1 year ago

ripperhe commented 1 year ago

https://developer.apple.com/news/?id=smofnyhj

image
phi161 commented 1 year ago

I'm also curious about this, it will be nice to learn if/how TPInAppReceipt is affected.

Looking at the source code, one candidate is verifySignature() which checks if the signature is valid, as well as the chain of trust (mentioned in Apple's docs).

tikhop commented 1 year ago

@ripperhe @phi161 Based on research I did, TPInAppReceipt is not affected.

Looking into checkSignatureValidity function we can see that we take an algorithm from the receipt itself:

var umErrorCF: Unmanaged<CFError>? = nil
guard let alg = receipt.digestAlgorithm,
       SecKeyVerifySignature(iTunesPublicKeySec, alg, payloadRawData as CFData, signature as CFData, &umErrorCF) else {
    let error = umErrorCF?.takeRetainedValue() as Error? as NSError?
    print("error is \(String(describing: error))")

    throw IARError.validationFailed(reason: .signatureValidation(.invalidSignature))
}

Nevertheless, I'm planing to come back to the issue on June 20 to double check that everything works well.

Thank you!

yusuftor commented 1 year ago

Hi @tikhop, this does fail (I checked) but I'm not 100% sure why. verifyHash() is part of the validation process for isValid in this library and uses the computedHash which is SHA1 only. Apple don't mention that part of the verification process changing but it fails for me as it doesn't match the receiptHash. I tested it by purchasing a product using a StoreKit configuration file on an iOS 17 device (it's the 20th June where I am)

tikhop commented 1 year ago

@yusuftor thanks for checking. I was actually curious about this part of validation since it's the only part that explicitly using SHA1, but, as you found as well, there is nothing should be changed based on Apple docs.

If you have a chance, could you please share your receipt?

tikhop commented 1 year ago

Hey @yusuftor. I confirm that the hash validation step fails, while the other steps are working well. I have inspected both: the decoded receipt and the raw one and everything looks good.

tikhop commented 1 year ago

Hey @ripperhe, @phi161, @yusuftor. I've tried different apps and devices, but the hash verification step continues to fail. I suspect this might be an issue on Apple's end, so I ended up submitting a Technical Support Incident request to Apple.

I'll update this thread as soon as I hear back from the Apple team.

Thank you!

yusuftor commented 1 year ago

Thanks so much for submitting this to Apple and keeping us updated @tikhop! Hoping they resolve this soon.

phi161 commented 1 year ago

In my case, things seem to be working fine, I'm starting to wonder if I'm doing something wrong here! Running this code, does not throw any error:

do {
    let receipt = try InAppReceipt() // Returns local receipt
    try receipt.validate()
    print("valid")
} catch {
    print(error)
}

Why does verification fail for you?

tikhop commented 1 year ago

@phi161 I do the same what you do, so there is nothing wrong in your code, and it fails when we try to verify the hash of the receipt. If it works for you, then you either still have an old version of the receipt on your device or it's magically got fixed by Apple.

Please, before validating try to refresh your receipt first :

InAppReceipt.refresh { (error) in
  if let err = error {
    print(err)
  } else {
    initializeReceipt()
  }
}
tikhop commented 1 year ago

Just to note:

Here is the commit when the change was implemented in order to support SHA256 version of certificates

tikhop commented 1 year ago

@phi161 I have tried to perform my tests on another device with a different Apple ID, but using the same app and it works well whereas my original device still fails. 🤷‍♂

phi161 commented 1 year ago

Hi @tikhop, thank you for taking the time to look into this! Can I help you somehow, to better understand what's going on? Would it help if we ran the same project for example (just changing the app id every time?)

yusuftor commented 1 year ago

Any update on this from Apple @tikhop?

tikhop commented 1 year ago

Hi @yusuftor, not yet - they only asked to provide additional information.

tikhop commented 1 year ago

@phi161 Sorry, somehow I missed your comment. I don't think it will help to solve the issue since the problem is definitely on apple side.

yusuftor commented 1 year ago

Okay thanks for the update. Providing them with a basic sample project with this framework that shows the issue would be useful, if you haven't already done that

tikhop commented 1 year ago

@yusuftor yup, I provided everything they need to reproduce the issue: receipt, device uid, the project I use to verify the library and the library itself.

yusuftor commented 1 year ago

Update, I think I solved this. It's not an Apple issue, it's the way that the guid() method works. I tried a different receipt validation framework with my own receipt and it worked. Currently for iOS this framework is doing this:

var uuidBytes = UIDevice.current.identifierForVendor?.uuid
return Data(bytes: &uuidBytes, count: MemoryLayout.size(ofValue: uuidBytes))

This is somehow not the right way to do it. The correct way to do it is this:

if let identifierForVendor = UIDevice.current.identifierForVendor {
  var rawUUID = identifierForVendor.uuid
  let count = MemoryLayout.size(ofValue: rawUUID)
  let data = withUnsafePointer(to: &rawUUID) {
    Data(bytes: $0, count: count)
  }
  return data
}

I'm not 100% sure about how other platforms should be changed in regards to this but this is what I've got:

private func guid() -> Data {
#if os(watchOS)
  if let identifierForVendor = WKInterfaceDevice.current().identifierForVendor {
    var rawUUID = identifierForVendor.uuid
    let count = MemoryLayout.size(ofValue: rawUUID)
    let data = withUnsafePointer(to: &rawUUID) {
      Data(bytes: $0, count: count)
    }
    return data
  }
  return Data()
#elseif !targetEnvironment(macCatalyst) && (os(iOS) || os(tvOS))
  if let identifierForVendor = UIDevice.current.identifierForVendor {
    var rawUUID = identifierForVendor.uuid
    let count = MemoryLayout.size(ofValue: rawUUID)
    let data = withUnsafePointer(to: &rawUUID) {
      Data(bytes: $0, count: count)
    }
    return data
  }
  return Data()
#elseif targetEnvironment(macCatalyst) || os(macOS)
  if let guid = getMacAddress() {
    return guid
  } else {
    assertionFailure("Failed to retrieve guid")
  }
  return Data()
#endif
}
tikhop commented 1 year ago

@yusuftor Awesome. Thanks for narrowing it down. It works for me now. The core issue in the example you have provided is that MemoryLayout.size(ofValue: uuidBytes) returns 17 instead of 16, since uuidBytes has the following type: Optional<(UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)>. We can either construct the data as you shown in your example or just unwrap identifierForVendorfirst and then use the same method we currently have:

var uuidBytes = UIDevice.current.identifierForVendor!.uuid // Or if let uuidBytes = UIDevice.current.identifierForVendor?.uuid
return Data(bytes: &uuidBytes, count: MemoryLayout.size(ofValue: uuidBytes))

I will apply the patch shortly. Thank you!

yusuftor commented 1 year ago

I see, thanks for explaining. I think it’s best not to use force unwrapping wherever possible just incase it causes crashes in production.

tikhop commented 1 year ago

@yusuftor I agree, but here we should expect identifierForVendor and if we don't have it than something really strange is going on.

tikhop commented 1 year ago

@yusuftor Unfortunately, hash verification still doesn't work. The code you have provided doesn't match the current implementation where I unwrap the uuid and have the correct Data. I still think that the issues is on Apple side, since it has been working for years and still work for other devices. Apple, on the other hand, says that it works as expected.

yusuftor commented 1 year ago

It works for me, what changed between you saying it worked and then saying it didn’t work?

tikhop commented 1 year ago

@yusuftor

Your code:

var uuidBytes = UIDevice.current.identifierForVendor?.uuid
return Data(bytes: &uuidBytes, count: MemoryLayout.size(ofValue: uuidBytes))

The code from the library:

var uuidBytes = UIDevice.current.identifierForVendor!.uuid
return Data(bytes: &uuidBytes, count: MemoryLayout.size(ofValue: uuidBytes))

These two snippets works differently and the library uses the second snippet which yields the same result as:

if let identifierForVendor = UIDevice.current.identifierForVendor {
  var rawUUID = identifierForVendor.uuid
  let count = MemoryLayout.size(ofValue: rawUUID)
  let data = withUnsafePointer(to: &rawUUID) {
    Data(bytes: $0, count: count)
  }
  return data
}

In other words, the solution you have proposed does not solve the issue because it produce exactly the same Data object as the current implementation.

The reason I have changed my mind is due to that misleading snippet you have provided. I thought you copied it from the library.

yusuftor commented 1 year ago

Ah it seems I was using a version of this framework which I modified a while back to remove the force unwraps and in doing so broke the validation. I assumed that was part of this framework's original code. So the original way actually works for me. I'm not sure why it doesn't for you. Can you upload your sample project here that demonstrates it not working? Want to see if it stops working for me too.

tikhop commented 1 year ago

@yusuftor, @phi161. I apologize for confusion, it was my mistake. Everything is actually working as expected. As I mentioned earlier, validation functions properly on one device but not on another. Upon further investigation, I discovered that I have a different sandbox account from the main one I used to download the app from TestFlight. Switching from the sandbox account to my regular account resolved the issue, and now all validation step work flawlessly.

Thanks again.