· 5 years ago · Feb 03, 2020, 07:30 AM
1package org.thoughtcrime.securesms.crypto;
2
3
4import android.os.Build;
5import android.security.keystore.KeyGenParameterSpec;
6import android.security.keystore.KeyProperties;
7import android.util.Base64;
8import android.util.Log;
9
10import androidx.annotation.NonNull;
11import androidx.annotation.RequiresApi;
12
13import com.fasterxml.jackson.annotation.JsonProperty;
14import com.fasterxml.jackson.core.JsonGenerator;
15import com.fasterxml.jackson.core.JsonParser;
16import com.fasterxml.jackson.databind.DeserializationContext;
17import com.fasterxml.jackson.databind.JsonDeserializer;
18import com.fasterxml.jackson.databind.JsonSerializer;
19import com.fasterxml.jackson.databind.SerializerProvider;
20import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
21import com.fasterxml.jackson.databind.annotation.JsonSerialize;
22
23import org.thoughtcrime.securesms.util.JsonUtils;
24
25import java.io.IOException;
26import java.security.InvalidAlgorithmParameterException;
27import java.security.InvalidKeyException;
28import java.security.KeyStore;
29import java.security.KeyStoreException;
30import java.security.NoSuchAlgorithmException;
31import java.security.NoSuchProviderException;
32import java.security.UnrecoverableEntryException;
33import java.security.cert.CertificateException;
34
35import javax.crypto.BadPaddingException;
36import javax.crypto.Cipher;
37import javax.crypto.IllegalBlockSizeException;
38import javax.crypto.KeyGenerator;
39import javax.crypto.NoSuchPaddingException;
40import javax.crypto.SecretKey;
41import javax.crypto.spec.GCMParameterSpec;
42
43public class KeyStoreHelper {
44
45 private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
46 private static final String KEY_ALIAS = "SignalSecret";
47 private static final String TAG = "KeyStoreHelper";
48
49 @RequiresApi(Build.VERSION_CODES.M)
50 public static SealedData seal(@NonNull byte[] input) {
51 SecretKey secretKey = getOrCreateKeyStoreEntry();
52
53 try {
54 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
55 cipher.init(Cipher.ENCRYPT_MODE, secretKey);
56
57 byte[] iv = cipher.getIV();
58 byte[] data = cipher.doFinal(input);
59
60 return new SealedData(iv, data);
61 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
62 throw new AssertionError(e);
63 }
64 }
65
66 @RequiresApi(Build.VERSION_CODES.M)
67 public static byte[] unseal(@NonNull SealedData sealedData) {
68 SecretKey secretKey = getKeyStoreEntry();
69
70 try {
71 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
72 cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
73
74 return cipher.doFinal(sealedData.data);
75 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
76 throw new AssertionError(e);
77 }
78 }
79
80 @RequiresApi(Build.VERSION_CODES.M)
81 private static SecretKey getOrCreateKeyStoreEntry() {
82 if (hasKeyStoreEntry()) return getKeyStoreEntry();
83 else return createKeyStoreEntry();
84 }
85
86 @RequiresApi(Build.VERSION_CODES.M)
87 private static SecretKey createKeyStoreEntry() {
88 Log.d(TAG, "createKeyStoreEntry: ");
89 try {
90 KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
91 KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
92 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
93 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
94 .build();
95 keyGenerator.init(keyGenParameterSpec);
96 SecretKey key = keyGenerator.generateKey();
97 return key;
98 } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
99 throw new AssertionError(e);
100 }
101 }
102
103 @RequiresApi(Build.VERSION_CODES.M)
104 private static SecretKey getKeyStoreEntry() {
105 Log.d(TAG, "getKeyStoreEntry: ");
106 try {
107 KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
108 keyStore.load(null);
109 return ((KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null)).getSecretKey();
110 } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableEntryException e) {
111 if (e instanceof UnrecoverableEntryException) {
112 }
113 throw new AssertionError(e);
114 }
115 }
116
117 @RequiresApi(Build.VERSION_CODES.M)
118 private static boolean hasKeyStoreEntry() {
119 try {
120 KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
121 ks.load(null);
122
123 return ks.containsAlias(KEY_ALIAS) && ks.entryInstanceOf(KEY_ALIAS, KeyStore.SecretKeyEntry.class);
124 } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) {
125 throw new AssertionError(e);
126 }
127 }
128
129 public static class SealedData {
130
131 @SuppressWarnings("unused")
132 private static final String TAG = SealedData.class.getSimpleName();
133
134 @JsonProperty
135 @JsonSerialize(using = ByteArraySerializer.class)
136 @JsonDeserialize(using = ByteArrayDeserializer.class)
137 private byte[] iv;
138
139 @JsonProperty
140 @JsonSerialize(using = ByteArraySerializer.class)
141 @JsonDeserialize(using = ByteArrayDeserializer.class)
142 private byte[] data;
143
144 SealedData(@NonNull byte[] iv, @NonNull byte[] data) {
145 this.iv = iv;
146 this.data = data;
147 }
148
149 @SuppressWarnings("unused")
150 public SealedData() {
151 }
152
153 public String serialize() {
154 try {
155 return JsonUtils.toJson(this);
156 } catch (IOException e) {
157 throw new AssertionError(e);
158 }
159 }
160
161 public static SealedData fromString(@NonNull String value) {
162 try {
163 return JsonUtils.fromJson(value, SealedData.class);
164 } catch (IOException e) {
165 throw new AssertionError(e);
166 }
167 }
168
169 private static class ByteArraySerializer extends JsonSerializer<byte[]> {
170 @Override
171 public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
172 gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING));
173 }
174 }
175
176 private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
177
178 @Override
179 public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
180 return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING);
181 }
182 }
183
184 }
185
186}