>_ Unvalidated input

stdin: is not a tty

iOS Storage Security

As a mobile developer, it’s prudent to assume that once data reaches a phone, it’s already at risk of compromise. Proper data security measures depend on the sensitivity of the information being stored. It’s crucial to comprehend the consequences of losing any data you choose to save on a device due to a silent jailbreak or root exploit.

Guiding Principle

Do not store data unless absolutely necessary. Do not store data for longer than you need.

Data classification

Data classification Type of data stored on the client side GDPR Classification Volume of data
Higly Sensitive Payment data or sensitive personal data of the owner of the device or individuals other than the owner. Sensitive Personal One or many individuals
Sensitive Personal data of individuals other than the owner of the mobile device. Personal Many individuals
Consumer grade Personal data that relates to the owner of the device only. Personal One individual

Highly sensitive data

Highly sensitive data refers to any information that may be stored on the device related to identifiable individuals other than the owner of the mobile device. This category also encompasses sensitive personal data (as classified by GDPR) but also payment data like credit cards and user passwords.

Example: Payment card data, passwords, genetic data, biometric data, health information, sexual orientation, any sensitive personal data related to one or many users.

Sensitive data

Sensitive data refers to any information that, if compromised, could potentially put at risk not only the owner but also other users of the service.

Example: Shared secrets, shared API keys, personal data of individuals other than the owner of the mobile device.

Consumer grade data

Consumer grade data refers to any remaining data that may be stored on the device to enable the application to function and data related to the owner of the device.

Example: User settings, user preferences, tickets, bookings, transactions, reference data, default payment method, user related configuration, any data that is related to the owner of the device only.

Storing highly sensitive data

Storing highly sensitive data on mobile devices should be avoided at all costs. The preferred method is to store data securely on the server side.

However, if client-side storage is absolutely necessary, the only acceptable approach is to encrypt the data using a secret key that remains with the user at all times. This requires asking the user for a passphrase during the first use. The passphrase should be fed into a key derivation function, such as PBKDF2, along with a random salt to generate a secure key for encryption. It’s important to note that the passphrase cannot be stored on the device or within the source code. Additionally, the encryption algorithm must be industry-tested and cannot rely on the standard iOS encryption. While the key may be cached in memory, it should be securely erased after decryption. The passphrase must be complex, alphanumeric and not a 4 digit PIN.

Open source products like SQLCipher with built-in key derivation have a good security track record, but proper key management is crucial, as outlined above. Any implementation must be subjected to a thorough security review.

It should be noted that user passwords fall within the highly sensitive data category and storage on the client-side is prohibited. Instead, token-based authentication can be used as it relies on exchanging the password for an access token and effectively downgrades the sensitivity of the data to the consumer grade.

Storing sensitive data

To prevent putting multiple users at risk, sensitive information should not be stored on mobile devices. Permanent storage of sensitive data owned by your organisation should be avoided unless absolutely necessary, such as for offline operations. Unprotected storage of sensitive information or data in an easily accessible location on the device may lead to unintended data leakage, making it susceptible to theft and resulting in fraud and reputational damage. The default iOS encryption and Keychain have inherent weaknesses, such as relying on the user’s device passcode, which is often a 4-digit PIN. Therefore, sensitive data should not solely rely on the default iOS encryption, and a defense-in-depth mechanism is necessary to protect the data. The recommended approach is to encrypt the data with a secret key derived from a passphrase provided by the user during the first use. If the passphrase cannot be provided each time the user requires access to sensitive data, custom obfuscation can be implemented to reduce the number of attack vectors.

As a developer, it’s crucial to comprehend the potential consequences of losing sensitive data and for the business to accept the risk. To mitigate this risk, obfuscation techniques can be used to make it harder for attackers to access the data. These techniques may include custom encryption using tools like

  • Common Crypto
  • code obfuscation
  • iOS encryption
  • application shielding
  • anti-jailbreak measures
  • anti-debugging techniques

However, it’s essential to keep in mind that these techniques do not guarantee complete protection, and additional measures such as thorough security reviews and a defense-in-depth approach should also be implemented.

Instead of generating a random encryption key and storing it on the device (e.g., in Keychain), a more secure approach is to derive the key each time it is required based on multiple constituents:

  • A partial secret stored in the code
  • A randomly generated salt stored securely in the Keychain
  • The device’s unique hardware identifier, such as its UDID, UUID, MAC Address, Advertising Identifier or Vendor Identifier

Although obfuscation itself doesn’t add any significant security measures, it can still be useful in making it more challenging for potential attackers to understand the app’s operation. This can potentially lower the number of attack vectors and make the app less vulnerable to exploitation.

Example approach to deriving an encryption key in an obfuscated manner

1. Store a partial secret in the source code:

 // static NSString *myKey = @"eif1Queaxe" //avoid - can be easilly found when strings command is run against the binary
 unsigned char myKey[] = { 0x65, 0x69, 0x66, 0x31, 0x51, 0x75, 0x65, 0x61, 0x78, 0x65 } // preferred

2. Generate a random salt and store it securely in Keychain. The most secure way to generate the salt is to use arc4random():

 - (NSData*)generateSalt256 {
     unsigned char salt[32];
     for (int i=0; i<32; i++) {
         salt[i] = (unsigned char)arc4random();
     }
     return [NSData dataWithBytes:salt length:32];
 }

 NSData* salt = [self generateSalt256];

3. Fetch the unique hardware identifier for the device e.g. MAC address:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#import <Foundation/Foundation.h>
#include <openssl/evp.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <net/if.h>
#include <net/if_dl.h>
- (NSString *)query_mac {
    int mib[6];
    size_t len;
    char *buf;
    unsigned char *ptr;
    struct if_msghdr *ifm;
    struct sockaddr_dl *sdl;
    mib[0] = CTL_NET;
    mib[1] = AF_ROUTE;
    mib[2] = 0;
    mib[3] = AF_LINK;
    mib[4] = NET_RT_IFLIST;
    if ((mib[5] = if_nametoindex("en0")) == 0)
        return NULL;
    if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0)
        return NULL;
    if ((buf = malloc(len)) == NULL)
        return NULL;
    if (sysctl(mib, 6, buf, &len, NULL, 0) < 0)
        return NULL;
    ifm = (struct if_msghdr *)buf;
    sdl = (struct sockaddr_dl *)(ifm + 1);
    ptr = (unsigned char *)LLADDR(sdl);
    NSString *out = [ NSString
        stringWithFormat:@ "%02X:%02X:%02X:%02X:%02X:%02X",
        *ptr, *(ptr+1), *(ptr+2), *(ptr+3), *(ptr+4), *(ptr+5) ];
    free(buf);
    return out
 }

4. Combine these two by XORing the hard coded secret key with SHA1 of the device’s MAC address:

 #include <CommonCrypto/CommonCrypto.h>

 unsigned char myKey[] = { 0x65, 0x69, 0x66, 0x31, 0x51, 0x75, 0x65, 0x61, 0x78, 0x65 }

 // Get the SHA1 of the devices MAC Address, to form the obfuscator.
 unsigned char obfuscator[CC_SHA1_DIGEST_LENGTH];
 // Fetch the device's MAC address
 NSString *deviceMac = [ myObject query_mac ];
 CC_SHA1(deviceMac.bytes, (CC_LONG)deviceMac.length, obfuscator);

 // XOR the MAC address against the key to from the real secret
 for (int i=0; i<sizeof(obfuscatedSecretKey); i++) {
    actualSecret[i] = myKey[i] ^ obfuscator[i];
 }

5. Use the calculated secret from the previous step to derive the encryption key:

 #import <CommonCrypto/CommonKeyDerivation.h>

 NSData* mySecret = [actualSecret dataUsingEncoding:NSUTF8StringEncoding];
 // Ask CommonCrypto how many rounds to use so the process takes 0.1s ?
 int rounds = CCCalibratePBKDF(kCCPBKDF2, mySecret.length, salt.length, kCCPRFHmacAlgSHA256, 32, 100);
 unsigned char key[32];

 // Use PBKDF2 to derive the key
 CCKeyDerivationPBKDF(kCCPBKDF2, mySecret.bytes, mySecret.length, salt.bytes, salt.length, kCCPRFHmacAlgSHA256, rounds, key, 32);
 NSData* keyData = [NSData dataWithBytes:key length:32];

6. As soon as the key is no longer necessary make sure to overwrite it:

 memset([ keyData bytes ], 0, [ keyData length ]);

The symmetric encryption key can be generated each time an encryption or decryption operation is required using the method described above. The salt, which is stored in the keychain, is unique to the device, and if it’s lost, the encrypted data will become unreadable, effectively becoming a de facto key in this scenario.

Alternatively, the salt can be derived from the device’s MAC address, and the secret hidden in the source code can be directly used in PBKDF2. This approach would eliminate the need for anything to be stored in Keychain. However, this would also mean that anyone who understands the process could decrypt the data from any device as long as they have the device’s MAC address or can brute force it. If other variations of the above approach are used, such as XOR with a known class name, it can further complicate the process.

Jailbroken devices

It is highly recommended for applications to implement a jailbreak detection mechanism to enhance their security posture. A jailbroken device is less secure because it can bypass many of the built-in security mechanisms and protections that are designed to protect the device and its data. By detecting whether the device has been jailbroken, an application can respond accordingly e.g. by deleting all its Keychain items and sensitive data files and inform the user.

One common approach to jailbreak detection is to look for the presence of known jailbreak files, directories, or system calls that are typically associated with jailbreaking. If any of these are detected, the application can take appropriate action, such as refusing to run or deleting sensitive data.

1
2
3
4
5
if ([[NSFileManager defaultManager] fileExistsAtPath:@"/bin/bash"] ||
    [[NSFileManager defaultManager] fileExistsAtPath:@"/Applications/Cydia.app"] ||
    [[NSFileManager defaultManager] fileExistsAtPath:@"/private/var/lib/apt"]) {
    // this device is jailbroken
}

It is also important to note that jailbreak detection is not foolproof, as there are many ways for a determined attacker to bypass these measures.

t is recommended to use commercial jailbreak detection solutions as they are constantly updated to detect new ways of bypassing security measures. relying solely on a single method may not be sufficient to detect all possible jailbreaks. Commercial solutions typically provide a more comprehensive approach to detecting jailbreaks by combining multiple methods and techniques to ensure a higher level of accuracy.

Anti Debugging

Any attempt to obfuscate the application code should be complemented with counter-tampering mechanisms to ensure the integrity of the app’s execution. One such mechanism is process trace checking:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <unistd.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <string.h>
static int check_debugger( ) __attribute__((always_inline));
int check_debugger( )
{
    size_t size = sizeof(struct kinfo_proc);
    struct kinfo_proc info;
    int ret, name[4];
    memset(&info, 0, sizeof(struct kinfo_proc));
    name[0] = CTL_KERN;
    name[1] = KERN_PROC;
    name[2] = KERN_PROC_PID;
    name[3] = getpid();
    if (ret = (sysctl(name, 4, &info, &size, NULL, 0))) {
        return ret; /* sysctl() failed for some reason */
    }
    return (info.kp_proc.p_flag & P_TRACED) ? 1 : 0;
}

App shielding

App shielding uses a combination of techniques such as code obfuscation, encryption, and runtime checks to detect and prevent malicious activities. There are third-party solutions that offer app shielding as a service.

Storing consumer grade data

Apple’s iOS offers a range of built-in data protection mechanisms that are generally sufficient for safeguarding consumer-grade data, i.e., any information related to the device owner. To ensure optimal security, it is recommended to store such data in secure containers that leverage Apple-provided APIs.

iOS device encryption

Although iOS does have an encrypted file system, it is typically unlocked from the moment the operating system boots up because both the OS and applications need access to it. This means that even when the device is locked with a PIN or passphrase, the encrypted file system can still be read by the operating system. As a result, your data is not encrypted using an encryption method that relies on your password for the most part.

Keychain

The Keychain is a secure database maintained by Apple that stores sensitive information, encrypted using a passphrase, which may be a simple 4-digit numeric passcode.

While the Keychain has several attributes, only two are recommended. It is advisable to set these attributes explicitly rather than relying on the default settings provided by iOS:

Attribute Data is Note
kSecAttrAccessibleWhenUnlocked Only accessible when the device is unlocked. Default
kSecAttrAccessibleWhenUnlockedThisDeviceOnly Only accessible when the device is unlocked. Data is not migrated via backups. Prefered

The usage of the remaining keychain attributes should be carefully considered and generally avoided.

For example, iOS defaults to a simple four-digit numeric passcode for the keychain passphrase. To deduce this passcode in iOS 8 and access the file system, an attacker would need to iterate through all 10,000 possible combinations. With root code execution on the device, this would take about 20 minutes, and could be done in the kernel without triggering a wipe after too many failed attempts.

However, recent hardening efforts in iOS 8 have made it no longer feasible to get root execution unless one possesses a rare low-level 0day exploit. Additionally, there are mitigation controls in place, such as Find My iPhone, which allows users to remotely locate, lock, and wipe a lost device.

Core Data

To safeguard larger or more diverse consumer data, iOS offers the default data protection API, which features four file protection classes similar to keychain protection classes. However, the three most significant classes are:

  • Complete (locked when the device is locked)
  • Complete until First Authentication (locked, until the user’s unlocked the device once after the most recent reboot)
  • None (no additional protections)
1
2
3
4
5
// Make sure the database is encrypted when the device is locked
NSDictionary *fileAttributes = [NSDictionary dictionaryWithObject:NSFileProtectionComplete forKey:NSFileProtectionKey];
if (![[NSFileManager defaultManager] setAttributes:fileAttributes ofItemAtPath:[storeURL path] error:&error]) {
    // Deal with the error
}

User passwords

Storing a user’s password on the client-side is a critical security concern and should be avoided. Instead, applications should use token-based authentication to exchange the user’s password for an access token. Once the token is obtained, the password should be securely removed from memory. To store authentication tokens, iOS Keychain APIs should be used. These tokens can be revoked in case of a lost or stolen device or a compromised session. Unlike passwords, which can be reused by users to access other services, tokens are unique to the application.

Device passcode

As explained above the default iOS encryption keying is tied to user’s device passcode. It is important to ensure that the application detects when the passcode is not in effect and inform the user about risks before storing any data e.g. train tickets. Starting from iOS 8 it is possible to programmatically detect whether the passcode is enabled or not. See https://github.com/liamnichols/UIDevice-PasscodeStatus for more details.