Skip to content

Commit a090f24

Browse files
committed
backup: back up randomized icon instead of the actual icon
Even with the upcoming secure backups with minimal metadata, an attacker can infer the backed up package name by matching the icon hash with the hash of the icons extracted from the application of interest. By randomly modifying the MSB part of the colors in the icon, App Manager effectively prevents any partial hash matching attacks. At the same time, it also attempts to preserves visual integrity by selecting a color that blends with its neighbors. Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent 7b562d8 commit a090f24

6 files changed

Lines changed: 210 additions & 3 deletions

File tree

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupOp.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import io.github.muntashirakon.AppManager.ssaid.SsaidSettings;
6161
import io.github.muntashirakon.AppManager.uri.UriManager;
6262
import io.github.muntashirakon.AppManager.utils.ArrayUtils;
63+
import io.github.muntashirakon.AppManager.utils.BitmapRandomizer;
6364
import io.github.muntashirakon.AppManager.utils.ContextUtils;
6465
import io.github.muntashirakon.AppManager.utils.DigestUtils;
6566
import io.github.muntashirakon.AppManager.utils.ExUtils;
@@ -232,9 +233,9 @@ private void backupIcon() {
232233
try {
233234
Path iconFile = mBackupItem.getIconFile();
234235
try (OutputStream outputStream = iconFile.openOutputStream()) {
235-
Bitmap bitmap = UIUtils.getBitmapFromDrawable(mApplicationInfo.loadIcon(mPm));
236+
Bitmap bitmap = UIUtils.getMutableBitmapFromDrawable(mApplicationInfo.loadIcon(mPm));
237+
BitmapRandomizer.randomizePixel(bitmap);
236238
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
237-
outputStream.flush();
238239
}
239240
} catch (IOException e) {
240241
Log.w(TAG, "Could not back up icon.");

app/src/main/java/io/github/muntashirakon/AppManager/crypto/ks/CompatUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ private static SecretKey readKeyApiL(@NonNull SharedPreferences sharedPreference
189189
* Returns the unique SecureRandom instance shared for all local storage encryption operations.
190190
*/
191191
@NonNull
192-
private static SecureRandom getPrng() {
192+
public static SecureRandom getPrng() {
193193
if (sPrng == null) {
194194
sPrng = new SecureRandom();
195195
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
package io.github.muntashirakon.AppManager.utils;
4+
5+
import android.graphics.Bitmap;
6+
import android.graphics.Color;
7+
8+
import androidx.annotation.NonNull;
9+
10+
import io.github.muntashirakon.AppManager.crypto.ks.CompatUtil;
11+
12+
public class BitmapRandomizer {
13+
public static void randomizePixel(@NonNull Bitmap bitmap) {
14+
if (!bitmap.isMutable()) {
15+
throw new IllegalArgumentException("Bitmap must be mutable");
16+
}
17+
18+
int width = bitmap.getWidth();
19+
int height = bitmap.getHeight();
20+
21+
// Randomly select a pixel location
22+
int x = CompatUtil.getPrng().nextInt(width);
23+
int y = CompatUtil.getPrng().nextInt(height);
24+
25+
// Get the original pixel color
26+
int originalColor = bitmap.getPixel(x, y);
27+
28+
// Extract ARGB components
29+
int alpha = Color.alpha(originalColor);
30+
int red = Color.red(originalColor);
31+
int green = Color.green(originalColor);
32+
int blue = Color.blue(originalColor);
33+
34+
// Get neighboring pixels for blending
35+
int[] neighborColors = getNeighborColors(bitmap, x, y);
36+
37+
// Calculate average of neighbors for blending
38+
int avgRed = 0, avgGreen = 0, avgBlue = 0;
39+
for (int neighborColor : neighborColors) {
40+
avgRed += Color.red(neighborColor);
41+
avgGreen += Color.green(neighborColor);
42+
avgBlue += Color.blue(neighborColor);
43+
}
44+
avgRed /= neighborColors.length;
45+
avgGreen /= neighborColors.length;
46+
avgBlue /= neighborColors.length;
47+
48+
// Modify at least one MSB (Most Significant Bit) while blending
49+
int newRed = modifyMsbWithBlending(red, avgRed);
50+
int newGreen = modifyMsbWithBlending(green, avgGreen);
51+
int newBlue = modifyMsbWithBlending(blue, avgBlue);
52+
53+
// Ensure the new color is different from original
54+
int newColor = Color.argb(alpha, newRed, newGreen, newBlue);
55+
if (newColor == originalColor) {
56+
// Force a change by flipping the MSB of red channel
57+
newRed = red ^ 0x80; // Flip bit 7 (MSB)
58+
newColor = Color.argb(alpha, newRed, newGreen, newBlue);
59+
}
60+
61+
// Set the modified pixel
62+
bitmap.setPixel(x, y, newColor);
63+
}
64+
65+
private static int[] getNeighborColors(Bitmap bitmap, int x, int y) {
66+
int width = bitmap.getWidth();
67+
int height = bitmap.getHeight();
68+
69+
// Get up to 8 neighboring pixels
70+
int[] neighbors = new int[8];
71+
int count = 0;
72+
73+
for (int dx = -1; dx <= 1; dx++) {
74+
for (int dy = -1; dy <= 1; dy++) {
75+
if (dx == 0 && dy == 0) continue; // Skip center pixel
76+
77+
int nx = x + dx;
78+
int ny = y + dy;
79+
80+
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
81+
neighbors[count++] = bitmap.getPixel(nx, ny);
82+
}
83+
}
84+
}
85+
86+
// Return only valid neighbors
87+
int[] result = new int[count];
88+
System.arraycopy(neighbors, 0, result, 0, count);
89+
return result;
90+
}
91+
92+
private static int modifyMsbWithBlending(int originalValue, int avgNeighborValue) {
93+
// Blend original with neighbor average (50% blend)
94+
int blended = (originalValue + avgNeighborValue) / 2;
95+
96+
// Ensure MSB modification by flipping a random bit in upper half (bits 4-7)
97+
int bitToFlip = 4 + CompatUtil.getPrng().nextInt(4); // Random bit from 4 to 7
98+
int modified = blended ^ (1 << bitToFlip);
99+
100+
// Clamp to valid range [0, 255]
101+
return Math.max(0, Math.min(255, modified));
102+
}
103+
}

app/src/main/java/io/github/muntashirakon/AppManager/utils/UIUtils.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,20 @@ public static Spannable charSequenceToSpannable(@NonNull CharSequence text) {
352352
} else return sSpannableFactory.newSpannable(text);
353353
}
354354

355+
@AnyThread
356+
@NonNull
357+
public static Bitmap getMutableBitmapFromDrawable(@NonNull Drawable drawable) {
358+
if (drawable instanceof BitmapDrawable) {
359+
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
360+
return bitmap.copy(Bitmap.Config.ARGB_8888, true);
361+
}
362+
final Bitmap bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
363+
final Canvas canvas = new Canvas(bmp);
364+
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
365+
drawable.draw(canvas);
366+
return bmp;
367+
}
368+
355369
@AnyThread
356370
@NonNull
357371
public static Bitmap getBitmapFromDrawable(@NonNull Drawable drawable) {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
package io.github.muntashirakon.AppManager.utils;
4+
5+
import static org.junit.Assert.*;
6+
import android.graphics.Bitmap;
7+
import android.graphics.BitmapFactory;
8+
import android.graphics.Color;
9+
import org.junit.Before;
10+
import org.junit.Test;
11+
import org.junit.runner.RunWith;
12+
import org.robolectric.RobolectricTestRunner;
13+
14+
import java.io.InputStream;
15+
16+
import io.github.muntashirakon.io.Paths;
17+
18+
@RunWith(RobolectricTestRunner.class)
19+
public class BitmapRandomizerTest {
20+
private final ClassLoader classLoader = getClass().getClassLoader();
21+
private Bitmap bitmap;
22+
23+
@Before
24+
public void setUp() throws Exception {
25+
assert classLoader != null;
26+
// Load test_icon.png as mutable Bitmap
27+
try (InputStream is = Paths.get(classLoader.getResource("images/test_icon.png").getFile()).openInputStream()) {
28+
Bitmap original = BitmapFactory.decodeStream(is);
29+
bitmap = original.copy(Bitmap.Config.ARGB_8888, true);
30+
}
31+
32+
assertNotNull(bitmap);
33+
assertTrue(bitmap.isMutable());
34+
}
35+
36+
@Test
37+
public void testRandomizePixelChangesPixelColor() {
38+
int width = bitmap.getWidth();
39+
int height = bitmap.getHeight();
40+
41+
// Store original bitmap copy for pixel color comparison
42+
Bitmap originalCopy = bitmap.copy(Bitmap.Config.ARGB_8888, false);
43+
44+
boolean pixelChanged = false;
45+
boolean msbFlipped = false;
46+
47+
// Run multiple times to test different random pixels
48+
for (int i = 0; i < 10; i++) {
49+
BitmapRandomizer.randomizePixel(bitmap);
50+
51+
// Find pixel locations changed by comparing with originalCopy
52+
for (int x = 0; x < width && !pixelChanged; x++) {
53+
for (int y = 0; y < height && !pixelChanged; y++) {
54+
int origColor = originalCopy.getPixel(x, y);
55+
int modColor = bitmap.getPixel(x, y);
56+
if (origColor != modColor) {
57+
pixelChanged = true;
58+
59+
// Extract RGB channels
60+
int origRed = Color.red(origColor);
61+
int origGreen = Color.green(origColor);
62+
int origBlue = Color.blue(origColor);
63+
64+
int modRed = Color.red(modColor);
65+
int modGreen = Color.green(modColor);
66+
int modBlue = Color.blue(modColor);
67+
68+
// Check for flipped bits in upper half (bits 4 to 7) for any channel
69+
if (checkUpperHalfBitsFlipped(origRed, modRed) ||
70+
checkUpperHalfBitsFlipped(origGreen, modGreen) ||
71+
checkUpperHalfBitsFlipped(origBlue, modBlue)) {
72+
msbFlipped = true;
73+
}
74+
}
75+
}
76+
}
77+
}
78+
79+
assertTrue("At least one pixel color should be changed", pixelChanged);
80+
assertTrue("At least one upper half bit (bits 4-7) should be flipped", msbFlipped);
81+
}
82+
83+
private boolean checkUpperHalfBitsFlipped(int original, int modified) {
84+
// Focus on bits 4-7 mask: 0b11110000 = 0xF0
85+
int originalMasked = original & 0xF0;
86+
int modifiedMasked = modified & 0xF0;
87+
return (originalMasked ^ modifiedMasked) != 0;
88+
}
89+
}
16.3 KB
Loading

0 commit comments

Comments
 (0)