iOS Application Security for Beginners
This article provides a brief overview of techniques that can be used in your mobile iOS application to keep it secure enough for the vast majority of cases.
Join the DZone community and get the full member experience.
Join For FreeThis article provides a brief overview of techniques that can be used in your mobile iOS application to keep it secure enough for the vast majority of cases. If you are a junior or middle iOS developer and have not given much thought to security topics, this material will provide a great introduction. I will try to explain it simply, so even if you have no prior knowledge of security, you should be able to follow along.
Although Apple has a closed ecosystem, it still has security vulnerabilities. If you thought that iOS takes care of all the security issues and we as developers can do nothing about it — you were wrong. There is one simple evidence for those of you who held this position: Apple regularly releases security updates to close some of the known iOS vulnerabilities. And God knows how many of them are still open and being used by attackers to gain access to personal and important data in the apps that users have installed on their devices.
There are several simple rules that you can follow in your app to significantly increase its security level:
- Never trust anyone, including users.
- Try to store as little personal data and secrets as possible and only in secure storage.
- Minimize the amount of time your secrets are not secured.
I know it's too vague for now, but let's take some real examples and check how we can increase the level of security by applying those principles and what it will exactly mean.
Application Example
Let's take the most popular and simple application example: Todo List App. Since we don't really care about the app's functionality in this article, I will explain only the parts that affect its security level:
- Application works with an API, and all the notes we make can be shared with other users of the app.
- The application should have a registration and sign-in process.
- The application provides an option to use a PIN or biometrics for authentication.
- The application stores personal data to provide information about the owner of the shared notes.
To manage sessions between our iOS application and backend, let's assume we use JWT tokens as a popular approach. You can read about JWT here.
Here is an example flow that we will have in our application for a sign-up process:
- We create a new user by providing some credentials, such as an email/password process or authentication through Google, Facebook, etc., using OAuth2.
- Backend returns a pair of tokens: Refresh and Access
- Access token has a short lifespan (e.g., 5 minutes) and is used with each API call.
- Refresh token has a longer lifespan (e.g., 5 days) and is used to obtain a new Access token whenever the old one expires.
- To further secure the app, we recommend asking the user to create a PIN. This PIN should be stored securely along with the Refresh token on the device.
- Using the Access token, we make an API call to retrieve the user's dashboard, which displays their personalized information.
To maintain a secure connection between the server and the client, we use token-based authentication. This involves regularly exchanging tokens to ensure that the connection is trustworthy. By doing so, we avoid situations where our application could be connected somewhere and send sensitive information without proper authorization. At the same time, our backend is assured that it sends stored data only to our app and not to an imposter.
The next diagram illustrates the sign-in process when a user opens the application for the second time and already has an account:
- We ask the user to provide a PIN.
- When the user provides their PIN during sign-in, we compare it with the PIN stored during the sign-up process to ensure a secure match.
- If the PIN provided by the user matches the one stored during sign-up, we retrieve the Refresh token from local storage. We then use this Refresh token to obtain a new Access token from our backend.
- Our backend checks the validity of the provided Refresh token. If it is valid, it generates a new Access token and returns it to the client side.
- Using the newly obtained Access token, we make an API call to retrieve the user's dashboard, which displays their personalized information.
Now that we have a better understanding of what we're dealing with let's consider potential issues that could arise.
Do I Need a PIN?
We can question whether requiring a PIN is necessary, especially since the user may already have a PIN or biometric authentication set up on their device. However, we must prioritize security and avoid putting trust in our users to secure their own data. Instead, we can offer the option of using biometry (such as Face ID or Touch ID) for an additional layer of security while still providing those who prefer it the choice to use a PIN. And we also want to follow the first rule: Never trust anyone, including users.
Ok, I Have a PIN, Refresh Token, How Do I Store It?
To ensure the highest level of security, we must store the PIN and Refresh token in a secure location. Unfortunately, many developers make the mistake of storing these sensitive data in unsecured locations such as UserDefaults or local databases. This is a significant security risk, as these locations are not protected by any encryption or other security measures. Instead, we should use the Keychain as the sole storage location for our PIN and Refresh token. By doing so, we can rest assured that our secrets are safe from prying eyes and unauthorized access.
Should I Store the PIN as It Is?
That's another common mistake that developers do: take the PIN from user and store it as it is. You may ask what's wrong with it, it's already in Keychain, encrypted there, all is fine, isn't it? And the answer is not exactly. Check the third rule: The time your secrets are not secured should be as little as possible. When we take our PIN from Keychain and use it in memory (for example you have some let pin: String = ...) you are not following that rule. There is a possibility that using some vulnerabilities from iOS or some other technics, attackers can get an access to stack and dynamic memory of your application, so to minimise the possibility of give them any kind of personal data or secrets, you should keep tham closed even when you work with them in the app.
Another common mistake that developers make is taking the PIN from the user and storing it as-is. You may ask why there's anything wrong with it since it's already encrypted in Keychain, right? However, the answer is not exactly that simple. According to the third rule, the time your secrets are not secured should be as short as possible. When you take the PIN from Keychain and use it in memory (e.g., String pin = ...), you're not following that rule. There is a possibility that using some vulnerabilities in iOS or other techniques, attackers could gain access to the stack and dynamic memory of your application, which could result in them obtaining sensitive personal data or secrets. To minimize this risk, it's important to keep them closed even when you work with them in the app.
The most common approach you can adopt for storing a PIN securely is hashing it.
What Is Hashing and How Does It Help Here?
In simple words Hashing is a process of transforming one data type to another data type the way that the back transformation doesn't exist. So, what does this give us in the case of PIN? Let's say we have a super secured PIN: 1234 from our user. If the attacker gets access to it, he will not only be able to get into the app in case of getting access to the device but also try to use the same PIN for another services that user has since most people prefer to use the same PINs on all the services where PIN is requered. When we hash the PIN, attacker will find something like: owierslkdjfkwheufrw..., that doesn't say anything and it will not be possible to get the original 1234 back from it. The only thing attacker can do is to try find the original PIN that will give the same hash value, that means he need to take all possible permutations of the PIN, hash it and compare with the one he got. And in fact it takes time, if you user more digets for the PIN it takes a lot of time especially if you are using modern Hash algorithms like Argon2 for example, that may take few seconds for one single hash generation.
In simple terms, hashing is a process of converting one data type into another data type in a way that the original data cannot be recovered. So, what does this mean for PINs? Let's say we have a super secure PIN of '1234' from our user. If an attacker gains access to it, they not only can gain access to the app on the device but also try to use the same PIN for other services that the user has since many people prefer to use the same PINs across different services. When we hash the PIN, the attacker will find a randomized (it's not random but looks like it is) output like 'owierslkdjfkwheufrw...'. This output does not reveal any information about the original PIN and cannot be used to retrieve it. The only thing the attacker can do is try all possible permutations of the PIN, hash them, and compare them with the one they have. However, this process takes time, especially if you use modern hash algorithms like Argon2, which may take a few seconds for just one hash generation.
Here is our flow for the PIN now:
- We take the PIN while Signing Up.
- Hash the PIN and store it in Keychain.
- When user wants to Sign In next time we ask him/her for a PIN.
- We take the PIN provided by the user, hash it, and then compare the resulting hash value with the one stored in Keychain.
- If the hashed value of the PIN provided by the user matches the value stored in the Keychain, then we allow the user to access the app or perform the desired action.
Okay, Let’s Hash the Refresh Token as Well To Ensure Its Security While We Work With It
And here, we can't do it. The reason is that the hashed value will not be able to convert back to the original one, and the Refresh token has a lot of information inside that the Backend needs to identify the user. So, we will need to use a different approach because we still want to follow the third rule. What we can do here to protect the tokens (it applies for Access tokens as well) is to come up with an Encrypted In Memory Cache that we will use to store tokens while we are working with them. Apple doesn't provide us with a final solution for this, but it's not difficult to implement using libraries such as CryptoKit, for example.
- We generate an encryption key that will be stored in the Keychain and inject it into our encrypted storage.
- We also inject our Tokens into Encrypted Storage.
- Encrypted Storage is using the Key to encrypt the Tokens and store it in Memory.
- When we need to use the token, we ask Encrypted Storage to provide it to us. It decrypts the token and returns it to us.
- We can use it for API requests and then remove it from memory since we already have an encrypted copy stored in our storage.
What About Personal Data?
We can employ a similar approach for storing personal user data within the app. However, it's not always necessary or possible to do so. For instance, it might be more prudent to avoid storing the information on the device altogether and instead request it from our backend only when necessary to display it on the screen. The less information we store in an open manner, the more secure our application is.
What Else Can We Do for App Security Improvement?
I would also like to highlight three approaches that can be used within the app to enhance security:
- Code obfuscation
- Certificate pinning
- RASP (Runtime Application Self Protection)
Code Obfuscation
Code obfuscation is the process of making code unreadable to make it more difficult for attackers to reverse engineer your app. They may try to find weak points in your algorithms or understand the logic of how your application works and then simulate the same to exploit vulnerabilities in your backend. While code obfuscation cannot prevent reverse engineering entirely, it can significantly complicate the process, making it more challenging for attackers to succeed.
Certificate Pinning
This approach can help protect against man-in-the-middle attacks, where your application thinks it's communicating with your backend, but in reality, it's talking to a different service pretending to be your backend. Certificate Pinning is not a foolproof solution, and it can introduce additional complexity, but it can help detect and prevent such attacks. The basic idea of Certificate Pinning is to maintain a list of trusted SSL certificates that your application can verify, and if an unfamiliar certificate is presented by the backend, your application will recognize it as a spoof and break the connection.
Runtime Application Self-Protection
There are situations where an attacker may attach a debugger to the device and attempt to debug your app to find vulnerabilities or repackage it, try to use code injections, etc. RASP can help prevent this. The idea is that you have a separate module within your application that monitors how your app behaves, and if it detects any suspicious activity, it can automatically shut it down, notify you of the issue, or attempt to prevent the operation from occurring.
Summary
Maintaining the security of your mobile application is a crucial aspect of mobile development. Adhering to the simple rules outlined above can help you avoid potential issues in the future. This article serves as a starting point for delving deeper into the various aspects of mobile application security.
Opinions expressed by DZone contributors are their own.
Comments