mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-14 05:52:52 +00:00
Add a UUID to database entries and slots to make merging databases easy
Also, some other fixes for database exporting
This commit is contained in:
parent
b27edb1b6b
commit
97c57210f1
14 changed files with 74 additions and 55 deletions
|
@ -6,12 +6,12 @@ import org.json.JSONObject;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class Database {
|
public class Database {
|
||||||
private static final int VERSION = 1;
|
private static final int VERSION = 1;
|
||||||
|
|
||||||
private List<DatabaseEntry> _entries = new ArrayList<>();
|
private List<DatabaseEntry> _entries = new ArrayList<>();
|
||||||
private long _counter = 0;
|
|
||||||
|
|
||||||
public JSONObject serialize() throws Exception {
|
public JSONObject serialize() throws Exception {
|
||||||
JSONArray array = new JSONArray();
|
JSONArray array = new JSONArray();
|
||||||
|
@ -26,10 +26,6 @@ public class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deserialize(JSONObject obj) throws Exception {
|
public void deserialize(JSONObject obj) throws Exception {
|
||||||
deserialize(obj, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deserialize(JSONObject obj, boolean incCount) throws Exception {
|
|
||||||
// TODO: support different VERSION deserialization providers
|
// TODO: support different VERSION deserialization providers
|
||||||
int ver = obj.getInt("version");
|
int ver = obj.getInt("version");
|
||||||
if (ver != VERSION) {
|
if (ver != VERSION) {
|
||||||
|
@ -40,30 +36,24 @@ public class Database {
|
||||||
for (int i = 0; i < array.length(); i++) {
|
for (int i = 0; i < array.length(); i++) {
|
||||||
DatabaseEntry entry = new DatabaseEntry(null);
|
DatabaseEntry entry = new DatabaseEntry(null);
|
||||||
entry.deserialize(array.getJSONObject(i));
|
entry.deserialize(array.getJSONObject(i));
|
||||||
|
addKey(entry);
|
||||||
// if incCount is false, don't increment the counter and don't set an ID
|
|
||||||
// this is used by the database importer to prevent an exception down the line
|
|
||||||
// TODO: find a better solution for this ugly hack
|
|
||||||
if (incCount) {
|
|
||||||
addKey(entry);
|
|
||||||
} else {
|
|
||||||
_entries.add(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addKey(DatabaseEntry entry) throws Exception {
|
public void addKey(DatabaseEntry entry) {
|
||||||
entry.setID(++_counter);
|
if (tryGetKeyByUUID(entry.getUUID()) != null) {
|
||||||
|
throw new AssertionError("entry found with the same uuid");
|
||||||
|
}
|
||||||
_entries.add(entry);
|
_entries.add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeKey(DatabaseEntry entry) {
|
public void removeKey(DatabaseEntry entry) {
|
||||||
entry = getKeyByID(entry.getID());
|
entry = getKeyByUUID(entry.getUUID());
|
||||||
_entries.remove(entry);
|
_entries.remove(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void replaceKey(DatabaseEntry newEntry) {
|
public void replaceKey(DatabaseEntry newEntry) {
|
||||||
DatabaseEntry oldEntry = getKeyByID(newEntry.getID());
|
DatabaseEntry oldEntry = getKeyByUUID(newEntry.getUUID());
|
||||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,12 +65,20 @@ public class Database {
|
||||||
return Collections.unmodifiableList(_entries);
|
return Collections.unmodifiableList(_entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DatabaseEntry getKeyByID(long id) {
|
private DatabaseEntry tryGetKeyByUUID(UUID uuid) {
|
||||||
for (DatabaseEntry entry : _entries) {
|
for (DatabaseEntry entry : _entries) {
|
||||||
if (entry.getID() == id) {
|
if (entry.getUUID().equals(uuid)) {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new AssertionError("no entry found with the same id");
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DatabaseEntry getKeyByUUID(UUID uuid) {
|
||||||
|
DatabaseEntry entry = tryGetKeyByUUID(uuid);
|
||||||
|
if (entry == null) {
|
||||||
|
throw new AssertionError("no entry found with the same uuid");
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,13 @@ import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import me.impy.aegis.crypto.KeyInfo;
|
import me.impy.aegis.crypto.KeyInfo;
|
||||||
|
import me.impy.aegis.crypto.KeyInfoException;
|
||||||
|
|
||||||
public class DatabaseEntry implements Serializable {
|
public class DatabaseEntry implements Serializable {
|
||||||
private long _id = -1;
|
private UUID _uuid;
|
||||||
private String _name = "";
|
private String _name = "";
|
||||||
private String _icon = "";
|
private String _icon = "";
|
||||||
private KeyInfo _info;
|
private KeyInfo _info;
|
||||||
|
@ -19,22 +21,30 @@ public class DatabaseEntry implements Serializable {
|
||||||
|
|
||||||
public DatabaseEntry(KeyInfo info) {
|
public DatabaseEntry(KeyInfo info) {
|
||||||
_info = info;
|
_info = info;
|
||||||
|
_uuid = UUID.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
public JSONObject serialize() throws JSONException {
|
public JSONObject serialize() throws JSONException {
|
||||||
JSONObject obj = new JSONObject();
|
JSONObject obj = new JSONObject();
|
||||||
|
obj.put("uuid", _uuid.toString());
|
||||||
obj.put("name", _name);
|
obj.put("name", _name);
|
||||||
obj.put("url", _info.getURL());
|
obj.put("url", _info.getURL());
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deserialize(JSONObject obj) throws Exception {
|
public void deserialize(JSONObject obj) throws JSONException, KeyInfoException {
|
||||||
|
// if there is no uuid, generate a new one
|
||||||
|
if (!obj.has("uuid")) {
|
||||||
|
_uuid = UUID.randomUUID();
|
||||||
|
} else {
|
||||||
|
_uuid = UUID.fromString(obj.getString("uuid"));
|
||||||
|
}
|
||||||
_name = obj.getString("name");
|
_name = obj.getString("name");
|
||||||
_info = KeyInfo.fromURL(obj.getString("url"));
|
_info = KeyInfo.fromURL(obj.getString("url"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getID() {
|
public UUID getUUID() {
|
||||||
return _id;
|
return _uuid;
|
||||||
}
|
}
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return _name;
|
return _name;
|
||||||
|
@ -46,12 +56,6 @@ public class DatabaseEntry implements Serializable {
|
||||||
return _info;
|
return _info;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setID(long id) throws Exception {
|
|
||||||
if (_id != -1) {
|
|
||||||
throw new Exception("this entry has already received an id");
|
|
||||||
}
|
|
||||||
_id = id;
|
|
||||||
}
|
|
||||||
public void setName(String name) {
|
public void setName(String name) {
|
||||||
_name = name;
|
_name = name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,9 +40,11 @@ public class DatabaseFile {
|
||||||
cryptObj.put("tag", Hex.toString(_cryptParameters.Tag));
|
cryptObj.put("tag", Hex.toString(_cryptParameters.Tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// don't write the crypt parameters if the content is not encrypted
|
||||||
|
boolean plain = _content instanceof JSONObject || _slots.isEmpty() || cryptObj == null;
|
||||||
JSONObject headerObj = new JSONObject();
|
JSONObject headerObj = new JSONObject();
|
||||||
headerObj.put("slots", _slots.isEmpty() ? JSONObject.NULL : SlotCollection.serialize(_slots));
|
headerObj.put("slots", plain ? JSONObject.NULL : SlotCollection.serialize(_slots));
|
||||||
headerObj.put("params", cryptObj != null ? cryptObj : JSONObject.NULL);
|
headerObj.put("params", plain ? JSONObject.NULL : cryptObj);
|
||||||
|
|
||||||
JSONObject obj = new JSONObject();
|
JSONObject obj = new JSONObject();
|
||||||
obj.put("version", VERSION);
|
obj.put("version", VERSION);
|
||||||
|
@ -99,6 +101,7 @@ public class DatabaseFile {
|
||||||
|
|
||||||
public void setContent(JSONObject dbObj) {
|
public void setContent(JSONObject dbObj) {
|
||||||
_content = dbObj;
|
_content = dbObj;
|
||||||
|
_cryptParameters = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContent(JSONObject dbObj, MasterKey key)
|
public void setContent(JSONObject dbObj, MasterKey key)
|
||||||
|
|
|
@ -108,10 +108,13 @@ public class DatabaseManager {
|
||||||
|
|
||||||
public String export(boolean encrypt) throws Exception {
|
public String export(boolean encrypt) throws Exception {
|
||||||
assertState(false, true);
|
assertState(false, true);
|
||||||
|
|
||||||
|
DatabaseFile dbFile = new DatabaseFile();
|
||||||
|
dbFile.setSlots(_file.getSlots());
|
||||||
if (encrypt && getFile().isEncrypted()) {
|
if (encrypt && getFile().isEncrypted()) {
|
||||||
_file.setContent(_db.serialize(), _key);
|
dbFile.setContent(_db.serialize(), _key);
|
||||||
} else {
|
} else {
|
||||||
_file.setContent(_db.serialize());
|
dbFile.setContent(_db.serialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
File file;
|
File file;
|
||||||
|
@ -123,7 +126,7 @@ public class DatabaseManager {
|
||||||
throw new IOException("error creating external storage directory");
|
throw new IOException("error creating external storage directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] bytes = _file.serialize();
|
byte[] bytes = dbFile.serialize();
|
||||||
file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
|
file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
|
||||||
stream = new FileOutputStream(file);
|
stream = new FileOutputStream(file);
|
||||||
stream.write(bytes);
|
stream.write(bytes);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import org.json.JSONObject;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
@ -24,13 +25,12 @@ public abstract class Slot implements Serializable {
|
||||||
public final static byte TYPE_RAW = 0x00;
|
public final static byte TYPE_RAW = 0x00;
|
||||||
public final static byte TYPE_DERIVED = 0x01;
|
public final static byte TYPE_DERIVED = 0x01;
|
||||||
public final static byte TYPE_FINGERPRINT = 0x02;
|
public final static byte TYPE_FINGERPRINT = 0x02;
|
||||||
public final static int ID_SIZE = 16;
|
|
||||||
|
|
||||||
protected byte[] _id;
|
protected UUID _uuid;
|
||||||
protected byte[] _encryptedMasterKey;
|
protected byte[] _encryptedMasterKey;
|
||||||
|
|
||||||
protected Slot() {
|
protected Slot() {
|
||||||
_id = CryptoUtils.generateRandomBytes(ID_SIZE);
|
_uuid = UUID.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
// getKey decrypts the encrypted master key in this slot with the given key and returns it.
|
// getKey decrypts the encrypted master key in this slot with the given key and returns it.
|
||||||
|
@ -57,7 +57,7 @@ public abstract class Slot implements Serializable {
|
||||||
public JSONObject serialize() throws JSONException {
|
public JSONObject serialize() throws JSONException {
|
||||||
JSONObject obj = new JSONObject();
|
JSONObject obj = new JSONObject();
|
||||||
obj.put("type", getType());
|
obj.put("type", getType());
|
||||||
obj.put("id", Hex.toString(_id));
|
obj.put("uuid", _uuid.toString());
|
||||||
obj.put("key", Hex.toString(_encryptedMasterKey));
|
obj.put("key", Hex.toString(_encryptedMasterKey));
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
@ -66,12 +66,18 @@ public abstract class Slot implements Serializable {
|
||||||
if (obj.getInt("type") != getType()) {
|
if (obj.getInt("type") != getType()) {
|
||||||
throw new Exception("slot type mismatch");
|
throw new Exception("slot type mismatch");
|
||||||
}
|
}
|
||||||
_id = Hex.toBytes(obj.getString("id"));
|
// if there is no uuid, generate a new one
|
||||||
|
if (!obj.has("uuid")) {
|
||||||
|
_uuid = UUID.randomUUID();
|
||||||
|
} else {
|
||||||
|
_uuid = UUID.fromString(obj.getString("uuid"));
|
||||||
|
}
|
||||||
_encryptedMasterKey = Hex.toBytes(obj.getString("key"));
|
_encryptedMasterKey = Hex.toBytes(obj.getString("key"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract byte getType();
|
public abstract byte getType();
|
||||||
public String getID() {
|
|
||||||
return Hex.toString(_id);
|
public UUID getUUID() {
|
||||||
|
return _uuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,11 @@ public class SlotCollection implements Iterable<Slot>, Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(Slot slot) {
|
public void add(Slot slot) {
|
||||||
|
for (Slot s : this) {
|
||||||
|
if (s.getUUID().equals(slot.getUUID())) {
|
||||||
|
throw new AssertionError("slot found with the same uuid");
|
||||||
|
}
|
||||||
|
}
|
||||||
_slots.add(slot);
|
_slots.add(slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ public class AegisImporter extends DatabaseImporter {
|
||||||
DatabaseFile file = new DatabaseFile();
|
DatabaseFile file = new DatabaseFile();
|
||||||
file.deserialize(bytes);
|
file.deserialize(bytes);
|
||||||
Database db = new Database();
|
Database db = new Database();
|
||||||
db.deserialize(file.getContent(), false);
|
db.deserialize(file.getContent());
|
||||||
return db.getKeys();
|
return db.getKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,8 +79,7 @@ public class FreeOTPImporter extends DatabaseImporter {
|
||||||
byte[] secret = toBytes(obj.getJSONArray("secret"));
|
byte[] secret = toBytes(obj.getJSONArray("secret"));
|
||||||
key.setSecret(secret);
|
key.setSecret(secret);
|
||||||
|
|
||||||
DatabaseEntry profile = new DatabaseEntry(null);
|
DatabaseEntry profile = new DatabaseEntry(key);
|
||||||
profile.setInfo(key);
|
|
||||||
profiles.add(profile);
|
profiles.add(profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||||
try {
|
try {
|
||||||
// find a fingerprint slot with an id that matches an alias in the keystore
|
// find a fingerprint slot with an id that matches an alias in the keystore
|
||||||
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
|
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
|
||||||
String id = slot.getID();
|
String id = slot.getUUID().toString();
|
||||||
KeyStoreHandle handle = new KeyStoreHandle();
|
KeyStoreHandle handle = new KeyStoreHandle();
|
||||||
if (handle.containsKey(id)) {
|
if (handle.containsKey(id)) {
|
||||||
SecretKey key = handle.getKey(id);
|
SecretKey key = handle.getKey(id);
|
||||||
|
|
|
@ -79,7 +79,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
||||||
try {
|
try {
|
||||||
KeyStoreHandle keyStore = new KeyStoreHandle();
|
KeyStoreHandle keyStore = new KeyStoreHandle();
|
||||||
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
|
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
|
||||||
if (keyStore.containsKey(slot.getID()) && FingerprintHelper.getManager(this) != null) {
|
if (keyStore.containsKey(slot.getUUID().toString()) && FingerprintHelper.getManager(this) != null) {
|
||||||
visibility = View.GONE;
|
visibility = View.GONE;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ public class FingerprintDialogFragment extends SlotDialogFragment implements Fin
|
||||||
FingerprintManager manager = FingerprintHelper.getManager(getContext());
|
FingerprintManager manager = FingerprintHelper.getManager(getContext());
|
||||||
try {
|
try {
|
||||||
_slot = new FingerprintSlot();
|
_slot = new FingerprintSlot();
|
||||||
SecretKey key = new KeyStoreHandle().generateKey(_slot.getID());
|
SecretKey key = new KeyStoreHandle().generateKey(_slot.getUUID().toString());
|
||||||
_cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
|
_cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
|
||||||
_helper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
|
_helper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -101,7 +101,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
|
||||||
_storeHandle = new KeyStoreHandle();
|
_storeHandle = new KeyStoreHandle();
|
||||||
_fingerSlot = new FingerprintSlot();
|
_fingerSlot = new FingerprintSlot();
|
||||||
}
|
}
|
||||||
key = _storeHandle.generateKey(_fingerSlot.getID());
|
key = _storeHandle.generateKey(_fingerSlot.getUUID().toString());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new UndeclaredThrowableException(e);
|
throw new UndeclaredThrowableException(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.view.ViewGroup;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import me.impy.aegis.R;
|
import me.impy.aegis.R;
|
||||||
import me.impy.aegis.helpers.ItemTouchHelperAdapter;
|
import me.impy.aegis.helpers.ItemTouchHelperAdapter;
|
||||||
|
@ -37,7 +38,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> im
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeKey(KeyProfile profile) {
|
public void removeKey(KeyProfile profile) {
|
||||||
profile = getKeyByID(profile.getEntry().getID());
|
profile = getKeyByUUID(profile.getEntry().getUUID());
|
||||||
int position = _keyProfiles.indexOf(profile);
|
int position = _keyProfiles.indexOf(profile);
|
||||||
_keyProfiles.remove(position);
|
_keyProfiles.remove(position);
|
||||||
notifyItemRemoved(position);
|
notifyItemRemoved(position);
|
||||||
|
@ -49,7 +50,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> im
|
||||||
}
|
}
|
||||||
|
|
||||||
public void replaceKey(KeyProfile newProfile) {
|
public void replaceKey(KeyProfile newProfile) {
|
||||||
KeyProfile oldProfile = getKeyByID(newProfile.getEntry().getID());
|
KeyProfile oldProfile = getKeyByUUID(newProfile.getEntry().getUUID());
|
||||||
int position = _keyProfiles.indexOf(oldProfile);
|
int position = _keyProfiles.indexOf(oldProfile);
|
||||||
_keyProfiles.set(position, newProfile);
|
_keyProfiles.set(position, newProfile);
|
||||||
notifyItemChanged(position);
|
notifyItemChanged(position);
|
||||||
|
@ -62,9 +63,9 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> im
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeyProfile getKeyByID(long id) {
|
private KeyProfile getKeyByUUID(UUID uuid) {
|
||||||
for (KeyProfile profile : _keyProfiles) {
|
for (KeyProfile profile : _keyProfiles) {
|
||||||
if (profile.getEntry().getID() == id) {
|
if (profile.getEntry().getUUID().equals(uuid)) {
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ public class SlotHolder extends RecyclerView.ViewHolder {
|
||||||
if (FingerprintHelper.isSupported()) {
|
if (FingerprintHelper.isSupported()) {
|
||||||
try {
|
try {
|
||||||
KeyStoreHandle keyStore = new KeyStoreHandle();
|
KeyStoreHandle keyStore = new KeyStoreHandle();
|
||||||
if (keyStore.containsKey(slot.getID())) {
|
if (keyStore.containsKey(slot.getUUID().toString())) {
|
||||||
_slotUsed.setVisibility(View.VISIBLE);
|
_slotUsed.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
} catch (KeyStoreHandleException e) { }
|
} catch (KeyStoreHandleException e) { }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue