Saturday, August 25, 2018

Understanding iOS NSNotification






iOS notification center delivers notifications to observers synchronously. In other words, when posting a notification, control does not return to the poster until all observers have received and processed the notification. 
To send notifications asynchronously use a notification queue, which is described in Notification Queues.
In a multithreaded application, notifications are always delivered in the thread in which the notification was posted, which may not be the same thread in which an observer registered itself.

This is similar to how javascript addEventListener and dispatchEvent works, so when dispatchEvent is called, the method will not return to caller until all added event listeners returns from their event handler call back methods.
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent

Wednesday, August 15, 2018

Utility functions for ios NSUserDefault

1. Dump all items in NSUserDefault


NSArray *keys = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys];

for(NSString* key in keys){
        NSLog(@"%@ : %@",key, [[NSUserDefaults standardUserDefaults] valueForKey:key]);
}



2. Observe changes in NSUserDefault


- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
  [[NSUserDefaults standardUserDefaults] addObserver:self
                                            forKeyPath:@"mykey"
                                               options:NSKeyValueObservingOptionNew
                                               context:NULL];

    self.viewController = [[MainViewController alloc] init];
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (void)observeValueForKeyPath:(NSString *) keyPath ofObject:(id) object change:(NSDictionary *) change context:(void *) context
{
    if([keyPath isEqual:@"mykey"])
    {
       NSLog(@"SomeKey change: %@", change);
    }
}

3. Settings bundle with NSUserDefault
Application can use ios Settings app to allow user to configure settings. However, settings put into Settings.bundle root.plist will not be automatically picked up when app reads settings from NSUserDefault. The settings only apply to NSUserDefault after user change the value from settings app.
So if user has not change a setting from ios setting app, read the key from nsuserdefault will return nil, which means the key does not exist in NSUserDefault.
One way to overcome this issue is calling NSUserDefault regsiterDefault method, this will set the initial values in NSUserDefault for all the keys in NSDictionary parameter. 

Friday, August 3, 2018

How to revert from iOS 12 beta to iOS 11

Recently a device was upgraded to 12 beta for testing, but as the device was previously used by other developers, so there is no backup available for restoring the device to ios 11.

Tried few things mentioned in web, but unfortunately none of them works.

Finally, find a simple way to do so, by just deleting the ios 12 beta installation profile from device settings app at Settings->general->Profile & Device Management, restore the device using iTune. It will restore the device to a new ios 11.4.1 setup.

Monday, July 16, 2018

Convert iOS app bundle (.app file) to ipa file

When xcode builds a project, it generates an app bundle as output. In order to install the app bundle to iOS device, the app bundle file needs to be converted to a ipa file.

The following steps can be used to convert app bundle to ipa file
1. Create a folder named as "Payload"
2. drag and drop the app bundle file (myapp.app file) into Payload folder. You can get the .app file from xcode derived folder
3. Compress the payload folder to a zip file
4. Rename the zip file to ipa file


Saturday, July 7, 2018

Understanding SAP UI5 view navigation and routing

SAP UI5 application manifest.json file defines targets and routes to manage page (view) navigation.

Usually, there is a root xml view, which does not contain any UI content, its only purpose is define an App id for holding other xml views

Root.view.xml

<mvc:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" controllerName="ui5.test.ui5.controller.Root"
xmlns:html="http://www.w3.org/1999/xhtml">
<App id ="root">
<pages/>
</App>
</mvc:View>  

The real UI are defined in several xml views. For example, an app may define three views
MainView.view.xml
SecondView.view.xml
ThirdView.view.xml

For each view, a target should be defined, which has below properties:
name:
target name is used by route to identify a unique target.
viewName:
indicates when this target is selected, which view will be displayed.

Then for each navigation operation, a route should be defined. Route name is used by navTo method to navigate the application to different xml view.

Route has the below properies
name:
unique name.
Target:
The target to show for the route.
Pattern:
the matched url pattern for selecting a particular route. The first pattern should be empty, so the route will be used to load the default xml view if not url available to select a particular route. Pattern can also include parameter, which can be set and got by view controller.

In view controller, javascript code can call navTo on router object to load a particular route based on name. For example: the below code load "ThirdViewTarget"
onShowThirdView: function (oEvent) {
//This code was generated by the layout editor.
var oRouter = sap.ui.core.UIComponent.getRouterFor(this);
oRouter.navTo("ThirdViewTarget");
},

navTo method can also include parameter for passing context information to the target view as below
onListItemPressed: function (evt) {
 
// Get Property of the Clicked Item. i.e. PO number of the item which was clicked
var selectPO = evt.getSource().getBindingContext().getProperty("ProductID");
 
// Now Get the Router Info
var oRouter = sap.ui.core.UIComponent.getRouterFor(this);
 
// Tell the Router to Navigate To Route_PODetail which is linked to V_PODetail view
oRouter.navTo("SecondTarget", { ProductID: selectPO});
}


The sample manifest.json is attached below:
{
"_version": "1.8.0",
"sap.app": {
"id": "ui5.test.ui5",
"type": "application",
"i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
"title": "{{appTitle}}",
"description": "{{appDescription}}",
"sourceTemplate": {
"id": "servicecatalog.connectivityComponentForManifest",
"version": "0.0.0"
},
"dataSources": {
"Northwind.svc": {
"uri": "/John/NorthwindDest/v2/Northwind/Northwind.svc/",
"type": "OData",
"settings": {
"odataVersion": "2.0",
"localUri": "localService/metadata.xml"
}
}
}
},
"sap.ui": {
"technology": "UI5",
"icons": {
"icon": "",
"favIcon": "",
"phone": "",
"phone@2": "",
"tablet": "",
"tablet@2": ""
},
"deviceTypes": {
"desktop": true,
"tablet": true,
"phone": true
},
"supportedThemes": ["sap_hcb", "sap_belize"]
},
"sap.ui5": {
"rootView": {
"viewName": "ui5.test.ui5.view.Root",
"type": "XML"
},
"dependencies": {
"minUI5Version": "1.30.0",
"libs": {
"sap.ui.layout": {},
"sap.ui.core": {},
"sap.m": {}
}
},
"contentDensities": {
"compact": true,
"cozy": true
},
"models": {
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "ui5.test.ui5.i18n.i18n"
}
},
"": {
"uri": "/John/NorthwindDest/v2/Northwind/Northwind.svc/",
"type": "sap.ui.model.odata.v2.ODataModel",
"settings": {
"defaultOperationMode": "Server",
"defaultBindingMode": "OneWay",
"defaultCountMode": "Request"
},
"dataSource": "Northwind.svc",
"preload": true
}
},
"resources": {
"css": [{
"uri": "css/style.css"
}]
},
"routing": {
"config": {
"routerClass": "sap.m.routing.Router",
"viewType": "XML",
"async": true,
"viewPath": "ui5.test.ui5.view",
"controlAggregation": "pages",
"controlId": "root",
"clearControlAggregation": false,
"viewLevel": 0,
"bypassed": {
"target": []
}
},
"routes": [{
"name": "MainTarget",
"pattern": "",
"titleTarget": "",
"greedy": false,
"target": ["MainTarget"]
}, {
"name": "SecondTarget",
"pattern": "second/{ProductID}",
"titleTarget": "",
"greedy": false,
"target": ["SecondTarget"]
}, {
"name": "ThirdViewTarget",
"pattern": "third",
"titleTarget": "",
"greedy": false,
"target": ["ThirdViewTarget"]
}],
"targets": {
"SecondTarget": {
"viewType": "XML",
"transition": "slide",
"clearControlAggregation": true,
"viewName": "SecondView",
"viewLevel": 2,
"routerClass": "sap.m.routing.Router",
"async": true,
"viewPath": "ui5.test.ui5.view",
"controlAggregation": "pages",
"controlId": "root",
"bypassed": {
"target": []
}
},
"MainTarget": {
"viewType": "XML",
"transition": "slide",
"clearControlAggregation": true,
"viewName": "MainView",
"viewLevel": 1,
"routerClass": "sap.m.routing.Router",
"async": true,
"viewPath": "ui5.test.ui5.view",
"controlAggregation": "pages",
"controlId": "root",
"bypassed": {
"target": []
}
},
"ThirdViewTarget": {
"viewType": "XML",
"transition": "slide",
"clearControlAggregation": true,
"viewName": "ThirdView",
"viewLevel": 2
}
}
}
}
}

Friday, May 25, 2018

Download IPA file from Apple app store

Sometimes it is important to download an ipa file from iOS app store to examine the app resource and configuration, or re-sign the ipa file for testing purpose.

The following steps can be used to do so

1. install Apple Configurator 2 on your mac.
2. From Top menu bar, select Account and add your AppID account
3. Connect your ios device
4. In the current window, click "add" button and select the app you want to download
5. When the dialog asks you about "Would you like to replace it with the one you are adding", from Finder, goto the below folder and search for the ipa file just downloaded. Copy the ipa file to a different folder for you to use, then click the stop button

~/Library/Group\ Containers/K36BKF7T3D.group.com.apple.configurator/Library/Caches/Assets/TemporaryItems/MobileApps


Thursday, May 24, 2018

Share encrypted data between ios and android clients

It is a common use case to use password to encrypt sensitive data in mobile applications. The basic logic is first deriving an encryption key based on password, and then use the password to encrypt the sensitive data.

The question is when using the same algorithm and password on both ios and android client,
1. Would both clients generate the same encryption key?
2. If so, could data encrypted from one client be decrypted by another client?

Question 1: Encryption key generation

For this testing, key generation is implemented using PBKDF2.

For iOS client (iOS 11.3 on iphone 7) , CCKeyDerivationPBKDF method can be used for this purpose. For testing purpose, the password is hardcoded as "password", and the salt is hardcoded as "salt", and the hash algorithm is KCCPRFHmacAlgSHA512,  the round is set to 100. Note the derived keysize should match the selected algorithm, there is no point to use KCCPRFHmacAlgSHA512 to generate an 32 bytes key (256 bit), as the total result hash is limited to 32 byte instead of 64 byte. Actually in iOS, if the keysize is smaller than the algorithm generated keysize, the generated key will be truncated to the specified keysize.

        var password = "password"
        var salt = "salt"
        var derivedKey : NSMutableData = NSMutableData(length: kCCKeySizeAES256)!
        var passwordData = NSString(string: password).utf8String
        var passwordDataSize = password.utf8.count
        var saltData = NSString(string: salt).utf8String
        var saltDataSize = salt.utf8.count
        var saltDataPointer = UnsafeRawPointer(saltData!).bindMemory(to:UInt8.self, capacity:saltDataSize)

        CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), passwordData,
                             passwordDataSize, saltDataPointer, saltDataSize,
                             CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
                             uint(100),
                             UnsafeMutablePointer<UInt8>         (derivedKey.mutableBytes.assumingMemoryBound(to:UInt8.self)),
                             derivedKey.length)

For Android client, to match the iOS side configuration, SecretKeyFactory is used with the algorithm of PBKDF2withHmacSHA512. Note this algorithm is only supported on Android API 26 (Android 8.0) and above. The testing is done on Samsung S9 device.

SecretKeyFactory provider = SecretKeyFactory.getInstance("PBKDF2withHmacSHA256");
String password = "password";
String salt = "salt";
char[] passwordData = password.toCharArray();
byte[] saltData = salt.getBytes("UTF8");
KeySpec keySpec = new PBEKeySpec(passwordData, saltData, 100, 256);
SecretKey key = provider.generateSecret(keySpec);
byte[] keydata = key.getEncoded();

The test shows both ios and android client generate the same encryption key as showing below in hex format, so this confirms the ios and android clients can derive the same encryption key using the same password and algorithm.

"07e6997180cf7f12904f04100d405d34888fdf62af6d506a0ecc23b196fe99d8"


Question 2: Data encryption

Now that we know that the same key can be generated by ios and android client based on the same password, the second steps is checking whether data encrypted by one client can be decrypted by another client using the same encryption key.

The IV is set to a 16 byte array of 0.  The data is a simple string of "this is a testing string".

On ios the encrypt method is using CCCrypte

        var outLength : size_t = 0;
        let cipherData : NSMutableData? = NSMutableData(length: dataToEncrypt.count + kCCBlockSizeAES128);
        let ivb : [UInt8] = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
        let iv = NSData(bytes: ivb, length: 16)
        let result = CCCrypt(            UInt32(kCCEncrypt), // operation
            UInt32(kCCAlgorithmAES128), // algorithm
            UInt32(kCCOptionPKCS7Padding), // options
            (encryptionKey as NSData).bytes, // key
            encryptionKey.count, // keylength
            iv.bytes, // iv
            (dataToEncrypt as NSData).bytes, // dataIn
            dataToEncrypt.count, // dataInLength,
            UnsafeMutablePointer<UInt8>(cipherData!.mutableBytes.assumingMemoryBound(to:UInt8.self)), // dataOut
            cipherData!.length, // dataOutAvailable
            &outLength); // dataOutMoved


On Android client, Cipher is used to encrypt data as below

byte[] dataByteToEncrypt = dataToEncrypt.getBytes("UTF8");
Cipher dataCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
dataCipher.init(Cipher.ENCRYPT_MODE, key, iv );
byte[] encrypedData = dataCipher.doFinal(dataByteToEncrypt);

On both client platform, the encryption generates the same output byte array of
"92a78f657da19a444e28c83f604a63401dc9a81300dcf4b2707fe66a9d62f158"ta 

Conclusion:
The testing result indicates when the same algorithm, salt, iv and password are used, the data encrypted in one platform can be decrypted in another platform, so there is no need to use external third party library to handle data encryption/decryption when the encrypted data need to be transferred between android and ios clients.