FreeRDP
Loading...
Searching...
No Matches
KeystoreHelper.java
1/*
2 AES-256-GCM key-wrapping helper backed by Android Keystore.
3
4 A random 256-bit database key is generated once, wrapped with the
5 Keystore-resident master key, and stored in SharedPreferences as
6 Base64( IV[12] || ciphertext ).
7*/
8
9package com.freerdp.freerdpcore.security;
10
11import android.content.Context;
12import android.content.SharedPreferences;
13import android.security.keystore.KeyGenParameterSpec;
14import android.security.keystore.KeyProperties;
15import android.util.Base64;
16
17import java.security.KeyStore;
18import java.security.SecureRandom;
19
20import javax.crypto.Cipher;
21import javax.crypto.KeyGenerator;
22import javax.crypto.SecretKey;
23import javax.crypto.spec.GCMParameterSpec;
24
25public final class KeystoreHelper
26{
27 private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
28 private static final String KEY_ALIAS = "freerdp_db_master_key";
29 private static final String PREFS_NAME = "freerdp_security_prefs";
30 private static final String PREF_ENCRYPTED_DB_KEY = "encrypted_db_key";
31 private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
32 private static final int GCM_IV_LENGTH = 12;
33 private static final int GCM_TAG_LENGTH = 128; // bits
34 private static final int DB_KEY_LENGTH = 32; // bytes (256-bit)
35
36 private static volatile KeystoreHelper instance;
37
38 private final Context applicationContext;
39
40 private KeystoreHelper(Context context)
41 {
42 this.applicationContext = context.getApplicationContext();
43 }
44
45 public static KeystoreHelper getInstance(Context context)
46 {
47 if (instance == null)
48 {
49 synchronized (KeystoreHelper.class)
50 {
51 if (instance == null)
52 {
53 instance = new KeystoreHelper(context);
54 }
55 }
56 }
57 return instance;
58 }
59
60 public byte[] getOrCreateDbKey() throws KeystoreException
61 {
62 SharedPreferences prefs =
63 applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
64 String encoded = prefs.getString(PREF_ENCRYPTED_DB_KEY, null);
65
66 if (encoded != null)
67 {
68 return decryptDbKey(encoded);
69 }
70
71 byte[] rawKey = new byte[DB_KEY_LENGTH];
72 new SecureRandom().nextBytes(rawKey);
73 String encrypted = encryptDbKey(rawKey);
74 prefs.edit().putString(PREF_ENCRYPTED_DB_KEY, encrypted).apply();
75 return rawKey;
76 }
77
78 private String encryptDbKey(byte[] rawKey) throws KeystoreException
79 {
80 try
81 {
82 SecretKey masterKey = getOrCreateMasterKey();
83 Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
84 cipher.init(Cipher.ENCRYPT_MODE, masterKey);
85 byte[] iv = cipher.getIV();
86 byte[] ciphertext = cipher.doFinal(rawKey);
87
88 // Serialise as Base64( IV[12] || ciphertext )
89 byte[] packed = new byte[GCM_IV_LENGTH + ciphertext.length];
90 System.arraycopy(iv, 0, packed, 0, GCM_IV_LENGTH);
91 System.arraycopy(ciphertext, 0, packed, GCM_IV_LENGTH, ciphertext.length);
92 return Base64.encodeToString(packed, Base64.NO_WRAP);
93 }
94 catch (Exception e)
95 {
96 throw new KeystoreException("Failed to encrypt database key", e);
97 }
98 }
99
100 private byte[] decryptDbKey(String encoded) throws KeystoreException
101 {
102 try
103 {
104 byte[] packed = Base64.decode(encoded, Base64.NO_WRAP);
105 byte[] iv = new byte[GCM_IV_LENGTH];
106 byte[] ciphertext = new byte[packed.length - GCM_IV_LENGTH];
107 System.arraycopy(packed, 0, iv, 0, GCM_IV_LENGTH);
108 System.arraycopy(packed, GCM_IV_LENGTH, ciphertext, 0, ciphertext.length);
109
110 SecretKey masterKey = getOrCreateMasterKey();
111 Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
112 cipher.init(Cipher.DECRYPT_MODE, masterKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
113 return cipher.doFinal(ciphertext);
114 }
115 catch (Exception e)
116 {
117 throw new KeystoreException("Failed to decrypt database key", e);
118 }
119 }
120
121 private SecretKey getOrCreateMasterKey() throws Exception
122 {
123 KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
124 keyStore.load(null);
125
126 if (keyStore.containsAlias(KEY_ALIAS))
127 {
128 return ((KeyStore.SecretKeyEntry)keyStore.getEntry(KEY_ALIAS, null)).getSecretKey();
129 }
130
131 KeyGenerator keyGen =
132 KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER);
133 keyGen.init(
134 new KeyGenParameterSpec
135 .Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
136 .setKeySize(256)
137 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
138 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
139 .build());
140 return keyGen.generateKey();
141 }
142
143 public static final class KeystoreException extends Exception
144 {
145 public KeystoreException(String message, Throwable cause)
146 {
147 super(message, cause);
148 }
149 }
150}