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 Copyright 2026 Ibrahim Sevinc <ibrahim.sevinc.mail@gmail.com>
9
10 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
11 If a copy of the MPL was not distributed with this file, You can obtain one at
12 http://mozilla.org/MPL/2.0/.
13*/
14
15package com.freerdp.freerdpcore.security;
16
17import android.content.Context;
18import android.content.SharedPreferences;
19import android.security.keystore.KeyGenParameterSpec;
20import android.security.keystore.KeyProperties;
21import android.util.Base64;
22
23import java.security.KeyStore;
24import java.security.SecureRandom;
25
26import javax.crypto.Cipher;
27import javax.crypto.KeyGenerator;
28import javax.crypto.SecretKey;
29import javax.crypto.spec.GCMParameterSpec;
30
31public final class KeystoreHelper
32{
33 private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
34 private static final String KEY_ALIAS = "freerdp_db_master_key";
35 private static final String PREFS_NAME = "freerdp_security_prefs";
36 private static final String PREF_ENCRYPTED_DB_KEY = "encrypted_db_key";
37 private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
38 private static final int GCM_IV_LENGTH = 12;
39 private static final int GCM_TAG_LENGTH = 128; // bits
40 private static final int DB_KEY_LENGTH = 32; // bytes (256-bit)
41
42 private static volatile KeystoreHelper instance;
43
44 private final Context applicationContext;
45
46 private KeystoreHelper(Context context)
47 {
48 this.applicationContext = context.getApplicationContext();
49 }
50
51 public static KeystoreHelper getInstance(Context context)
52 {
53 if (instance == null)
54 {
55 synchronized (KeystoreHelper.class)
56 {
57 if (instance == null)
58 {
59 instance = new KeystoreHelper(context);
60 }
61 }
62 }
63 return instance;
64 }
65
66 public byte[] getOrCreateDbKey() throws KeystoreException
67 {
68 SharedPreferences prefs =
69 applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
70 String encoded = prefs.getString(PREF_ENCRYPTED_DB_KEY, null);
71
72 if (encoded != null)
73 {
74 return decryptDbKey(encoded);
75 }
76
77 byte[] rawKey = new byte[DB_KEY_LENGTH];
78 new SecureRandom().nextBytes(rawKey);
79 String encrypted = encryptDbKey(rawKey);
80 prefs.edit().putString(PREF_ENCRYPTED_DB_KEY, encrypted).apply();
81 return rawKey;
82 }
83
84 private String encryptDbKey(byte[] rawKey) throws KeystoreException
85 {
86 try
87 {
88 SecretKey masterKey = getOrCreateMasterKey();
89 Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
90 cipher.init(Cipher.ENCRYPT_MODE, masterKey);
91 byte[] iv = cipher.getIV();
92 byte[] ciphertext = cipher.doFinal(rawKey);
93
94 // Serialise as Base64( IV[12] || ciphertext )
95 byte[] packed = new byte[GCM_IV_LENGTH + ciphertext.length];
96 System.arraycopy(iv, 0, packed, 0, GCM_IV_LENGTH);
97 System.arraycopy(ciphertext, 0, packed, GCM_IV_LENGTH, ciphertext.length);
98 return Base64.encodeToString(packed, Base64.NO_WRAP);
99 }
100 catch (Exception e)
101 {
102 throw new KeystoreException("Failed to encrypt database key", e);
103 }
104 }
105
106 private byte[] decryptDbKey(String encoded) throws KeystoreException
107 {
108 try
109 {
110 byte[] packed = Base64.decode(encoded, Base64.NO_WRAP);
111 byte[] iv = new byte[GCM_IV_LENGTH];
112 byte[] ciphertext = new byte[packed.length - GCM_IV_LENGTH];
113 System.arraycopy(packed, 0, iv, 0, GCM_IV_LENGTH);
114 System.arraycopy(packed, GCM_IV_LENGTH, ciphertext, 0, ciphertext.length);
115
116 SecretKey masterKey = getOrCreateMasterKey();
117 Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
118 cipher.init(Cipher.DECRYPT_MODE, masterKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
119 return cipher.doFinal(ciphertext);
120 }
121 catch (Exception e)
122 {
123 throw new KeystoreException("Failed to decrypt database key", e);
124 }
125 }
126
127 private SecretKey getOrCreateMasterKey() throws Exception
128 {
129 KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
130 keyStore.load(null);
131
132 if (keyStore.containsAlias(KEY_ALIAS))
133 {
134 return ((KeyStore.SecretKeyEntry)keyStore.getEntry(KEY_ALIAS, null)).getSecretKey();
135 }
136
137 KeyGenerator keyGen =
138 KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER);
139 keyGen.init(
140 new KeyGenParameterSpec
141 .Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
142 .setKeySize(256)
143 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
144 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
145 .build());
146 return keyGen.generateKey();
147 }
148
149 public static final class KeystoreException extends Exception
150 {
151 public KeystoreException(String message, Throwable cause)
152 {
153 super(message, cause);
154 }
155 }
156}