From 28c8cfd7624902bbd72cb021fe000e0a8c818aab Mon Sep 17 00:00:00 2001 From: Christopher Tate Date: Mon, 13 Jan 2014 13:48:22 -0800 Subject: [PATCH] Adapt to underlying changes in the PBKDF2 implementation We need to specify "PBKDF2WithHmacSHA1And8bit" now in order to get precisely the same output as was previously generated with "PBKDF2WithHmacSHA1". We also now try both when it's ambiguous which was used to generate the archive checksums. Bug 12494407 Cherry-pick from master. Change-Id: I2d6081dd62f50f7d493045150b327ed120de7abd --- .../android/server/BackupManagerService.java | 254 ++++++++++++------ tests/LegacyRestoreTest/README | 18 ++ .../jbmr2-encrypted-settings-abcd.ab | Bin 0 -> 2229 bytes .../kk-fixed-encrypted-settings-abcd.ab | Bin 0 -> 2293 bytes 4 files changed, 188 insertions(+), 84 deletions(-) create mode 100644 tests/LegacyRestoreTest/README create mode 100644 tests/LegacyRestoreTest/jbmr2-encrypted-settings-abcd.ab create mode 100644 tests/LegacyRestoreTest/kk-fixed-encrypted-settings-abcd.ab diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java index 00bfee78bfbbe..44cb019aff03d 100644 --- a/services/java/com/android/server/BackupManagerService.java +++ b/services/java/com/android/server/BackupManagerService.java @@ -82,7 +82,7 @@ import android.util.StringBuilderPrinter; import com.android.internal.backup.BackupConstants; import com.android.internal.backup.IBackupTransport; import com.android.internal.backup.IObbBackupService; -import com.android.internal.backup.LocalTransport; +import com.android.server.EventLogTags; import com.android.server.PackageManagerBackupAgent.Metadata; import java.io.BufferedInputStream; @@ -140,11 +140,16 @@ class BackupManagerService extends IBackupManager.Stub { private static final boolean DEBUG = true; private static final boolean MORE_DEBUG = false; + // Historical and current algorithm names + static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1"; + static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit"; + // Name and current contents version of the full-backup manifest file static final String BACKUP_MANIFEST_FILENAME = "_manifest"; static final int BACKUP_MANIFEST_VERSION = 1; static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n"; - static final int BACKUP_FILE_VERSION = 1; + static final int BACKUP_FILE_VERSION = 2; + static final int BACKUP_PW_FILE_VERSION = 2; static final boolean COMPRESS_FULL_BACKUPS = true; // should be true in production static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup"; @@ -450,6 +455,8 @@ class BackupManagerService extends IBackupManager.Stub { private final SecureRandom mRng = new SecureRandom(); private String mPasswordHash; private File mPasswordHashFile; + private int mPasswordVersion; + private File mPasswordVersionFile; private byte[] mPasswordSalt; // Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys @@ -810,6 +817,27 @@ class BackupManagerService extends IBackupManager.Stub { } mDataDir = Environment.getDownloadCacheDirectory(); + mPasswordVersion = 1; // unless we hear otherwise + mPasswordVersionFile = new File(mBaseStateDir, "pwversion"); + if (mPasswordVersionFile.exists()) { + FileInputStream fin = null; + DataInputStream in = null; + try { + fin = new FileInputStream(mPasswordVersionFile); + in = new DataInputStream(fin); + mPasswordVersion = in.readInt(); + } catch (IOException e) { + Slog.e(TAG, "Unable to read backup pw version"); + } finally { + try { + if (in != null) in.close(); + if (fin != null) fin.close(); + } catch (IOException e) { + Slog.w(TAG, "Error closing pw version files"); + } + } + } + mPasswordHashFile = new File(mBaseStateDir, "pwhash"); if (mPasswordHashFile.exists()) { FileInputStream fin = null; @@ -1110,13 +1138,13 @@ class BackupManagerService extends IBackupManager.Stub { } } - private SecretKey buildPasswordKey(String pw, byte[] salt, int rounds) { - return buildCharArrayKey(pw.toCharArray(), salt, rounds); + private SecretKey buildPasswordKey(String algorithm, String pw, byte[] salt, int rounds) { + return buildCharArrayKey(algorithm, pw.toCharArray(), salt, rounds); } - private SecretKey buildCharArrayKey(char[] pwArray, byte[] salt, int rounds) { + private SecretKey buildCharArrayKey(String algorithm, char[] pwArray, byte[] salt, int rounds) { try { - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm); KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE); return keyFactory.generateSecret(ks); } catch (InvalidKeySpecException e) { @@ -1127,8 +1155,8 @@ class BackupManagerService extends IBackupManager.Stub { return null; } - private String buildPasswordHash(String pw, byte[] salt, int rounds) { - SecretKey key = buildPasswordKey(pw, salt, rounds); + private String buildPasswordHash(String algorithm, String pw, byte[] salt, int rounds) { + SecretKey key = buildPasswordKey(algorithm, pw, salt, rounds); if (key != null) { return byteArrayToHex(key.getEncoded()); } @@ -1156,13 +1184,13 @@ class BackupManagerService extends IBackupManager.Stub { return result; } - private byte[] makeKeyChecksum(byte[] pwBytes, byte[] salt, int rounds) { + private byte[] makeKeyChecksum(String algorithm, byte[] pwBytes, byte[] salt, int rounds) { char[] mkAsChar = new char[pwBytes.length]; for (int i = 0; i < pwBytes.length; i++) { mkAsChar[i] = (char) pwBytes[i]; } - Key checksum = buildCharArrayKey(mkAsChar, salt, rounds); + Key checksum = buildCharArrayKey(algorithm, mkAsChar, salt, rounds); return checksum.getEncoded(); } @@ -1174,7 +1202,7 @@ class BackupManagerService extends IBackupManager.Stub { } // Backup password management - boolean passwordMatchesSaved(String candidatePw, int rounds) { + boolean passwordMatchesSaved(String algorithm, String candidatePw, int rounds) { // First, on an encrypted device we require matching the device pw final boolean isEncrypted; try { @@ -1217,7 +1245,7 @@ class BackupManagerService extends IBackupManager.Stub { } else { // hash the stated current pw and compare to the stored one if (candidatePw != null && candidatePw.length() > 0) { - String currentPwHash = buildPasswordHash(candidatePw, mPasswordSalt, rounds); + String currentPwHash = buildPasswordHash(algorithm, candidatePw, mPasswordSalt, rounds); if (mPasswordHash.equalsIgnoreCase(currentPwHash)) { // candidate hash matches the stored hash -- the password matches return true; @@ -1232,11 +1260,37 @@ class BackupManagerService extends IBackupManager.Stub { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "setBackupPassword"); - // If the supplied pw doesn't hash to the the saved one, fail - if (!passwordMatchesSaved(currentPw, PBKDF2_HASH_ROUNDS)) { + // When processing v1 passwords we may need to try two different PBKDF2 checksum regimes + final boolean pbkdf2Fallback = (mPasswordVersion < BACKUP_PW_FILE_VERSION); + + // If the supplied pw doesn't hash to the the saved one, fail. The password + // might be caught in the legacy crypto mismatch; verify that too. + if (!passwordMatchesSaved(PBKDF_CURRENT, currentPw, PBKDF2_HASH_ROUNDS) + && !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK, + currentPw, PBKDF2_HASH_ROUNDS))) { return false; } + // Snap up to current on the pw file version + mPasswordVersion = BACKUP_PW_FILE_VERSION; + FileOutputStream pwFout = null; + DataOutputStream pwOut = null; + try { + pwFout = new FileOutputStream(mPasswordVersionFile); + pwOut = new DataOutputStream(pwFout); + pwOut.writeInt(mPasswordVersion); + } catch (IOException e) { + Slog.e(TAG, "Unable to write backup pw version; password not changed"); + return false; + } finally { + try { + if (pwOut != null) pwOut.close(); + if (pwFout != null) pwFout.close(); + } catch (IOException e) { + Slog.w(TAG, "Unable to close pw version record"); + } + } + // Clearing the password is okay if (newPw == null || newPw.isEmpty()) { if (mPasswordHashFile.exists()) { @@ -1254,7 +1308,7 @@ class BackupManagerService extends IBackupManager.Stub { try { // Okay, build the hash of the new backup password byte[] salt = randomBytes(PBKDF2_SALT_SIZE); - String newPwHash = buildPasswordHash(newPw, salt, PBKDF2_HASH_ROUNDS); + String newPwHash = buildPasswordHash(PBKDF_CURRENT, newPw, salt, PBKDF2_HASH_ROUNDS); OutputStream pwf = null, buffer = null; DataOutputStream out = null; @@ -1297,6 +1351,19 @@ class BackupManagerService extends IBackupManager.Stub { } } + private boolean backupPasswordMatches(String currentPw) { + if (hasBackupPassword()) { + final boolean pbkdf2Fallback = (mPasswordVersion < BACKUP_PW_FILE_VERSION); + if (!passwordMatchesSaved(PBKDF_CURRENT, currentPw, PBKDF2_HASH_ROUNDS) + && !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK, + currentPw, PBKDF2_HASH_ROUNDS))) { + if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); + return false; + } + } + return true; + } + // Maintain persistent state around whether need to do an initialize operation. // Must be called with the queue lock held. void recordInitPendingLocked(boolean isPending, String transportName) { @@ -2717,11 +2784,9 @@ class BackupManagerService extends IBackupManager.Stub { // Verify that the given password matches the currently-active // backup password, if any - if (hasBackupPassword()) { - if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { - if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); - return; - } + if (!backupPasswordMatches(mCurrentPassword)) { + if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); + return; } // Write the global file header. All strings are UTF-8 encoded; lines end @@ -2729,7 +2794,7 @@ class BackupManagerService extends IBackupManager.Stub { // final '\n'. // // line 1: "ANDROID BACKUP" - // line 2: backup file format version, currently "1" + // line 2: backup file format version, currently "2" // line 3: compressed? "0" if not compressed, "1" if compressed. // line 4: name of encryption algorithm [currently only "none" or "AES-256"] // @@ -2837,7 +2902,7 @@ class BackupManagerService extends IBackupManager.Stub { OutputStream ofstream) throws Exception { // User key will be used to encrypt the master key. byte[] newUserSalt = randomBytes(PBKDF2_SALT_SIZE); - SecretKey userKey = buildPasswordKey(mEncryptPassword, newUserSalt, + SecretKey userKey = buildPasswordKey(PBKDF_CURRENT, mEncryptPassword, newUserSalt, PBKDF2_HASH_ROUNDS); // the master key is random for each backup @@ -2884,7 +2949,7 @@ class BackupManagerService extends IBackupManager.Stub { // stated number of PBKDF2 rounds IV = c.getIV(); byte[] mk = masterKeySpec.getEncoded(); - byte[] checksum = makeKeyChecksum(masterKeySpec.getEncoded(), + byte[] checksum = makeKeyChecksum(PBKDF_CURRENT, masterKeySpec.getEncoded(), checksumSalt, PBKDF2_HASH_ROUNDS); ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length @@ -3227,11 +3292,9 @@ class BackupManagerService extends IBackupManager.Stub { FileInputStream rawInStream = null; DataInputStream rawDataIn = null; try { - if (hasBackupPassword()) { - if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { - if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); - return; - } + if (!backupPasswordMatches(mCurrentPassword)) { + if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); + return; } mBytes = 0; @@ -3252,8 +3315,12 @@ class BackupManagerService extends IBackupManager.Stub { if (Arrays.equals(magicBytes, streamHeader)) { // okay, header looks good. now parse out the rest of the fields. String s = readHeaderLine(rawInStream); - if (Integer.parseInt(s) == BACKUP_FILE_VERSION) { - // okay, it's a version we recognize + final int archiveVersion = Integer.parseInt(s); + if (archiveVersion <= BACKUP_FILE_VERSION) { + // okay, it's a version we recognize. if it's version 1, we may need + // to try two different PBKDF2 regimes to compare checksums. + final boolean pbkdf2Fallback = (archiveVersion == 1); + s = readHeaderLine(rawInStream); compressed = (Integer.parseInt(s) != 0); s = readHeaderLine(rawInStream); @@ -3261,7 +3328,8 @@ class BackupManagerService extends IBackupManager.Stub { // no more header to parse; we're good to go okay = true; } else if (mDecryptPassword != null && mDecryptPassword.length() > 0) { - preCompressStream = decodeAesHeaderAndInitialize(s, rawInStream); + preCompressStream = decodeAesHeaderAndInitialize(s, pbkdf2Fallback, + rawInStream); if (preCompressStream != null) { okay = true; } @@ -3321,7 +3389,71 @@ class BackupManagerService extends IBackupManager.Stub { return buffer.toString(); } - InputStream decodeAesHeaderAndInitialize(String encryptionName, InputStream rawInStream) { + InputStream attemptMasterKeyDecryption(String algorithm, byte[] userSalt, byte[] ckSalt, + int rounds, String userIvHex, String masterKeyBlobHex, InputStream rawInStream, + boolean doLog) { + InputStream result = null; + + try { + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKey userKey = buildPasswordKey(algorithm, mDecryptPassword, userSalt, + rounds); + byte[] IV = hexToByteArray(userIvHex); + IvParameterSpec ivSpec = new IvParameterSpec(IV); + c.init(Cipher.DECRYPT_MODE, + new SecretKeySpec(userKey.getEncoded(), "AES"), + ivSpec); + byte[] mkCipher = hexToByteArray(masterKeyBlobHex); + byte[] mkBlob = c.doFinal(mkCipher); + + // first, the master key IV + int offset = 0; + int len = mkBlob[offset++]; + IV = Arrays.copyOfRange(mkBlob, offset, offset + len); + offset += len; + // then the master key itself + len = mkBlob[offset++]; + byte[] mk = Arrays.copyOfRange(mkBlob, + offset, offset + len); + offset += len; + // and finally the master key checksum hash + len = mkBlob[offset++]; + byte[] mkChecksum = Arrays.copyOfRange(mkBlob, + offset, offset + len); + + // now validate the decrypted master key against the checksum + byte[] calculatedCk = makeKeyChecksum(algorithm, mk, ckSalt, rounds); + if (Arrays.equals(calculatedCk, mkChecksum)) { + ivSpec = new IvParameterSpec(IV); + c.init(Cipher.DECRYPT_MODE, + new SecretKeySpec(mk, "AES"), + ivSpec); + // Only if all of the above worked properly will 'result' be assigned + result = new CipherInputStream(rawInStream, c); + } else if (doLog) Slog.w(TAG, "Incorrect password"); + } catch (InvalidAlgorithmParameterException e) { + if (doLog) Slog.e(TAG, "Needed parameter spec unavailable!", e); + } catch (BadPaddingException e) { + // This case frequently occurs when the wrong password is used to decrypt + // the master key. Use the identical "incorrect password" log text as is + // used in the checksum failure log in order to avoid providing additional + // information to an attacker. + if (doLog) Slog.w(TAG, "Incorrect password"); + } catch (IllegalBlockSizeException e) { + if (doLog) Slog.w(TAG, "Invalid block size in master key"); + } catch (NoSuchAlgorithmException e) { + if (doLog) Slog.e(TAG, "Needed decryption algorithm unavailable!"); + } catch (NoSuchPaddingException e) { + if (doLog) Slog.e(TAG, "Needed padding mechanism unavailable!"); + } catch (InvalidKeyException e) { + if (doLog) Slog.w(TAG, "Illegal password; aborting"); + } + + return result; + } + + InputStream decodeAesHeaderAndInitialize(String encryptionName, boolean pbkdf2Fallback, + InputStream rawInStream) { InputStream result = null; try { if (encryptionName.equals(ENCRYPTION_ALGORITHM_NAME)) { @@ -3338,59 +3470,13 @@ class BackupManagerService extends IBackupManager.Stub { String masterKeyBlobHex = readHeaderLine(rawInStream); // 9 // decrypt the master key blob - Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); - SecretKey userKey = buildPasswordKey(mDecryptPassword, userSalt, - rounds); - byte[] IV = hexToByteArray(userIvHex); - IvParameterSpec ivSpec = new IvParameterSpec(IV); - c.init(Cipher.DECRYPT_MODE, - new SecretKeySpec(userKey.getEncoded(), "AES"), - ivSpec); - byte[] mkCipher = hexToByteArray(masterKeyBlobHex); - byte[] mkBlob = c.doFinal(mkCipher); - - // first, the master key IV - int offset = 0; - int len = mkBlob[offset++]; - IV = Arrays.copyOfRange(mkBlob, offset, offset + len); - offset += len; - // then the master key itself - len = mkBlob[offset++]; - byte[] mk = Arrays.copyOfRange(mkBlob, - offset, offset + len); - offset += len; - // and finally the master key checksum hash - len = mkBlob[offset++]; - byte[] mkChecksum = Arrays.copyOfRange(mkBlob, - offset, offset + len); - - // now validate the decrypted master key against the checksum - byte[] calculatedCk = makeKeyChecksum(mk, ckSalt, rounds); - if (Arrays.equals(calculatedCk, mkChecksum)) { - ivSpec = new IvParameterSpec(IV); - c.init(Cipher.DECRYPT_MODE, - new SecretKeySpec(mk, "AES"), - ivSpec); - // Only if all of the above worked properly will 'result' be assigned - result = new CipherInputStream(rawInStream, c); - } else Slog.w(TAG, "Incorrect password"); + result = attemptMasterKeyDecryption(PBKDF_CURRENT, userSalt, ckSalt, + rounds, userIvHex, masterKeyBlobHex, rawInStream, false); + if (result == null && pbkdf2Fallback) { + result = attemptMasterKeyDecryption(PBKDF_FALLBACK, userSalt, ckSalt, + rounds, userIvHex, masterKeyBlobHex, rawInStream, true); + } } else Slog.w(TAG, "Unsupported encryption method: " + encryptionName); - } catch (InvalidAlgorithmParameterException e) { - Slog.e(TAG, "Needed parameter spec unavailable!", e); - } catch (BadPaddingException e) { - // This case frequently occurs when the wrong password is used to decrypt - // the master key. Use the identical "incorrect password" log text as is - // used in the checksum failure log in order to avoid providing additional - // information to an attacker. - Slog.w(TAG, "Incorrect password"); - } catch (IllegalBlockSizeException e) { - Slog.w(TAG, "Invalid block size in master key"); - } catch (NoSuchAlgorithmException e) { - Slog.e(TAG, "Needed decryption algorithm unavailable!"); - } catch (NoSuchPaddingException e) { - Slog.e(TAG, "Needed padding mechanism unavailable!"); - } catch (InvalidKeyException e) { - Slog.w(TAG, "Illegal password; aborting"); } catch (NumberFormatException e) { Slog.w(TAG, "Can't parse restore data header"); } catch (IOException e) { diff --git a/tests/LegacyRestoreTest/README b/tests/LegacyRestoreTest/README new file mode 100644 index 0000000000000..cdd157e58295b --- /dev/null +++ b/tests/LegacyRestoreTest/README @@ -0,0 +1,18 @@ +The file "jbmr2-encrypted-settings-abcd.ab" in this directory is an encrypted +"adb backup" archive of the settings provider package. It was generated on a +Nexus 4 running Android 4.3 (API 18), and so predates the Android 4.4 changes +to the PBKDF2 implementation. The archive's encryption password, entered on-screen, +is "abcd" (with no quotation marks). + +'adb restore' decrypts and applies the restored archive successfully on a device +running Android 4.3, but fails to restore correctly on a device running Android 4.4, +reporting an invalid password in logcat. This is the situation reported in bug +. + +The file "kk-fixed-encrypted-settings-abcd.ab" is a similar encrypted "adb backup" +archive, using the same key, generated on a Nexus 4 running Android 4.4 with a fix +to this bug in place. This archive should be successfully restorable on any +version of Android which incorporates the fix. + +These archives can be used as an ongoing test to verify that historical encrypted +archives from various points in Android's history can be successfully restored. diff --git a/tests/LegacyRestoreTest/jbmr2-encrypted-settings-abcd.ab b/tests/LegacyRestoreTest/jbmr2-encrypted-settings-abcd.ab new file mode 100644 index 0000000000000000000000000000000000000000..192dcf5d4c52847bd28c52878b9e0d4983ab61e5 GIT binary patch literal 2229 zcmaKoiCd0a7se^lK$9qMhNzcFd4~6y8sr(9O9`bp5~7qk4T_|ca!TTD5+$8z&_s%g zqc{?gNQyd%Iw(zOLf_*b_}0Gm+Sj$Owbp&FA8F0l*_bgJ6iHib-7E|8hh(?tLI^60 zvMdY(EX)E3OK=24APfou42wYo1(7I16BK~rFoNI&jesJ6MJSSl2)>KrP>62@8C(SL z)d-DH00F>=2!IfdA#j3#S&)Jm7QsoDe?<_)0yw}yBu0xMoMb3kL}9WhMuHe6B3TsY zU=C+!0wZXU#9)L5IUFJY5ibafK#-$J4B-n95idmpEC?Y8hKu-%a}dKpEDT``Logf+ zO62mAArU(&)u_By7VUXYe0;WYQ0+KXE0Xz$U582=8|Aj1yUR-A{-_d3Ibt%3mzl^kR-^E1V>>!7Mj6%T^vVP z903RhL;1NJfb&35ek=ho1kK~2c_A2qWTPL=E7J;`SlhLFJS929@tjwyXZo15zR!$ypT#=W0FG>am{hWA zu=lTEyShh>t80bz#*3T_rXpfWyE?;W|06vtj7^=FJ^Z77U1U{eB+#*SqUdqmJhk1RF(JZTgmBnpznAu$k-h{#u{5?(|6G@ZUcR zv9@iOQrfViu7MMa7NFMz83C>)lDqfQ%O9*fGJ5-`%QYX@go4;sBmAvPNcQ-o^t-)8DXN)38 z%uA(xy4&9C3)Cq});XlYN#}1%ji6RvdsXeSdr=<`H`(Y+U-a4ee(3HSSK`f2O(Q4s zg!_MZmiff>*={P>IRA@K{iHM?Xdl)5u7OmVNW#Ypk_?_TtnsLO(`(|p>!w_$r%`U2 zOwX47Bgz%7aLNY#E}fYVwH`Uj9i{CsBJrXW-JVgVjhgInbUYvI#+O0^@gfJbeTkd z`rS(@gQlbRp^JBzE!*{9c$Hjficar8rer)*i6%GZoNMTK(7SMc`+?_jTF#3N_2fe( zUya4(=TBEj-{R85$*x4g5{LISQX$5HWQ`z#>yNpv&Z_s^K-UAt_fV=U_6ZJj@PRTqkW-UCqRGn;M5LB^)xdQvSQF8z2UZINwym%G?3Q z`jCgT#HZpsXEa;wP_%-kqGj{QH=N{ zwz9P1emT)@R-M6R?!)$GlRZgnokn7fj*QYwUtFMbF?euKvkmy}QTj?JH`(;(vnD$7 z0;ff(_ov6bnt$FkELaU%hxhIuxL^A~g;5X&+iizC4Ki;RpRSyp`&G~HMw89Zl>SRi zt^Om`jvZARu{D%)qP~uIr|`lfIc3wcgJFsma#j5XCALuC)RfNBc+a=NH*|wS1==hxuLPwHmVfL6ePKS3n`hiG$-?2jktHo(K;aS_9e3yz`o+!i|uQ}RA<(E zP)9H9@QV?5X=C$Swjoz$=~a(sYYoTbJY2&Zv6L`5Yuw>5eq+@>je%uKHd#BqhhN!k z8`2_W6wv9e+uT0yuQ;)fb$7gQ4ZOmzH7{dYEkO-Xv5!&w_p$q&r%54u+|!@>AN_Jn z+@js5pIm34N0nMs>D(K*8SkIr_(RZFu(>|)h(j@JeeQlU4g>m;fB*nR$}F! zzxH~Du|jh}Y>P%tgx=bm9N*mTaD7*aZEoEaPo*wgRc8n8k~&rE?{aE< zeBfhSq{s2eicLYnl6TJO8&`aoP@U`NX6|~yZ0w7A>RXdniSoXi F{s$JY#1{Yn literal 0 HcmV?d00001 diff --git a/tests/LegacyRestoreTest/kk-fixed-encrypted-settings-abcd.ab b/tests/LegacyRestoreTest/kk-fixed-encrypted-settings-abcd.ab new file mode 100644 index 0000000000000000000000000000000000000000..bf2b558d49b9910b3a50518a716bbc4a239f0aa7 GIT binary patch literal 2293 zcmWlVc{r5`8^)1I4k2SnQVMm#Y2JP57<$*1gNmuKwNPX$N|bFZA&KhL*v3SJhLRyE zQIr_kOr^0k_QFV(vhN@3PBJJVkpGWFvb#c5Sk}A5W;8}ATW%FX^22E9-$eO zV^NZzIT$7&6lLX_$0?AdDUhHsh=2$N!~lqcC<5X+juOa|0)T)70h(k{h5;}+Hbmeg z1YiV@;Q&Ly6o?C00)PpWgLs(7ah4_s8kfK4coaqunBY(x0a%^^S%@PzIW{4;;UF3z zV4lSQl9fw&R|qLjPf;|-Qu5j`n!o{qL2*)Uhsy0p7Qs1Ej>i!s!T>mfu?&d+KN9HMa^qG?8sLehx*DTrbqL6I=Q$Vo7QhEWWn2@(QPgaHL{ov)ut z^Pa5D9Mb$GGPtdn`$w+G;8xhfrXagH6MwI{-_Jg{bG4%>&AY;)Ps8kVX^xVH>UV|D zZ_>gLY^Y=Q#cn*7ak~8>qGJO5pwd zpAzb_D-MjLx{nscHanlZ`&F+Lud6K^889`WlFY0f)QgsGz5m3*&w6}3I;vR1tEtVg zL8~$_Vd{))@yPz{pZ1-X1|Ge&rmMWV=B0X1KT`!v9!< zzGmjk_YGBJzsw(K-Po9puE4qMH9xen&T7YGsr^II!^dw1mTHJ&e3ttdCfCxs1#9v$e2f-)JBq2}0=+c_4n{f| zLn8qXR!y;W%`v=cNYo3{P=imr*!Ky7Z-z-PsCWewZRl^0)Y`{z)8Nc58l<+0xBNBZYHUG92R zt_%P7o97$)#$ppwZw>FfE(R}LpWHLwr*2u(sg&T>cxa@~uT$sfFTl9S?P_o02~Qxn z)pO#^0hipa__JNRek&MRu4=tSrjU8hzBtqBXnjT-bsV-mneA!mXTkE2j z$aaYW8-I1M`qivO4dn8d3^+TxB&#;TXTQnn;Ff`j{&B*%U-$l@LD|y935}B5g}Ra@ zyPUPtvx?47`s6xK&5ix9(AVnML=6eko3;hN(qiHi48n#kSHa^0x1z^Rq5|9+;pG1 ze5m$L_4FIvH4n7nPup&D%iP+mYn^aaIm#=;^1kh=$73flf7|CF9u@3;T?fOL5UYb9 zV%JC1YBgNxt0|wnW#Y@076ruIxF;@i4Ea&9CcHf=d+cHI+I1T*KAs%Q$-vhyOLRd)cxP_&lWi;Jbh2|s>Z^**6QO%sMn z^25S#)Zom0`VDD+bI92=Cq}%}w{GZh<-FEAZP$N#FDUA|yf9DX)V=FZ#_4WZ+xs3> zbFs>+n-OM9ZE%gW#`%ZZo%1eli>80G@$L!=EqogAvn%y|-UlPS&IZH9Z>#*3j4<^z zOJ6_FEc3D2XG^N+32mv8&{y8B-<0>+j9M0mVhG`_mU4yzVc-rMWwPdR7Owtr!1& zVXjbm?X|npwb4nPXENENL|gNrN6GnoMviN+@NsXRMQ2ul=I#6w(iE>bpZ4ytlH^to z(P3x5H@7w8rMW_1g_K-%dUR!Z3okSoje8ms`Nq@!%dC1te|b!eQ=MA!^=}tjbf_Em ziY~YLXh*%0rSY#8y$c@uwc;N$=^yQ_hxWfX=I6O|S$AZ3s|nI3hBx;ah7XG2*M}QJ zmOl6X8Z`nKc0hzWxa5GA+rl2p%1&vC7W7#xNL|%t)A&)%NhxffqxdE$PND9EJWX3* zwl~yQrG3zGU~H;-IQXOUWNMj9@#U2Gl7_wSQWt(-ui2#${IB;rmF%tON6gn(vz9JBcS&X!P#= jh}uiALv|hOcK