The simple credentials manager receives a pair of Credentials and perform:
- Storage of the values (either 1 by 1 or all together in a single operation)
- Retrieval of the values (either 1 by 1 or all together in a single operation)
- Check for existing credentials
- Check for valid credentials and renew them if required
Because the manager receives a Storage
, which abstracts the logic of reading/writing stuff, how the values are persisted is up to that implementation. What it's missing is a security layer to encrypt the values before saving them, ask for authentication before retrieving them, or both of the previous.
Current CredentialsManager
implementation is here.
- It's not required to extend the
CredentialsManager
but it would be nice. - Must encrypt values before saving them.
- Encryption/Decryption can be done either in the manager or in the Storage implementation.
API 21 includes a [method](https://developer.android.com/reference/android/app/KeyguardManager.html#createConfirmDeviceCredentialIntent(java.lang.CharSequence, java.lang.CharSequence)) to call the LockScreen (same security configured by the user) to ask for authentication. It must be called from an activity expecting a result back either OK or CANCELED.
In addition, by using the Android KeyStore we can generate a new key pair to encrypt/decrypt data. The key usage can be restricted to only authenticated users (since API 23), meaning that if the user hasn't unlocked the device in the past X seconds (configurable) it will throw a UserNotAuthenticated
exception. This is where we can call our Confirm Credentials Screen and show the LockScreen to the user.
We can't know if the user has recently authenticated without trying to use the key first, and because the authentication is an async process we can't block the read/write operation of the Credentials while waiting the user to authenticate. The next 2 sections will discuss how to make the credentials manager store encrypted data and also request authentication before trying any operation. An alternative to this is to delegate the encryption logic to the Storage implementation, and make the manager only request authentication. This is discussed at the bottom.
Add a method to authenticate the user which must be called by the user right before trying to read/write credentials using the manager. The method will try to use the key in order to know if the user is authenticated or not, and if it's not it will show the authentication screen. It can return a flag telling the current authentication status (true->ok to proceed/false->lock-screen was launched).
If the lock-screen is shown, the result of the authentication is received later on the activity. The user must then retry the operation.
//Authenticated Manager
boolean verifyUserId(Activity activity){
try {
useKey();
return true;
} catch (UserNotAuthenticated e){
Intent i = createConfirmCredentialsIntent();
activity.startActivityForResult(i, CONFIRM_REQ_CODE);
return false;
}
}
void encrypt(Credentials credentials) throw UserNotAuthenticated{
}
void decrypt(Callback callback) throw UserNotAuthenticated{
//Callback required if the credentials exists but have expired and can be refreshed.
//The Auth0 API will be called to renew them.
}
//Activity
AuthenticatedManager manager;
//->Read
checkAuth(){
if (manager.verifyUserId(this)){
manager.decrypt(new CredentialsCallback(){
void onSuccess(Credentials credentials){
//use them
}
})
}
}
//->Write
login(){
apiClient.login("usr", "pwd").start(new LoginCallback(){
void onSuccess(Credentials credentials){
if (manager.verifyUserId(this)){
manager.encrypt(credentials);
}
}
});
}
onActivityResult(int resultCode, int requestCode, Intent data){
if (requestCode==CONFIRM_REQ_CODE){
//Problem: we need to keep the credentials (when writing) or the callback (when reading) somewhere to retry the operation
}
}
Similar to 1 but perform the authentication check in the same encrypt/decrypt method.
//Authenticated Manager
void encrypt(Activity activity, Credentials credentials) {
try {
encryptUsingKey();
return;
} catch (UserNotAuthenticated e){
//Save used parameters to retry later
this.lastOperation = ENCRYPT;
this.credentials = credentials;
Intent i = createConfirmCredentialsIntent();
activity.startActivityForResult(i, CONFIRM_REQ_CODE);
return;
}
}
void decrypt(Activity activity, Callback callback) {
//Callback required if the credentials exists but have expired and can be refreshed.
//The Auth0 API will be called to renew them.
try {
decryptUsingKey();
return;
} catch (UserNotAuthenticated e){
//Save used parameters to retry later
this.lastOperation = DECRYPT;
this.callback = callback;
Intent i = createConfirmCredentialsIntent();
activity.startActivityForResult(i, CONFIRM_REQ_CODE);
return;
}
}
boolean notifyAuthenticationResult(int resultCode, int requestCode){
if(requestCode!=CONFIRM_REQ_CODE){
return false;
//Ignore other codes
}
//Process our code
if (resultCode == OK){
if (this.lastOperation==DECRYPT){
decrypt(this.callback);
} else {
encrypt(this.credentials);
}
} else if (this.lastOperation==DECRYPT){
this.callback.onFailed(new AuthenticationCanceledException("User went back"));
}
clearState();
//true means that this method consumed the call, either in a good or bad way.
return true;
}
void clearState(){
this.credentials=null;
this.callback=null;
this.lastOperation=null;
}
//Activity
AuthenticatedManager manager;
//->Read
checkAuth(){
manager.decrypt(this, new CredentialsCallback(){
void onSuccess(Credentials credentials){
//use them
}
});
}
//->Write
login(){
apiClient.login("usr", "pwd").start(new LoginCallback(){
void onSuccess(Credentials credentials){
manager.encrypt(this, credentials);
}
});
}
onActivityResult(int resultCode, int requestCode, Intent data){
if(manager.notifyAuthenticationResult(resultCode, requestCode)){
return;
}
}
- I'd prefer not to pass the
activity
context in the constructor of the class so we don't keep it tied to the instance. I think it's better to pass it on every read/write operation, although if the user remembers to callmanager=null
in the activity destroy he's covered. But we do need to keep some state in order to retry the last operation without asking the user to pass the same values again, because in some cases it might not be possible (too many levels or in line declaration of callbacks, etc..). requestCode
should be customized. This can be a parameter in the constructor, maybe providing one as default but having a second constructor to allow to change it.- As a pro: Users can still choose the Storage implementation they want.
- If the authentication is canceled (user goes back) we should still notify the callback (decryption) with an exception.
- 16:
isKeyguardSecure()
. To know if the user has setup a pin/pattern/password/fingerprint on the LockScreen. Required to show the confirm credentials dialog. - 18:
AndroidKeyStore provider
. To generate key pairs and require user authentication to use them. - 21:
createConfirmDeviceCredentialIntent()
. To show the configured LockScreen to the user. - 23:
setUserAuthenticationRequired()
. To make the key require the user being recently authenticated.
I might be able to make it work using API 18, as there's an alternate "show LockScreen" method in that API but haven't tested it yet.
So TL;DR
General
The CredentialsManager is a convenience utility method we added and I would say the audience is non power user. So don't feel you need to make it generic for the user to plugin their own storage if you don't want to.
I/O
In general you when you access any "disk" you want to minimise write operations. So you should always write in one operation and subsequently you have one read.