From a492bcbde30af2065470c01257cd46fb44ecce6d Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Sun, 5 Jun 2022 18:26:46 +0200 Subject: [PATCH] Add an option to set a separate password for backups and exports --- .../aegis/AegisBackupAgent.java | 17 ++- .../aegis/ui/dialogs/Dialogs.java | 6 +- .../ImportExportPreferencesFragment.java | 6 +- .../SecurityPreferencesFragment.java | 108 +++++++++++++++++- .../aegis/vault/VaultFile.java | 15 +++ .../aegis/vault/VaultFileCredentials.java | 8 ++ .../aegis/vault/VaultManager.java | 7 +- .../aegis/vault/VaultRepository.java | 13 +-- .../aegis/vault/slots/PasswordSlot.java | 17 ++- .../aegis/vault/slots/Slot.java | 3 +- .../aegis/vault/slots/SlotList.java | 27 +++++ app/src/main/res/values/strings.xml | 5 + app/src/main/res/xml/preferences_security.xml | 17 +++ .../aegis/vault/slots/SlotTest.java | 27 ++++- 14 files changed, 243 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java b/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java index dc7dd002..4d688637 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java +++ b/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java @@ -9,12 +9,16 @@ import android.os.ParcelFileDescriptor; import android.util.Log; import com.beemdevelopment.aegis.util.IOUtils; +import com.beemdevelopment.aegis.vault.VaultFile; import com.beemdevelopment.aegis.vault.VaultRepository; +import com.beemdevelopment.aegis.vault.VaultRepositoryException; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; public class AegisBackupAgent extends BackupAgent { private static final String TAG = AegisBackupAgent.class.getSimpleName(); @@ -46,14 +50,17 @@ public class AegisBackupAgent extends BackupAgent { } // first copy the vault to the files/backup directory - createBackupDir(); File vaultBackupFile = getVaultBackupFile(); - try { - VaultRepository.copyFileTo(this, vaultBackupFile); - } catch (IOException e) { + try (OutputStream outputStream = new FileOutputStream(vaultBackupFile)) { + createBackupDir(); + + VaultFile vaultFile = VaultRepository.readVaultFile(this); + byte[] bytes = vaultFile.exportable().toBytes(); + outputStream.write(bytes); + } catch (VaultRepositoryException | IOException e) { Log.e(TAG, String.format("onFullBackup() failed: %s", e)); deleteBackupDir(); - throw e; + throw new IOException(e); } // then call the original implementation so that fullBackupContent specified in AndroidManifest is read diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java index 435e4dd0..74d8da17 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java @@ -114,7 +114,7 @@ public class Dialogs { .create()); } - public static void showSetPasswordDialog(ComponentActivity activity, Dialogs.SlotListener listener) { + public static void showSetPasswordDialog(ComponentActivity activity, PasswordSlotListener listener) { Zxcvbn zxcvbn = new Zxcvbn(); View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null); EditText textPassword = view.findViewById(R.id.text_password); @@ -460,8 +460,8 @@ public class Dialogs { void onTextInputResult(char[] text); } - public interface SlotListener { - void onSlotResult(Slot slot, Cipher cipher); + public interface PasswordSlotListener { + void onSlotResult(PasswordSlot slot, Cipher cipher); void onException(Exception e); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java index 49877350..0e5a7c6f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java @@ -30,7 +30,7 @@ import com.beemdevelopment.aegis.vault.VaultBackupManager; import com.beemdevelopment.aegis.vault.VaultFileCredentials; import com.beemdevelopment.aegis.vault.VaultRepository; import com.beemdevelopment.aegis.vault.VaultRepositoryException; -import com.beemdevelopment.aegis.vault.slots.Slot; +import com.beemdevelopment.aegis.vault.slots.PasswordSlot; import com.beemdevelopment.aegis.vault.slots.SlotException; import java.io.File; @@ -267,9 +267,9 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { if (_vaultManager.getVault().isEncryptionEnabled()) { cb.exportVault(stream -> _vaultManager.getVault().export(stream)); } else { - Dialogs.showSetPasswordDialog(requireActivity(), new Dialogs.SlotListener() { + Dialogs.showSetPasswordDialog(requireActivity(), new Dialogs.PasswordSlotListener() { @Override - public void onSlotResult(Slot slot, Cipher cipher) { + public void onSlotResult(PasswordSlot slot, Cipher cipher) { VaultFileCredentials creds = new VaultFileCredentials(); try { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/SecurityPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/SecurityPreferencesFragment.java index 70d60b5a..b09f3a24 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/SecurityPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/SecurityPreferencesFragment.java @@ -24,6 +24,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.preferences.SwitchPreference; import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask; import com.beemdevelopment.aegis.vault.VaultFileCredentials; +import com.beemdevelopment.aegis.vault.VaultRepository; import com.beemdevelopment.aegis.vault.VaultRepositoryException; import com.beemdevelopment.aegis.vault.slots.BiometricSlot; import com.beemdevelopment.aegis.vault.slots.PasswordSlot; @@ -33,6 +34,7 @@ import com.beemdevelopment.aegis.vault.slots.SlotList; import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import javax.crypto.Cipher; @@ -43,6 +45,8 @@ public class SecurityPreferencesFragment extends PreferencesFragment { private Preference _setPasswordPreference; private Preference _passwordReminderPreference; private SwitchPreferenceCompat _pinKeyboardPreference; + private SwitchPreference _backupPasswordPreference; + private Preference _backupPasswordChangePreference; @Override public void onResume() { @@ -161,7 +165,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment { Dialogs.showPasswordInputDialog(requireContext(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> { if (isDigitsOnly(new String(password))) { - List slots = _vaultManager.getVault().getCredentials().getSlots().findAll(PasswordSlot.class); + List slots = getRegularPasswordSlots(_vaultManager.getVault().getCredentials().getSlots()); PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password); PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(requireContext(), new PasswordConfirmationListener()); task.execute(getLifecycle(), params); @@ -232,33 +236,88 @@ public class SecurityPreferencesFragment extends PreferencesFragment { Dialogs.showSecureDialog(builder.create()); return false; }); + + _backupPasswordPreference = requirePreference("pref_backup_password"); + _backupPasswordPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if (!isBackupPasswordSet()) { + Dialogs.showSetPasswordDialog(requireActivity(), new SetBackupPasswordListener()); + } else { + SlotList slots = _vaultManager.getVault().getCredentials().getSlots(); + for (Slot slot : getBackupPasswordSlots(slots)) { + slots.remove(slot); + } + + saveAndBackupVault(); + updateEncryptionPreferences(); + } + + return false; + }); + + _backupPasswordChangePreference = requirePreference("pref_backup_password_change"); + _backupPasswordChangePreference.setOnPreferenceClickListener(preference -> { + Dialogs.showSetPasswordDialog(requireActivity(), new SetBackupPasswordListener()); + return false; + }); } private void updateEncryptionPreferences() { boolean encrypted = _vaultManager.getVault().isEncryptionEnabled(); + boolean backupPasswordSet = isBackupPasswordSet(); _encryptionPreference.setChecked(encrypted, true); _setPasswordPreference.setVisible(encrypted); _biometricsPreference.setVisible(encrypted); _autoLockPreference.setVisible(encrypted); _pinKeyboardPreference.setVisible(encrypted); + _backupPasswordPreference.getParent().setVisible(encrypted); + _backupPasswordPreference.setChecked(backupPasswordSet, true); + _backupPasswordChangePreference.setVisible(backupPasswordSet); if (encrypted) { SlotList slots = _vaultManager.getVault().getCredentials().getSlots(); - boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1; + boolean multiBackupPassword = getBackupPasswordSlots(slots).size() > 1; + boolean multiPassword = getRegularPasswordSlots(slots).size() > 1; boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1; boolean canUseBio = BiometricsHelper.isAvailable(requireContext()); _setPasswordPreference.setEnabled(!multiPassword); _biometricsPreference.setEnabled(canUseBio && !multiBio); _biometricsPreference.setChecked(slots.has(BiometricSlot.class), true); _passwordReminderPreference.setVisible(slots.has(BiometricSlot.class)); + _backupPasswordChangePreference.setEnabled(!multiBackupPassword); } else { _setPasswordPreference.setEnabled(false); _biometricsPreference.setEnabled(false); _biometricsPreference.setChecked(false, true); _passwordReminderPreference.setVisible(false); + _backupPasswordChangePreference.setEnabled(false); } } + private boolean isBackupPasswordSet() { + VaultRepository vault = _vaultManager.getVault(); + if (!vault.isEncryptionEnabled()) { + return false; + } + + return getBackupPasswordSlots(vault.getCredentials().getSlots()).size() > 0; + } + + private static List getBackupPasswordSlots(SlotList slots) { + return getPasswordSlots(slots, true); + } + + private static List getRegularPasswordSlots(SlotList slots) { + return getPasswordSlots(slots, false); + } + + private static List getPasswordSlots(SlotList slots, boolean isBackup) { + return slots + .findAll(PasswordSlot.class) + .stream() + .filter(s -> s.isBackup() == isBackup) + .collect(Collectors.toList()); + } + private String getPasswordReminderSummary() { PassReminderFreq freq = _prefs.getPasswordReminderFrequency(); if (freq == PassReminderFreq.NEVER) { @@ -291,9 +350,9 @@ public class SecurityPreferencesFragment extends PreferencesFragment { return getString(R.string.pref_auto_lock_summary, builder.toString()); } - private class SetPasswordListener implements Dialogs.SlotListener { + private class SetPasswordListener implements Dialogs.PasswordSlotListener { @Override - public void onSlotResult(Slot slot, Cipher cipher) { + public void onSlotResult(PasswordSlot slot, Cipher cipher) { VaultFileCredentials creds = _vaultManager.getVault().getCredentials(); SlotList slots = creds.getSlots(); @@ -331,6 +390,43 @@ public class SecurityPreferencesFragment extends PreferencesFragment { } } + private class SetBackupPasswordListener implements Dialogs.PasswordSlotListener { + @Override + public void onSlotResult(PasswordSlot slot, Cipher cipher) { + slot.setIsBackup(true); + + VaultFileCredentials creds = _vaultManager.getVault().getCredentials(); + SlotList slots = creds.getSlots(); + + try { + // encrypt the master key for this slot + slot.setKey(creds.getKey(), cipher); + + // remove the old backup password slot + for (Slot oldSlot : getBackupPasswordSlots(slots)) { + slots.remove(oldSlot); + } + + // add the new backup password slot + slots.add(slot); + } catch (SlotException e) { + onException(e); + return; + } + + _vaultManager.getVault().setCredentials(creds); + saveAndBackupVault(); + updateEncryptionPreferences(); + } + + @Override + public void onException(Exception e) { + e.printStackTrace(); + updateEncryptionPreferences(); + Dialogs.showErrorDialog(requireContext(), R.string.encryption_set_password_error, e); + } + } + private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener { @Override public void onInitializeSlot(BiometricSlot slot, Cipher cipher) { @@ -357,9 +453,9 @@ public class SecurityPreferencesFragment extends PreferencesFragment { } } - private class EnableEncryptionListener implements Dialogs.SlotListener { + private class EnableEncryptionListener implements Dialogs.PasswordSlotListener { @Override - public void onSlotResult(Slot slot, Cipher cipher) { + public void onSlotResult(PasswordSlot slot, Cipher cipher) { VaultFileCredentials creds = new VaultFileCredentials(); try { diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java index ba68f08d..a28ac4ed 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java @@ -117,6 +117,21 @@ public class VaultFile { } } + /** + * Returns a copy of this VaultFile that's suitable for exporting. + * In case there's a backup password slot, any regular password slots are stripped. + */ + public VaultFile exportable() { + if (!isEncrypted()) { + return this; + } + + return new VaultFile(getContent(), new VaultFile.Header( + getHeader().getSlots().exportable(), + getHeader().getParams() + )); + } + public static class Header { private SlotList _slots; private CryptParameters _params; diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFileCredentials.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFileCredentials.java index 1c9bf383..05ff965f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFileCredentials.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFileCredentials.java @@ -37,4 +37,12 @@ public class VaultFileCredentials implements Serializable { public SlotList getSlots() { return _slots; } + + /** + * Returns a copy of these VaultFileCredentials that is suitable for exporting. + * In case there's a backup password slot, any regular password slots are stripped. + */ + public VaultFileCredentials exportable() { + return new VaultFileCredentials(_key, _slots.exportable()); + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java index 8a4fddaf..206c685d 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java @@ -19,7 +19,9 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import java.io.File; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; @@ -198,7 +200,10 @@ public class VaultManager { } File tempFile = File.createTempFile(VaultBackupManager.FILENAME_PREFIX, ".json", dir); - VaultRepository.copyFileTo(_context, tempFile); + try (OutputStream outStream = new FileOutputStream(tempFile)) { + _repo.export(outStream); + } + _backups.scheduleBackup(tempFile, _prefs.getBackupsLocation(), _prefs.getBackupsVersionCount()); } catch (IOException e) { throw new VaultRepositoryException(e); diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java index f888e6ad..74136af3 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultRepository.java @@ -107,13 +107,6 @@ public class VaultRepository { return new VaultRepository(context, vault, creds); } - public static void copyFileTo(Context context, File destFile) throws IOException { - try (InputStream inStream = VaultRepository.getAtomicFile(context).openRead(); - OutputStream outStream = new FileOutputStream(destFile)) { - IOUtils.copy(inStream, outStream); - } - } - void save() throws VaultRepositoryException { try { JSONObject obj = _vault.toJson(); @@ -137,7 +130,7 @@ public class VaultRepository { } /** - * Exports the vault bt serializing it and writing it to the given OutputStream. If encryption + * Exports the vault by serializing it and writing it to the given OutputStream. If encryption * is enabled, the vault will be encrypted automatically. */ public void export(OutputStream stream) throws VaultRepositoryException { @@ -149,6 +142,10 @@ public class VaultRepository { * not null, it will be used to encrypt the vault first. */ public void export(OutputStream stream, VaultFileCredentials creds) throws VaultRepositoryException { + if (creds != null) { + creds = creds.exportable(); + } + try { VaultFile vaultFile = new VaultFile(); if (creds != null) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/slots/PasswordSlot.java b/app/src/main/java/com/beemdevelopment/aegis/vault/slots/PasswordSlot.java index d4b5c54f..16455066 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/slots/PasswordSlot.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/slots/PasswordSlot.java @@ -16,16 +16,19 @@ import javax.crypto.SecretKey; public class PasswordSlot extends RawSlot { private boolean _repaired; + private boolean _isBackup; + private SCryptParameters _params; public PasswordSlot() { super(); } - protected PasswordSlot(UUID uuid, byte[] key, CryptParameters keyParams, SCryptParameters scryptParams, boolean repaired) { + protected PasswordSlot(UUID uuid, byte[] key, CryptParameters keyParams, SCryptParameters scryptParams, boolean repaired, boolean isBackup) { super(uuid, key, keyParams); _params = scryptParams; _repaired = repaired; + _isBackup = isBackup; } @Override @@ -37,6 +40,7 @@ public class PasswordSlot extends RawSlot { obj.put("p", _params.getP()); obj.put("salt", Hex.encode(_params.getSalt())); obj.put("repaired", _repaired); + obj.put("is_backup", _isBackup); return obj; } catch (JSONException e) { throw new RuntimeException(e); @@ -70,8 +74,15 @@ public class PasswordSlot extends RawSlot { return _repaired; } - public SCryptParameters getSCryptParameters() { - return _params; + /** + * Reports whether this slot is a backup password slot. + */ + public boolean isBackup() { + return _isBackup; + } + + public void setIsBackup(boolean isBackup) { + _isBackup = isBackup; } @Override diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/slots/Slot.java b/app/src/main/java/com/beemdevelopment/aegis/vault/slots/Slot.java index bcf05ac5..6abf645d 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/slots/Slot.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/slots/Slot.java @@ -140,7 +140,8 @@ public abstract class Slot extends UUIDMap.Value { Hex.decode(obj.getString("salt")) ); boolean repaired = obj.optBoolean("repaired", false); - slot = new PasswordSlot(uuid, key, keyParams, scryptParams, repaired); + boolean isBackup = obj.optBoolean("is_backup", false); + slot = new PasswordSlot(uuid, key, keyParams, scryptParams, repaired, isBackup); break; case Slot.TYPE_BIOMETRIC: slot = new BiometricSlot(uuid, key, keyParams); diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/slots/SlotList.java b/app/src/main/java/com/beemdevelopment/aegis/vault/slots/SlotList.java index e5b19ace..dbada724 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/slots/SlotList.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/slots/SlotList.java @@ -57,4 +57,31 @@ public class SlotList extends UUIDMap { public boolean has(Class type) { return find(type) != null; } + + /** + * Returns a copy of this SlotList that is suitable for exporting. + * In case there's a backup password slot, any regular password slots are stripped. + */ + public SlotList exportable() { + boolean hasBackupSlots = false; + for (Slot slot : this) { + if (slot instanceof PasswordSlot && ((PasswordSlot) slot).isBackup()) { + hasBackupSlots = true; + break; + } + } + + if (!hasBackupSlots) { + return this; + } + + SlotList slots = new SlotList(); + for (Slot slot : this) { + if (!(slot instanceof PasswordSlot) || ((PasswordSlot) slot).isBackup()) { + slots.add(slot); + } + } + + return slots; + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b14ffee7..c5fc2d4f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,6 +76,11 @@ The back button is pressed The app is minimized The device is locked + Backup & Export + Separate password for backup & export + If enabled, the password that is used to unlock the app can\'t be used to decrypt backups and exports anymore. + Change password for backup & export + Set a new password that\'ll be used to encrypt the vault during backup and export. Encryption Encrypt the vault and unlock it with a password or biometrics Biometric unlock diff --git a/app/src/main/res/xml/preferences_security.xml b/app/src/main/res/xml/preferences_security.xml index 37e68fc0..0e8592ed 100644 --- a/app/src/main/res/xml/preferences_security.xml +++ b/app/src/main/res/xml/preferences_security.xml @@ -33,6 +33,23 @@ android:dependency="pref_biometrics" app:iconSpaceReserved="false"/> + + + + + diff --git a/app/src/test/java/com/beemdevelopment/aegis/vault/slots/SlotTest.java b/app/src/test/java/com/beemdevelopment/aegis/vault/slots/SlotTest.java index 08fc4112..345c8f1d 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/vault/slots/SlotTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/vault/slots/SlotTest.java @@ -1,5 +1,9 @@ package com.beemdevelopment.aegis.vault.slots; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + import com.beemdevelopment.aegis.crypto.CryptoUtils; import com.beemdevelopment.aegis.crypto.MasterKey; import com.beemdevelopment.aegis.crypto.SCryptParameters; @@ -16,9 +20,6 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertThrows; - public class SlotTest { private MasterKey _masterKey; @@ -93,4 +94,24 @@ public class SlotTest { garbledCiphertext[0] = (byte) ~garbledCiphertext[0]; assertThrows(SlotIntegrityException.class, () -> slot.getKey(decryptCipher)); } + + @Test + public void testPasswordSlotExclusion() { + SlotList slots = new SlotList(); + PasswordSlot passSlot = new PasswordSlot(); + PasswordSlot passSlot2 = new PasswordSlot(); + slots.add(passSlot); + slots.add(passSlot2); + + assertArrayEquals(slots.getValues().toArray(), slots.exportable().getValues().toArray()); + + SlotList backupSlots = new SlotList(); + PasswordSlot backupSlot = new PasswordSlot(); + backupSlot.setIsBackup(true); + slots.add(backupSlot); + backupSlots.add(backupSlot); + + assertArrayEquals(backupSlots.getValues().toArray(), slots.exportable().getValues().toArray()); + assertNotEquals(slots.getValues().toArray(), slots.exportable().getValues().toArray()); + } }