· 7 years ago · Jul 03, 2018, 12:24 PM
1public class DeviceFingerprintManager implements ServiceLocator.IService {
2private static final String TAG = DeviceFingerprintManager.class.getName();
3private static final String KEY_ALIAS = "teamapt-moneytor";
4private static final String FINGERPRINT_KEY_GENERATED = "fingerprintKeyGenerated";
5private static final String FINGERPRINT_PASSWORD_COMPAT = "fingerprintPasswordCompat";
6private static final String FINGERPRINT_KEY_CREATED = "fingerprintKeyCreated";
7private static final String IV_FILE = "moneytor_iv";
8
9private final Context mContext;
10private KeyGenerator mKeyGenerator;
11private KeyStore mKeyStore;
12private Cipher mCipher;
13
14public DeviceFingerprintManager(Context context) {
15mContext = context;
16}
17
18@RequiresApi(api = Build.VERSION_CODES.M)
19public boolean init() {
20try {
21mKeyStore = getKeyStoreInstance();
22mKeyGenerator = getKeyGeneratorInstance();
23mCipher = getCipherInstanceCompat();
24return true;
25} catch (Exception e) {
26e.printStackTrace();
27Log.e(DeviceFingerprintManager.class.getSimpleName(), e.getMessage());
28return false;
29}
30}
31
32public boolean isFingerprintAuthAvailable() {
33
34if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
35return false;
36}
37
38KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
39FingerprintManager fingerprintManager = mContext.getSystemService(FingerprintManager.class);
40
41if (keyguardManager == null || fingerprintManager == null) {
42return false;
43}
44
45if (!keyguardManager.isKeyguardSecure()) {
46// Show a message that the user hasn't set up a fingerprint or lock screen.
47Toast.makeText(mContext,
48"Secure lock screen hasn't set up.\n"
49+ "Go to 'Settings -> Security -> FINGERPRINT' to set up a fingerprint",
50Toast.LENGTH_LONG).show();
51return false;
52}
53
54// Now the protection level of USE_FINGERPRINT permission is normal instead of dangerous.
55// See http://developer.android.com/reference/android/Manifest.permission.html#USE_FINGERPRINT
56// The line below prevents the false positive inspection from Android Studio
57// noinspection ResourceType
58if (!fingerprintManager.hasEnrolledFingerprints()) {
59// This happens when no fingerprints are registered.
60Toast.makeText(mContext,
61"Go to 'Settings -> Security -> FINGERPRINT' and register at least one fingerprint",
62Toast.LENGTH_LONG).show();
63return false;
64}
65
66return true;
67}
68
69private KeyStore getKeyStoreInstance() {
70try {
71return KeyStore.getInstance("AndroidKeyStore");
72} catch (KeyStoreException exception) {
73throw new RuntimeException("Failed to get an instance of KeyStore", exception);
74}
75}
76
77@Deprecated
78private KeyPairGenerator getKeyPairGeneratorInstance() {
79try {
80return KeyPairGenerator.getInstance(/*"AES"*/"RSA", "AndroidKeyStore");
81} catch (NoSuchAlgorithmException | NoSuchProviderException exception) {
82throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception);
83}
84}
85
86@RequiresApi(api = Build.VERSION_CODES.M)
87private KeyGenerator getKeyGeneratorInstance() {
88try {
89return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
90} catch (NoSuchAlgorithmException | NoSuchProviderException exception) {
91throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception);
92}
93}
94
95@RequiresApi(api = Build.VERSION_CODES.M)
96@Deprecated
97private Cipher getCipherInstance() {
98try {
99return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_RSA + "/"
100+ KeyProperties.BLOCK_MODE_ECB + "/"
101+ "OAEPWithSHA-256AndMGF1Padding");
102} catch (NoSuchAlgorithmException | NoSuchPaddingException exception) {
103throw new RuntimeException("Failed to get an instance of Cipher", exception);
104}
105}
106
107@RequiresApi(api = Build.VERSION_CODES.M)
108private Cipher getCipherInstanceCompat() {
109try {
110return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
111+ KeyProperties.BLOCK_MODE_CBC + "/"
112+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
113} catch (NoSuchAlgorithmException | NoSuchPaddingException exception) {
114throw new RuntimeException("Failed to get an instance of Cipher", exception);
115}
116}
117
118@RequiresApi(api = Build.VERSION_CODES.M)
119@RestrictTo(RestrictTo.Scope.LIBRARY)
120public void createKey() {
121if (isKeyCreated()) {
122return;
123}
124
125try {
126//Initialize the KeyGenerator//
127
128KeyGenParameterSpec.Builder builder = new
129
130//Specify the operation(s) this key can be used for//
131KeyGenParameterSpec.Builder(KEY_ALIAS,
132KeyProperties.PURPOSE_ENCRYPT |
133KeyProperties.PURPOSE_DECRYPT)
134.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
135
136//Configure this key so that the user has to confirm their identity with a fingerprint each time they want to use it//
137.setUserAuthenticationRequired(true)
138.setEncryptionPaddings(
139KeyProperties.ENCRYPTION_PADDING_PKCS7);
140
141// This is a workaround to avoid crashes on devices whose API level is < 24
142// because KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment is only
143// visible on API level +24.
144// Ideally there should be a compat library for KeyGenParameterSpec.Builder but
145// which isn't available yet.
146if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
147builder.setInvalidatedByBiometricEnrollment(true);
148}
149mKeyGenerator.init(
150builder.build());
151
152//Generate the key//
153mKeyGenerator.generateKey();
154setKeyCreated();
155} catch (InvalidAlgorithmParameterException exception) {
156exception.printStackTrace();
157Log.e(TAG, "Failed to generate key: " + exception.getMessage());
158}
159}
160
161@Nullable
162@RestrictTo(RestrictTo.Scope.LIBRARY)
163public String createNewFingerprintPassword() {
164
165if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
166return null;
167}
168
169String fingerPrintPassword;
170try {
171fingerPrintPassword = generateFingerprintPassword();
172} catch (NoSuchAlgorithmException e) {
173e.printStackTrace();
174Log.e(TAG, "Unable to create password");
175return null;
176}
177
178return fingerPrintPassword;
179}
180
181@RestrictTo(RestrictTo.Scope.LIBRARY)
182public void savePassword(String fingerPrintPassword) {
183if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
184return;
185}
186
187String encryptedPassword = encrypt(fingerPrintPassword);
188PreferenceManager.getDefaultSharedPreferences(mContext).edit()
189.putString(FINGERPRINT_PASSWORD_COMPAT, encryptedPassword).apply();
190}
191
192public void deleteFingerprintPassword() {
193PreferenceManager.getDefaultSharedPreferences(mContext).edit()
194.remove(FINGERPRINT_PASSWORD_COMPAT).apply();
195}
196
197public String getFingerprintPassword() {
198return PreferenceManager.getDefaultSharedPreferences(mContext).getString(FINGERPRINT_PASSWORD_COMPAT, null);
199}
200
201@SuppressLint("HardwareIds")
202public String getDeviceId() {
203return Settings.Secure.getString(mContext.getContentResolver(),
204Settings.Secure.ANDROID_ID);
205}
206
207public String getDeviceName() {
208String manufacturer = Build.MANUFACTURER;
209String model = Build.MODEL;
210if (model.startsWith(manufacturer)) {
211return model;
212} else {
213return manufacturer + " " + model;
214}
215}
216
217@RequiresApi(api = Build.VERSION_CODES.M)
218private String generateFingerprintPassword() throws NoSuchAlgorithmException {
219String deviceKey = UUID.randomUUID().toString();
220String timeInstance = String.valueOf(Calendar.getInstance().getTimeInMillis());
221String salt = String.valueOf(ThreadLocalRandom.current().nextLong());
222
223String[] passwordCollection = {deviceKey, timeInstance, salt};
224Integer[] order = {0, 1, 2};
225Utils.shuffleArray(order);
226
227StringBuilder password = new StringBuilder();
228for (int anOrder : order) {
229password.append(passwordCollection[anOrder]);
230}
231
232MessageDigest digest = MessageDigest.getInstance("SHA-256");
233byte[] hash = digest.digest(password.toString().getBytes(StandardCharsets.UTF_8));
234return Base64.encodeToString(hash, Base64.NO_WRAP);
235}
236
237private void setKeyCreated() {
238PreferenceManager.getDefaultSharedPreferences(mContext)
239.edit().putBoolean(FINGERPRINT_KEY_CREATED, true).apply();
240}
241
242public boolean isKeyCreated() {
243return PreferenceManager.getDefaultSharedPreferences(mContext).contains(FINGERPRINT_KEY_CREATED);
244}
245
246@RestrictTo(RestrictTo.Scope.LIBRARY)
247public boolean initCipher(int cipherMode) {
248if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
249return false;
250}
251
252if (mKeyStore == null || mCipher == null) {
253Log.e(TAG, "KEYSTORE AND/OR CIPHER NOT INITIALIZED");
254return false;
255}
256
257try {
258mKeyStore.load(null);
259SecretKey key = (SecretKey) mKeyStore.getKey(KEY_ALIAS, null);
260
261IvParameterSpec ivParams;
262byte[] iv;
263if (cipherMode == Cipher.ENCRYPT_MODE) {
264mCipher.init(cipherMode, key);
265ivParams = mCipher.getParameters().getParameterSpec(IvParameterSpec.class);
266iv = ivParams.getIV();
267FileOutputStream fos = mContext.openFileOutput(IV_FILE, Context.MODE_PRIVATE);
268fos.write(iv);
269fos.close();
270} else {
271File file = new File(mContext.getFilesDir() + "/" + IV_FILE);
272int fileSize = (int) file.length();
273iv = new byte[fileSize];
274FileInputStream fis = mContext.openFileInput(IV_FILE);
275//noinspection ResultOfMethodCallIgnored
276fis.read(iv, 0, fileSize);
277fis.close();
278ivParams = new IvParameterSpec(iv);
279mCipher.init(cipherMode, key, ivParams);
280}
281return true;
282} catch (KeyPermanentlyInvalidatedException exception) {
283return false;
284} catch (KeyStoreException | CertificateException | UnrecoverableKeyException
285| IOException | NoSuchAlgorithmException | InvalidKeyException
286| InvalidAlgorithmParameterException | InvalidParameterSpecException exception) {
287throw new RuntimeException("Failed to initialize Cipher", exception);
288}
289}
290
291@Nullable
292private String encrypt(String password) {
293if (mCipher == null) {
294Log.e(TAG, "CIPHER NOT CREATED");
295return null;
296}
297try {
298byte[] bytes = mCipher.doFinal(password.getBytes());
299return Base64.encodeToString(bytes, Base64.NO_WRAP);
300} catch (IllegalBlockSizeException | BadPaddingException exception) {
301throw new RuntimeException("Failed to encrypt password", exception);
302}
303}
304
305@RestrictTo(RestrictTo.Scope.LIBRARY)
306public String decrypt(Cipher cipher, String encoded) {
307if (cipher == null || encoded == null) {
308Log.e(TAG, "CIPHER NOT CREATED OR NULL PASSWORD");
309return null;
310}
311
312try {
313byte[] bytes = Base64.decode(encoded, Base64.NO_WRAP);
314return new String(cipher.doFinal(bytes));
315} catch (IllegalBlockSizeException | BadPaddingException exception) {
316throw new RuntimeException("Failed to decrypt password", exception);
317}
318}
319
320@RestrictTo(RestrictTo.Scope.LIBRARY)
321public Cipher getCipher() {
322return mCipher;
323}
324}