SSKeychain: Accounts not stored in iCloud? - ios

I'm using sskeychain (https://github.com/soffes/sskeychain) to store my accounts and passwords in the IOS keychain. I assume, that if I store an account, it should be available on my other device. But it doesn't appear there.
I read my accounts with this code:
NSArray *arr=[SSKeychain accountsForService:#"Login"];
for (NSString *s in arr) {
NSLog(#"Account: %#",s);
}
and get this (only shown one entry, the others are similar):
Account: {
acct = "friXXXXXter#XXXX.com";
agrp = "3B4384Z34A.de.gondomir.LocalButler";
cdat = "2014-05-09 22:55:08 +0000";
mdat = "2014-05-09 22:55:08 +0000";
pdmn = ak;
svce = Login;
sync = 0;
tomb = 0;
}
But this doesn't appear on the other device. Both devices have IOS 7.1.1.
I store the password with this line:
[SSKeychain setPassword:self.passwortField.text forService:#"Login" account:self.userField.text];
I have switched on keychain sharing in Xcode and have a keychain group "de.gondomir.LocalButler" listed there.
Am I missing something? Must the service name something special?
Thanks!

In case this is still relevant for you I managed to find the solution. (works for >=iOS7)
Don't use the static methods of SSKeychain to write your credentials. Instead use SSKeychainQuery and set the synchronizationMode to SSKeychainQuerySynchronizationModeYes like this
NSError *error;
[SSKeychain setAccessibilityType:self.keychainAccessibilityType];
SSKeychainQuery *query = [[SSKeychainQuery alloc] init];
query.service = service;
query.account = account;
query.password = password;
query.synchronizationMode = SSKeychainQuerySynchronizationModeYes;
[query save:&error];
if (error) {
NSLog(#"Error writing credentials %#", [error description]);
}
The static convenience methods on SSKeychain use the default synchronization mode SSKeychainQuerySynchronizationModeAny causing credentials not to be synchronized with the iCloud keychain.
Additionally, make sure your devices have Keychain via iCloud enabled (Settings>iCloud>Keychain). You might also want to enable Keychain Sharing in your targets Capabilities.

After I have a new project I tried the answer of MarkHim, it works.
I used swift now, so here is my working code:
let account = defaults.objectForKey("Sync_toPhoneNumber") as? String
SSKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlock)
var error:NSError?
let lookupQuery = SSKeychainQuery()
lookupQuery.synchronizationMode = .Yes
lookupQuery.service = "DasDing"
lookupQuery.account = account
let password = SSKeychain.passwordForService("DasDing", account: account, error: &error)
if error == nil {
commandKey = password!
} else {
print("Error für \(account): \(error!.localizedDescription)")
commandKey = ""
}
// query all accounts for later use
let allQuery = SSKeychainQuery()
allQuery.service = "DasDing"
do {
let dict = try allQuery.fetchAll()
print("Accounts:")
for acc in dict {
print(acc["acct"]!)
}
} catch let error as NSError {
print("keine Accounts")
print("Error: \(error.localizedDescription)")
}
That's for reading, for writing you must delete the account first (if you want to change the password):
let account = defaults.objectForKey("Sync_toPhoneNumber") as? String
SSKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlock)
SSKeychain.deletePasswordForService("DasDing", account: account)
let newQuery = SSKeychainQuery()
newQuery.service = "DasDing"
newQuery.account = account
newQuery.password = str?.uppercaseString
newQuery.synchronizationMode = .Yes
try! newQuery.save()

Related

How do I configure identityData of NEVPNProtocolIKEv2 with String certificate?

I using NetworkExtension framework to creating an application, it connect to VPN server via NEVPNProtocolIKEv2.
After research, I found an tutorial about working with NetworkExtension framework, and I try to follow it.
(http://ramezanpour.net/post/2014/08/03/configure-and-manage-vpn-connections-programmatically-in-ios-8/)
But, I stuck when I configure identityData of this protocol.
Here is m code:
self.vpnManager.loadFromPreferencesWithCompletionHandler { [unowned self] (error) in
if error != nil {
printError("\(error?.errorDescription)")
return
}
let p = NEVPNProtocolIKEv2()
p.username = server.userName
p.serverAddress = server.serverUrl
// Get password persistent reference from keychain
self.createKeychainValue(server.password, forIdentifier: KeychainId_Password)
p.passwordReference = self.searchKeychainCopyMatching(KeychainId_Password)
p.authenticationMethod = NEVPNIKEAuthenticationMethod.None
self.createKeychainValue(kVPNsecret, forIdentifier: KeychainId_PSK)
p.sharedSecretReference = self.searchKeychainCopyMatching(KeychainId_PSK)
// certificate
p.identityData = ??????
p.useExtendedAuthentication = true
p.disconnectOnSleep = false
self.vpnManager.`protocol` = p
self.vpnManager.localizedDescription = server.serverName
self.vpnManager.saveToPreferencesWithCompletionHandler({ [unowned self] (error) in
if error != nil {
printError("Save config failed " + error!.localizedDescription)
}
})
}
In tutorial, p.identityData is NSData, that was loading from a P12 file.
But I have only a string that call: server.certificate
This server.certificate has a value like this
"-----BEGIN CERTIFICATE-----\nMIIEdDCCA1ygAwIBAgIBADANBgkqhki......1iEtCZg7SAlsBiaxpJzpZm5C6OifUCkUfZNdPQ==\n-----END CERTIFICATE-----\n"
This is a very very long string, that call x509Certificate... or something like that, I do not remember exactly.
I found an library support write an String to file p12, It is "openssl".
But demo code is Objective-C. I keep trying port this code to Swift, but it is so hard.
(democode: iOS: How to create PKCS12 (P12) keystore from private key and x509certificate in application programmatically?)
Finally, I have only a String certificate, and I want to configure p.identityData for my application.
How I do it?

IOS9 Contacts framework fails to update linked contact

In AddressBook on device I have a record linked with Facebook contact record.
I fetch it into CNContact with CNContactFetchRequest with:
contactFetchRequest.mutableObjects = true
contactFetchRequest.unifyResults = false
After getting, I modify it, then I trying to update it with:
let store = CNContactStore()
let saveRequest = CNSaveRequest()
if contact != nil {
mutableContact = contact!.mutableCopy() as! CNMutableContact
saveRequest.updateContact( mutableContact )
} else {
mutableContact = CNMutableContact()
saveRequest.addContact( mutableContact, toContainerWithIdentifier:nil )
}
// Modify mutableContact
mutableContact.jobTitle = "Worker";
do {
// Will fails with error
try store.executeSaveRequest(saveRequest)
} catch let error as NSError {
BCRLog(error)
self.isFailed = true
} catch {
self.isFailed = true
}
On execute executeSaveRequest, I caught an error:
NSError with domain:CNErrorDomain, code:500 (witch is
CNErrorCodePolicyViolation), _userInfo: {"NSUnderlyingError" :
{"ABAddressBookErrorDomain" - code 0}} witch is
kABOperationNotPermittedByStoreError
The question: Is it possible to modify linked contact (not unified), and if it is, what i do wrong?
If I modifying not linked contact - all OK!
I have this error when the Contacts app is configured to store contacts in an Exchange account. When I choose an iCloud account as a default it immediately saves a contact well. I can check what is set on your device in Settings -> Contacts -> Default Account

How to get Wifi SSID in iOS9 after CaptiveNetwork is deprecated and calls for Wifi name are already blocked

Until today I used the CaptiveNetwork Interface to display the name of the currently connected Wifi.
The iOS 9 Prerelease reference already stated, that the CaptiveNetwork methods are depracted now, but they still worked at the beginning.
With the newest version Apple seems to have blocked this calls already (maybe due to privacy concerns?).
Is there any other way to get the name of the current Wifi?
This is how I obtained the SSID until today, but you only get nil now:
#import <SystemConfiguration/CaptiveNetwork.h>
NSString *wifiName = nil;
NSArray *interFaceNames = (__bridge_transfer id)CNCopySupportedInterfaces();
for (NSString *name in interFaceNames) {
NSDictionary *info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)name);
if (info[#"SSID"]) {
wifiName = info[#"SSID"];
}
}
Register your app as Hotspot helper.
#import <NetworkExtension/NetworkExtension.h>
NSArray * networkInterfaces = [NEHotspotHelper supportedNetworkInterfaces];
NSLog(#"Networks %#",networkInterfaces);
UPDATE (Sept. 11th, 2015)
The following Captive Network APIs have been re-enabled in the latest version of iOS 9 instead.
CNCopySupportedInterfaces
CNCopyCurrentNetworkInfo
UPDATE (Sept. 16th, 2015)
If you still prefer to use NetworkExtension and Apple gave you permission to add the entitlements, then you can do this to get the wifi information:
for(NEHotspotNetwork *hotspotNetwork in [NEHotspotHelper supportedNetworkInterfaces]) {
NSString *ssid = hotspotNetwork.SSID;
NSString *bssid = hotspotNetwork.BSSID;
BOOL secure = hotspotNetwork.secure;
BOOL autoJoined = hotspotNetwork.autoJoined;
double signalStrength = hotspotNetwork.signalStrength;
}
NetworkExtension provides you some extra information as secure, auto joined or the signal strength. And it also allows you to set credential to wifis on background mode, when the user scans wifis around.
In the GM for iOS 9, it seems like this is enabled again. In fact, it's not even listed as deprecated in the online documentation, however the CaptiveNetwork header file does have the following:
CNCopySupportedInterfaces (void) __OSX_AVAILABLE_BUT_DEPRECATED_MSG(__MAC_10_8, __MAC_NA, __IPHONE_4_1, __IPHONE_9_0, CN_DEPRECATION_NOTICE);
So, it is working in the iOS 9 GM, but not sure for how long :)
The answer by abdullahselek is still correct even for Swift 4.1 and 4.2.
A small caveat is that now in iOS 12, you must go to the capabilities section of your app project and enable the Access WiFi Information feature. It will add an entitlement entry to your project and allow the function call CNCopyCurrentNetworkInfo to work properly.
If you do not do this, that function simply returns nil. No errors or warnings at runtime about the missing entitlement will be displayed.
For more info, see the link below to Apple's documentation.
https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo
Confirm on 2017-April-27, Captive Network is still working for Swift 3.1, XCode 8.3
For Swift > 3.0
func printCurrentWifiInfo() {
if let interface = CNCopySupportedInterfaces() {
for i in 0..<CFArrayGetCount(interface) {
let interfaceName: UnsafeRawPointer = CFArrayGetValueAtIndex(interface, i)
let rec = unsafeBitCast(interfaceName, to: AnyObject.self)
if let unsafeInterfaceData = CNCopyCurrentNetworkInfo("\(rec)" as CFString), let interfaceData = unsafeInterfaceData as? [String : AnyObject] {
// connected wifi
print("BSSID: \(interfaceData["BSSID"]), SSID: \(interfaceData["SSID"]), SSIDDATA: \(interfaceData["SSIDDATA"])")
} else {
// not connected wifi
}
}
}
}
For Objective-C
NSArray *interFaceNames = (__bridge_transfer id)CNCopySupportedInterfaces();
for (NSString *name in interFaceNames) {
NSDictionary *info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)name);
NSLog(#"wifi info: bssid: %#, ssid:%#, ssidData: %#", info[#"BSSID"], info[#"SSID"], info[#"SSIDDATA"]);
}
As mentioned before CaptiveNetwork works well with Xcode 8.3 and upper. Here are code snippets for both Swift 3, Swift 4 and Objective-C.
Swift 3 & 4
import SystemConfiguration.CaptiveNetwork
internal class SSID {
class func fetchSSIDInfo() -> [String: Any] {
var interface = [String: Any]()
if let interfaces = CNCopySupportedInterfaces() {
for i in 0..<CFArrayGetCount(interfaces){
let interfaceName = CFArrayGetValueAtIndex(interfaces, i)
let rec = unsafeBitCast(interfaceName, to: AnyObject.self)
guard let unsafeInterfaceData = CNCopyCurrentNetworkInfo("\(rec)" as CFString) else {
return interface
}
guard let interfaceData = unsafeInterfaceData as? [String: Any] else {
return interface
}
interface = interfaceData
}
}
return interface
}
}
Objective-C
#import <SystemConfiguration/CaptiveNetwork.h>
+ (NSDictionary *)fetchSSIDInfo
{
NSArray *interFaceNames = (__bridge_transfer id)CNCopySupportedInterfaces();
for (NSString *name in interFaceNames)
{
NSDictionary *info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)name);
return info;
}
return nil;
}
This should be working now with iOS 13.3. I'm using a related Pod library that uses the exact function in Objc and with a Swift wrapper.
https://github.com/Feghal/FGRoute
CaptiveNetwork is still working. Due to many requests Apple may have reinstated the API's.
Using CaptiveNetwork we can get the SSID of the WiFi network.
It even works in iOS 10.
#import <SystemConfiguration/CaptiveNetwork.h>
NSDictionary *info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)name);
Here is the output:
Printing description of info:
{
BSSID = "5*:**:**:**:**:**";
SSID = Cisco12814;
SSIDDATA = <43697363 6f313238 3134>;
}
CaptiveNetwork is still working. But you will need to add this:
com.apple.developer.networking.wifi-info = true inside your Entitlements.plist.
Plus, you need to be Enable the Access WiFi Information in the App ID
part in your developer.apple.com portal.
Be sure, to clean your environment, generate new provisioning profile after enabling option "Access WiFi Information" in the App ID.

iOS Upload file to Google Cloud Storage - getting 401: 'Login Required'

I am having trouble figuring out how to upload a file to a Public Access bucket to Cloud Storage.
I have set up a bucket and set it's ACLs to READ and WRITE for all users.
I have enabled the Cloud Storage JSON API and the Cloud Storage API.
I have created an API key for browser applications that allows any referrer.
Here is my code in Swift:
private lazy var googleServiceStorage:GTLServiceStorage = {
var storage = GTLServiceStorage()
storage.APIKey = "AIzaSy**********m8TPCM"
storage.additionalHTTPHeaders = ["x-goog-project-id" : "159*******7"]
return storage
}()
}
public func uploadAssetToGoogle(resourcePath: String?) {
if let _resourcePath = resourcePath {
let fileHandle = NSFileHandle(forReadingAtPath: _resourcePath)
let uploadParams = GTLUploadParameters(fileHandle: fileHandle, MIMEType: "video/mov")
var storageObject = GTLStorageObject.object() as GTLStorageObject
storageObject.name = "12345678"
let query = GTLQueryStorage.queryForObjectsInsertWithObject(storageObject, bucket: "my-bucket", uploadParameters: uploadParams) as GTLQuery
var ticket = googleServiceStorage.executeQuery(query) { ticket, object, error in
if let _error = error {
println("Error upload file: \(error.localizedDescription) : \(error.localizedFailureReason)")
return
}
println("Upload succeeded")
}
ticket.uploadProgressBlock = {ticket, numberOfBytesRead, dataLength in
println("Ticket: \(ticket)")
NSLog("read %llu from %llu bytes", numberOfBytesRead, dataLength)
}
}
When I call this code I get the following output in the console:
mediaURL type: (Metatype) - file:///Users/Michael/Library/Developer/CoreSimulator/Devices/5895B7FA-41E7-4958-84FD-2C1043CA7CD7/data/Containers/Data/Application/2DD62539-E364-4BE0-A89C-E0DD2827D74B/tmp/trim.FCE68DAD-0FC6-4E2D-8C92-37055A02DD12.MOV
Ticket: GTLServiceTicket 0x7feed4860660: {service:<GTLServiceStorage: 0x7feed2cf9d90> devKey:AIzaSyBClcLHWtXzlBHb2VbATA1xIlUO0m8TPCM fetcher:GTMHTTPUploadFetcher 0x7feed4890fb0 (https://www.googleapis.com/upload/rpc?uploadType=resumable&prettyPrint=false) }
2014-09-25 18:25:23.515 Beta[14917:1971326] read 202 from 29512 bytes
Error upload file: The operation couldn’t be completed. (Login Required) : Optional("(Login Required)")
There is something I am missing, but I can't figure it out. If anyone can help me, it will save me a lot more hair.
For Login Required error message try with adding new bucket permission item
ENTITY = User, NAME = allUsers, ACCESS = Writer
Please note: You have to use Server API key not iOS API key
For object upload, you can add GTLStorageObjectAccessControl
GTLStorageObjectAccessControl *objAccessControl = [GTLStorageObjectAccessControl new];
objAccessControl.entity = #"allUsers";
objAccessControl.email = #"xxxxxx-xxxxx#developer.gserviceaccount.com";
objAccessControl.role = #"OWNER";
GTLStorageObject *newObject = [GTLStorageObject object];
newObject.name = #"image.png";
newObject.acl = #[objAccessControl];
Than,
GTLQueryStorage *query = [GTLQueryStorage queryForObjectsInsertWithObject:newObject bucket:kBucketName uploadParameters:uploadParameters];

A complete solution to LOCALLY validate an in-app receipts and bundle receipts on iOS 7

I have read a lot of docs and code that in theory will validate an in-app and/or bundle receipt.
Given that my knowledge of SSL, certificates, encryption, etc., is nearly zero, all of the explanations I have read, like this promising one, I have found difficult to understand.
They say the explanations are incomplete because every person has to figure out how to do it, or the hackers will have an easy job creating a cracker app that can recognize and identify patterns and patch the application. OK, I agree with this up to a certain point. I think they could explain completely how to do it and put a warning saying "modify this method", "modify this other method", "obfuscate this variable", "change the name of this and that", etc.
Can some good soul out there be kind enough to explain how to LOCALLY validate, bundle receipts and in-app purchase receipts on iOS 7 as I am five years old (ok, make it 3), from top to bottom, clearly?
Thanks!!!
If you have a version working on your apps and your concerns are that hackers will see how you did it, simply change your sensitive methods before publishing here. Obfuscate strings, change the order of lines, change the way you do loops (from using for to block enumeration and vice-versa) and things like that. Obviously, every person that uses the code that may be posted here, has to do the same thing, not to risk being easily hacked.
Here's a walkthrough of how I solved this in my in-app purchase library RMStore. I will explain how to verify a transaction, which includes verifying the whole receipt.
At a glance
Get the receipt and verify the transaction. If it fails, refresh the receipt and try again. This makes the verification process asynchronous as refreshing the receipt is asynchronous.
From RMStoreAppReceiptVerifier:
RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
const BOOL verified = [self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:nil]; // failureBlock is nil intentionally. See below.
if (verified) return;
// Apple recommends to refresh the receipt if validation fails on iOS
[[RMStore defaultStore] refreshReceiptOnSuccess:^{
RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
[self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:failureBlock];
} failure:^(NSError *error) {
[self failWithBlock:failureBlock error:error];
}];
Getting the receipt data
The receipt is in [[NSBundle mainBundle] appStoreReceiptURL] and is actually a PCKS7 container. I suck at cryptography so I used OpenSSL to open this container. Others apparently have done it purely with system frameworks.
Adding OpenSSL to your project is not trivial. The RMStore wiki should help.
If you opt to use OpenSSL to open the PKCS7 container, your code could look like this. From RMAppReceipt:
+ (NSData*)dataFromPKCS7Path:(NSString*)path
{
const char *cpath = [[path stringByStandardizingPath] fileSystemRepresentation];
FILE *fp = fopen(cpath, "rb");
if (!fp) return nil;
PKCS7 *p7 = d2i_PKCS7_fp(fp, NULL);
fclose(fp);
if (!p7) return nil;
NSData *data;
NSURL *certificateURL = [[NSBundle mainBundle] URLForResource:#"AppleIncRootCertificate" withExtension:#"cer"];
NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL];
if ([self verifyPKCS7:p7 withCertificateData:certificateData])
{
struct pkcs7_st *contents = p7->d.sign->contents;
if (PKCS7_type_is_data(contents))
{
ASN1_OCTET_STRING *octets = contents->d.data;
data = [NSData dataWithBytes:octets->data length:octets->length];
}
}
PKCS7_free(p7);
return data;
}
We'll get into the details of the verification later.
Getting the receipt fields
The receipt is expressed in ASN1 format. It contains general information, some fields for verification purposes (we'll come to that later) and specific information of each applicable in-app purchase.
Again, OpenSSL comes to the rescue when it comes to reading ASN1. From RMAppReceipt, using a few helper methods:
NSMutableArray *purchases = [NSMutableArray array];
[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) {
const uint8_t *s = data.bytes;
const NSUInteger length = data.length;
switch (type)
{
case RMAppReceiptASN1TypeBundleIdentifier:
_bundleIdentifierData = data;
_bundleIdentifier = RMASN1ReadUTF8String(&s, length);
break;
case RMAppReceiptASN1TypeAppVersion:
_appVersion = RMASN1ReadUTF8String(&s, length);
break;
case RMAppReceiptASN1TypeOpaqueValue:
_opaqueValue = data;
break;
case RMAppReceiptASN1TypeHash:
_hash = data;
break;
case RMAppReceiptASN1TypeInAppPurchaseReceipt:
{
RMAppReceiptIAP *purchase = [[RMAppReceiptIAP alloc] initWithASN1Data:data];
[purchases addObject:purchase];
break;
}
case RMAppReceiptASN1TypeOriginalAppVersion:
_originalAppVersion = RMASN1ReadUTF8String(&s, length);
break;
case RMAppReceiptASN1TypeExpirationDate:
{
NSString *string = RMASN1ReadIA5SString(&s, length);
_expirationDate = [RMAppReceipt formatRFC3339String:string];
break;
}
}
}];
_inAppPurchases = purchases;
Getting the in-app purchases
Each in-app purchase is also in ASN1. Parsing it is very similar than parsing the general receipt information.
From RMAppReceipt, using the same helper methods:
[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) {
const uint8_t *p = data.bytes;
const NSUInteger length = data.length;
switch (type)
{
case RMAppReceiptASN1TypeQuantity:
_quantity = RMASN1ReadInteger(&p, length);
break;
case RMAppReceiptASN1TypeProductIdentifier:
_productIdentifier = RMASN1ReadUTF8String(&p, length);
break;
case RMAppReceiptASN1TypeTransactionIdentifier:
_transactionIdentifier = RMASN1ReadUTF8String(&p, length);
break;
case RMAppReceiptASN1TypePurchaseDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_purchaseDate = [RMAppReceipt formatRFC3339String:string];
break;
}
case RMAppReceiptASN1TypeOriginalTransactionIdentifier:
_originalTransactionIdentifier = RMASN1ReadUTF8String(&p, length);
break;
case RMAppReceiptASN1TypeOriginalPurchaseDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_originalPurchaseDate = [RMAppReceipt formatRFC3339String:string];
break;
}
case RMAppReceiptASN1TypeSubscriptionExpirationDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_subscriptionExpirationDate = [RMAppReceipt formatRFC3339String:string];
break;
}
case RMAppReceiptASN1TypeWebOrderLineItemID:
_webOrderLineItemID = RMASN1ReadInteger(&p, length);
break;
case RMAppReceiptASN1TypeCancellationDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_cancellationDate = [RMAppReceipt formatRFC3339String:string];
break;
}
}
}];
It should be noted that certain in-app purchases, such as consumables and non-renewable subscriptions, will appear only once in the receipt. You should verify these right after the purchase (again, RMStore helps you with this).
Verification at a glance
Now we got all the fields from the receipt and all its in-app purchases. First we verify the receipt itself, and then we simply check if the receipt contains the product of the transaction.
Below is the method that we called back at the beginning. From RMStoreAppReceiptVerificator:
- (BOOL)verifyTransaction:(SKPaymentTransaction*)transaction
inReceipt:(RMAppReceipt*)receipt
success:(void (^)())successBlock
failure:(void (^)(NSError *error))failureBlock
{
const BOOL receiptVerified = [self verifyAppReceipt:receipt];
if (!receiptVerified)
{
[self failWithBlock:failureBlock message:NSLocalizedString(#"The app receipt failed verification", #"")];
return NO;
}
SKPayment *payment = transaction.payment;
const BOOL transactionVerified = [receipt containsInAppPurchaseOfProductIdentifier:payment.productIdentifier];
if (!transactionVerified)
{
[self failWithBlock:failureBlock message:NSLocalizedString(#"The app receipt doest not contain the given product", #"")];
return NO;
}
if (successBlock)
{
successBlock();
}
return YES;
}
Verifying the receipt
Verifying the receipt itself boils down to:
Checking that the receipt is valid PKCS7 and ASN1. We have done this implicitly already.
Verifying that the receipt is signed by Apple. This was done before parsing the receipt and will be detailed below.
Checking that the bundle identifier included in the receipt corresponds to your bundle identifier. You should hardcode your bundle identifier, as it doesn't seem to be very difficult to modify your app bundle and use some other receipt.
Checking that the app version included in the receipt corresponds to your app version identifier. You should hardcode the app version, for the same reasons indicated above.
Check the receipt hash to make sure the receipt correspond to the current device.
The 5 steps in code at a high-level, from RMStoreAppReceiptVerificator:
- (BOOL)verifyAppReceipt:(RMAppReceipt*)receipt
{
// Steps 1 & 2 were done while parsing the receipt
if (!receipt) return NO;
// Step 3
if (![receipt.bundleIdentifier isEqualToString:self.bundleIdentifier]) return NO;
// Step 4
if (![receipt.appVersion isEqualToString:self.bundleVersion]) return NO;
// Step 5
if (![receipt verifyReceiptHash]) return NO;
return YES;
}
Let's drill-down into steps 2 and 5.
Verifying the receipt signature
Back when we extracted the data we glanced over the receipt signature verification. The receipt is signed with the Apple Inc. Root Certificate, which can be downloaded from Apple Root Certificate Authority. The following code takes the PKCS7 container and the root certificate as data and checks if they match:
+ (BOOL)verifyPKCS7:(PKCS7*)container withCertificateData:(NSData*)certificateData
{ // Based on: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW17
static int verified = 1;
int result = 0;
OpenSSL_add_all_digests(); // Required for PKCS7_verify to work
X509_STORE *store = X509_STORE_new();
if (store)
{
const uint8_t *certificateBytes = (uint8_t *)(certificateData.bytes);
X509 *certificate = d2i_X509(NULL, &certificateBytes, (long)certificateData.length);
if (certificate)
{
X509_STORE_add_cert(store, certificate);
BIO *payload = BIO_new(BIO_s_mem());
result = PKCS7_verify(container, NULL, store, NULL, payload, 0);
BIO_free(payload);
X509_free(certificate);
}
}
X509_STORE_free(store);
EVP_cleanup(); // Balances OpenSSL_add_all_digests (), per http://www.openssl.org/docs/crypto/OpenSSL_add_all_algorithms.html
return result == verified;
}
This was done back at the beginning, before the receipt was parsed.
Verifying the receipt hash
The hash included in the receipt is a SHA1 of the device id, some opaque value included in the receipt and the bundle id.
This is how you would verify the receipt hash on iOS. From RMAppReceipt:
- (BOOL)verifyReceiptHash
{
// TODO: Getting the uuid in Mac is different. See: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
NSUUID *uuid = [[UIDevice currentDevice] identifierForVendor];
unsigned char uuidBytes[16];
[uuid getUUIDBytes:uuidBytes];
// Order taken from: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
NSMutableData *data = [NSMutableData data];
[data appendBytes:uuidBytes length:sizeof(uuidBytes)];
[data appendData:self.opaqueValue];
[data appendData:self.bundleIdentifierData];
NSMutableData *expectedHash = [NSMutableData dataWithLength:SHA_DIGEST_LENGTH];
SHA1(data.bytes, data.length, expectedHash.mutableBytes);
return [expectedHash isEqualToData:self.hash];
}
And that's the gist of it. I might be missing something here or there, so I might come back to this post later. In any case, I recommend browsing the complete code for more details.
I'm surprised nobody mentioned Receigen here. It's a tool that automatically generates obfuscated receipt validation code, a different one each time; it supports both GUI and command-line operation. Highly recommended.
(Not affiliated with Receigen, just a happy user.)
I use a Rakefile like this to automatically rerun Receigen (because it needs to be done on every version change) when I type rake receigen:
desc "Regenerate App Store Receipt validation code using Receigen app (which must be already installed)"
task :receigen do
# TODO: modify these to match your app
bundle_id = 'com.example.YourBundleIdentifierHere'
output_file = File.join(__dir__, 'SomeDir/ReceiptValidation.h')
version = PList.get(File.join(__dir__, 'YourProjectFolder/Info.plist'), 'CFBundleVersion')
command = %Q</Applications/Receigen.app/Contents/MacOS/Receigen --identifier #{bundle_id} --version #{version} --os ios --prefix ReceiptValidation --success callblock --failure callblock>
puts "#{command} > #{output_file}"
data = `#{command}`
File.open(output_file, 'w') { |f| f.write(data) }
end
module PList
def self.get file_name, key
if File.read(file_name) =~ %r!<key>#{Regexp.escape(key)}</key>\s*<string>(.*?)</string>!
$1.strip
else
nil
end
end
end
Note: It's not recommend to do this type of verification in the client side
This is a Swift 4 version for validation of in-app-purchase receipt...
Lets create an enum to represent the possible errors of the receipt validation
enum ReceiptValidationError: Error {
case receiptNotFound
case jsonResponseIsNotValid(description: String)
case notBought
case expired
}
Then let's create the function that validates the receipt, it will throws an error if it's unable to validate it.
func validateReceipt() throws {
guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) else {
throw ReceiptValidationError.receiptNotFound
}
let receiptData = try! Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString()
let jsonObjectBody = ["receipt-data" : receiptString, "password" : <#String#>]
#if DEBUG
let url = URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")!
#else
let url = URL(string: "https://buy.itunes.apple.com/verifyReceipt")!
#endif
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = try! JSONSerialization.data(withJSONObject: jsonObjectBody, options: .prettyPrinted)
let semaphore = DispatchSemaphore(value: 0)
var validationError : ReceiptValidationError?
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, let httpResponse = response as? HTTPURLResponse, error == nil, httpResponse.statusCode == 200 else {
validationError = ReceiptValidationError.jsonResponseIsNotValid(description: error?.localizedDescription ?? "")
semaphore.signal()
return
}
guard let jsonResponse = (try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)) as? [AnyHashable: Any] else {
validationError = ReceiptValidationError.jsonResponseIsNotValid(description: "Unable to parse json")
semaphore.signal()
return
}
guard let expirationDate = self.expirationDate(jsonResponse: jsonResponse, forProductId: <#String#>) else {
validationError = ReceiptValidationError.notBought
semaphore.signal()
return
}
let currentDate = Date()
if currentDate > expirationDate {
validationError = ReceiptValidationError.expired
}
semaphore.signal()
}
task.resume()
semaphore.wait()
if let validationError = validationError {
throw validationError
}
}
Let's use this helper function, to get the expiration date of a specific product. The function receives a JSON response and a product id. The JSON response can contain multiple receipts info for different products, so it get the last info for the specified parameter.
func expirationDate(jsonResponse: [AnyHashable: Any], forProductId productId :String) -> Date? {
guard let receiptInfo = (jsonResponse["latest_receipt_info"] as? [[AnyHashable: Any]]) else {
return nil
}
let filteredReceipts = receiptInfo.filter{ return ($0["product_id"] as? String) == productId }
guard let lastReceipt = filteredReceipts.last else {
return nil
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
if let expiresString = lastReceipt["expires_date"] as? String {
return formatter.date(from: expiresString)
}
return nil
}
Now you can call this function and handle of the possible error cases
do {
try validateReceipt()
// The receipt is valid 😌
print("Receipt is valid")
} catch ReceiptValidationError.receiptNotFound {
// There is no receipt on the device 😱
} catch ReceiptValidationError.jsonResponseIsNotValid(let description) {
// unable to parse the json 🤯
print(description)
} catch ReceiptValidationError.notBought {
// the subscription hasn't being purchased 😒
} catch ReceiptValidationError.expired {
// the subscription is expired 😵
} catch {
print("Unexpected error: \(error).")
}
You can get a Password from the App Store Connect. https://developer.apple.com open this link click on
Account tab
Do Sign in
Open iTune Connect
Open My App
Open Feature Tab
Open In App Purchase
Click at the right side on 'View Shared Secret'
At the bottom you will get a secrete key
Copy that key and paste into the password field.

Resources