Skip to content

Commit f94f766

Browse files
committed
[FM] Add option to view, generate, and verify file checksums from properties
Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent c44ede8 commit f94f766

9 files changed

Lines changed: 498 additions & 81 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import com.topjohnwu.superuser.Shell;
1313

14+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
1415
import org.lsposed.hiddenapibypass.HiddenApiBypass;
1516

1617
import java.security.Security;
@@ -40,7 +41,9 @@ public void onCreate() {
4041
Thread.setDefaultUncaughtExceptionHandler(new AMExceptionHandler(this));
4142
AppearanceUtils.init(this);
4243
TypefaceUtil.replaceFontsWithSystem(this);
44+
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
4345
Security.addProvider(new JavaKeyStoreProvider());
46+
Security.addProvider(new BouncyCastleProvider());
4447
}
4548

4649
@Keep

app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020

2121
import android.Manifest;
2222
import android.app.ActivityManager;
23-
import android.content.ClipData;
24-
import android.content.ClipboardManager;
2523
import android.content.ComponentName;
2624
import android.content.Context;
2725
import android.content.Intent;
@@ -313,35 +311,29 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
313311
showFreezeDialog(freezeTypeN, freezeType != null);
314312
});
315313
mIconView.setOnClickListener(v -> {
316-
ClipboardManager clipboard = (ClipboardManager) ContextUtils.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
317314
ThreadUtils.postOnBackgroundThread(() -> {
318-
ClipData clipData = clipboard.getPrimaryClip();
319-
if (clipData != null && clipData.getItemCount() > 0) {
320-
String data = clipData.getItemAt(0).coerceToText(ContextUtils.getContext()).toString().trim()
321-
.toLowerCase(Locale.ROOT);
322-
if (data.matches("[0-9a-f: \n]+")) {
323-
data = data.replaceAll("[: \n]+", "");
324-
SignerInfo signerInfo = PackageUtils.getSignerInfo(mPackageInfo, mIsExternalApk);
325-
if (signerInfo != null) {
326-
X509Certificate[] certs = signerInfo.getCurrentSignerCerts();
327-
if (certs != null && certs.length == 1) {
328-
try {
329-
Pair<String, String>[] digests = DigestUtils.getDigests(certs[0].getEncoded());
330-
for (Pair<String, String> digest : digests) {
331-
if (digest.second.equals(data)) {
332-
if (digest.first.equals(DigestUtils.MD5) || digest.first.equals(DigestUtils.SHA_1)) {
333-
ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.verified_using_unreliable_hash));
334-
} else
335-
ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.verified));
336-
return;
337-
}
315+
String data = Utils.readHashValueFromClipboard(ContextUtils.getContext());
316+
if (data != null) {
317+
SignerInfo signerInfo = PackageUtils.getSignerInfo(mPackageInfo, mIsExternalApk);
318+
if (signerInfo != null) {
319+
X509Certificate[] certs = signerInfo.getCurrentSignerCerts();
320+
if (certs != null && certs.length == 1) {
321+
try {
322+
Pair<String, String>[] digests = DigestUtils.getDigests(certs[0].getEncoded());
323+
for (Pair<String, String> digest : digests) {
324+
if (digest.second.equals(data)) {
325+
if (digest.first.equals(DigestUtils.MD5) || digest.first.equals(DigestUtils.SHA_1)) {
326+
ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.verified_using_unreliable_hash));
327+
} else
328+
ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.verified));
329+
return;
338330
}
339-
} catch (CertificateEncodingException ignore) {
340331
}
332+
} catch (CertificateEncodingException ignore) {
341333
}
342334
}
343-
ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.not_verified));
344335
}
336+
ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.not_verified));
345337
}
346338
});
347339
});
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
package io.github.muntashirakon.AppManager.fm.dialogs;
4+
5+
import static io.github.muntashirakon.AppManager.utils.UIUtils.displayLongToast;
6+
7+
import android.app.Application;
8+
import android.app.Dialog;
9+
import android.net.Uri;
10+
import android.os.Bundle;
11+
import android.view.LayoutInflater;
12+
import android.view.View;
13+
import android.view.ViewGroup;
14+
15+
import androidx.annotation.NonNull;
16+
import androidx.annotation.Nullable;
17+
import androidx.core.os.BundleCompat;
18+
import androidx.core.util.Pair;
19+
import androidx.fragment.app.DialogFragment;
20+
import androidx.lifecycle.AndroidViewModel;
21+
import androidx.lifecycle.LiveData;
22+
import androidx.lifecycle.MutableLiveData;
23+
import androidx.lifecycle.ViewModelProvider;
24+
import androidx.recyclerview.widget.LinearLayoutManager;
25+
import androidx.recyclerview.widget.RecyclerView;
26+
27+
import com.google.android.material.button.MaterialButton;
28+
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
29+
import com.google.android.material.textfield.TextInputLayout;
30+
31+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
32+
33+
import java.io.InputStream;
34+
import java.security.MessageDigest;
35+
import java.security.Provider;
36+
import java.security.Security;
37+
import java.util.ArrayList;
38+
import java.util.Collections;
39+
import java.util.LinkedHashMap;
40+
import java.util.List;
41+
import java.util.Map;
42+
import java.util.Objects;
43+
import java.util.zip.CRC32;
44+
45+
import aosp.libcore.util.HexEncoding;
46+
import io.github.muntashirakon.AppManager.R;
47+
import io.github.muntashirakon.AppManager.utils.DigestUtils;
48+
import io.github.muntashirakon.AppManager.utils.ThreadUtils;
49+
import io.github.muntashirakon.AppManager.utils.Utils;
50+
import io.github.muntashirakon.adapters.SelectedArrayAdapter;
51+
import io.github.muntashirakon.dialog.DialogTitleBuilder;
52+
import io.github.muntashirakon.io.IoUtils;
53+
import io.github.muntashirakon.io.Path;
54+
import io.github.muntashirakon.io.Paths;
55+
import io.github.muntashirakon.view.TextInputLayoutCompat;
56+
import io.github.muntashirakon.widget.MaterialSpinner;
57+
import io.github.muntashirakon.widget.TextInputTextView;
58+
59+
public class ChecksumsDialogFragment extends DialogFragment {
60+
public static final String TAG = ChecksumsDialogFragment.class.getSimpleName();
61+
62+
private static final String ARG_PATH = "path";
63+
64+
@NonNull
65+
public static ChecksumsDialogFragment getInstance(@NonNull Path path) {
66+
ChecksumsDialogFragment fragment = new ChecksumsDialogFragment();
67+
Bundle args = new Bundle();
68+
args.putParcelable(ARG_PATH, path.getUri());
69+
fragment.setArguments(args);
70+
return fragment;
71+
}
72+
73+
private Path mPath;
74+
private View mDialogView;
75+
private RecyclerView mRecyclerView;
76+
private MaterialSpinner mSpinner;
77+
private MaterialButton mButton;
78+
private TextInputTextView mTextView;
79+
private ChecksumsViewModel mViewModel;
80+
private ChecksumsRecyclerViewAdapter mAdapter;
81+
82+
@NonNull
83+
@Override
84+
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
85+
mViewModel = new ViewModelProvider(this).get(ChecksumsViewModel.class);
86+
mPath = Paths.get(Objects.requireNonNull(BundleCompat.getParcelable(requireArguments(), ARG_PATH, Uri.class)));
87+
mAdapter = new ChecksumsRecyclerViewAdapter();
88+
mDialogView = View.inflate(requireActivity(), R.layout.dialog_checksums, null);
89+
mRecyclerView = mDialogView.findViewById(R.id.recycler_view);
90+
mRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
91+
mRecyclerView.setAdapter(mAdapter);
92+
mSpinner = mDialogView.findViewById(R.id.spinner);
93+
SelectedArrayAdapter<String> algorithmsAdapter = new SelectedArrayAdapter<>(requireContext(),
94+
io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item_small, getAlgorithms());
95+
mSpinner.setAdapter(algorithmsAdapter);
96+
mButton = mDialogView.findViewById(R.id.action);
97+
mButton.setOnClickListener(v -> {
98+
String algorithm = mSpinner.getEditText().getText().toString().trim();
99+
mViewModel.loadChecksum(algorithm, mPath);
100+
});
101+
mTextView = mDialogView.findViewById(R.id.text);
102+
mTextView.setVisibility(View.GONE);
103+
DialogTitleBuilder titleBuilder = new DialogTitleBuilder(requireActivity())
104+
.setTitle(R.string.checksums)
105+
.setSubtitle(mPath.getName())
106+
.setEndIcon(R.drawable.ic_content_paste, v -> {
107+
String data = Utils.readHashValueFromClipboard(v.getContext());
108+
if (data != null) {
109+
for (Map.Entry<String, String> digest : mAdapter.mNameChecksumMap) {
110+
if (digest.getValue().equals(data)) {
111+
if (digest.getValue().equals(DigestUtils.MD5) || digest.getValue().equals(DigestUtils.SHA_1)) {
112+
displayLongToast(R.string.verified_using_unreliable_hash);
113+
} else {
114+
displayLongToast(R.string.verified);
115+
}
116+
return;
117+
}
118+
}
119+
displayLongToast(R.string.not_verified);
120+
}
121+
})
122+
.setEndIconContentDescription(R.string.paste);
123+
return new MaterialAlertDialogBuilder(requireActivity())
124+
.setCustomTitle(titleBuilder.build())
125+
.setView(mDialogView)
126+
.setNegativeButton(R.string.close, null)
127+
.create();
128+
}
129+
130+
@Nullable
131+
@Override
132+
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
133+
return mDialogView;
134+
}
135+
136+
@Override
137+
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
138+
if (mViewModel != null) {
139+
mViewModel.getChecksumsLiveData().observe(getViewLifecycleOwner(), map -> mAdapter.setDefaultList(map));
140+
mViewModel.getChecksumLiveData().observe(getViewLifecycleOwner(), algoHashPair -> {
141+
mTextView.setVisibility(View.VISIBLE);
142+
mTextView.setText(algoHashPair.second);
143+
TextInputLayoutCompat.fromTextInputEditText(mTextView).setHint(algoHashPair.first);
144+
});
145+
mViewModel.loadChecksums(mPath);
146+
}
147+
}
148+
149+
private List<String> getAlgorithms() {
150+
Provider provider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
151+
List<String> algorithms = new ArrayList<>();
152+
for (Provider.Service service : provider.getServices()) {
153+
if ("MessageDigest".equals(service.getType())) {
154+
algorithms.add(service.getAlgorithm());
155+
}
156+
}
157+
return algorithms;
158+
}
159+
160+
private static class ChecksumsRecyclerViewAdapter extends RecyclerView.Adapter<ChecksumsRecyclerViewAdapter.ViewHolder> {
161+
@NonNull
162+
private final List<Map.Entry<String, String>> mNameChecksumMap = new ArrayList<>();
163+
164+
public void setDefaultList(@NonNull Map<String, String> map) {
165+
synchronized (mNameChecksumMap) {
166+
mNameChecksumMap.clear();
167+
mNameChecksumMap.addAll(map.entrySet());
168+
notifyDataSetChanged();
169+
}
170+
}
171+
172+
@NonNull
173+
@Override
174+
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
175+
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_text_input_layout_monospace, parent, false);
176+
return new ViewHolder(view);
177+
}
178+
179+
@Override
180+
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
181+
Map.Entry<String, String> entry;
182+
synchronized (mNameChecksumMap) {
183+
entry = mNameChecksumMap.get(position);
184+
}
185+
holder.input.setHint(entry.getKey());
186+
holder.textView.setText(entry.getValue());
187+
}
188+
189+
@Override
190+
public int getItemCount() {
191+
synchronized (mNameChecksumMap) {
192+
return mNameChecksumMap.size();
193+
}
194+
}
195+
196+
static class ViewHolder extends RecyclerView.ViewHolder {
197+
TextInputLayout input;
198+
TextInputTextView textView;
199+
200+
public ViewHolder(@NonNull View itemView) {
201+
super(itemView);
202+
input = (TextInputLayout) itemView;
203+
textView = (TextInputTextView) Objects.requireNonNull(input.getEditText());
204+
}
205+
}
206+
}
207+
208+
public static class ChecksumsViewModel extends AndroidViewModel {
209+
private final MutableLiveData<Map<String, String>> mChecksumsLiveData = new MutableLiveData<>();
210+
private final MutableLiveData<Pair<String, String>> mChecksumLiveData = new MutableLiveData<>();
211+
212+
public ChecksumsViewModel(@NonNull Application application) {
213+
super(application);
214+
}
215+
216+
public LiveData<Map<String, String>> getChecksumsLiveData() {
217+
return mChecksumsLiveData;
218+
}
219+
220+
public LiveData<Pair<String, String>> getChecksumLiveData() {
221+
return mChecksumLiveData;
222+
}
223+
224+
public void loadChecksum(@NonNull String algo, @NonNull Path path) {
225+
ThreadUtils.postOnBackgroundThread(() -> {
226+
try {
227+
MessageDigest digest = MessageDigest.getInstance(algo, BouncyCastleProvider.PROVIDER_NAME);
228+
try (InputStream is = path.openInputStream()) {
229+
byte[] buffer = new byte[IoUtils.DEFAULT_BUFFER_SIZE];
230+
int length;
231+
while ((length = is.read(buffer)) != -1) {
232+
digest.update(buffer, 0, length);
233+
}
234+
}
235+
mChecksumLiveData.postValue(new Pair<>(algo, HexEncoding.encodeToString(digest.digest(), false)));
236+
} catch (Exception e) {
237+
e.printStackTrace();
238+
mChecksumLiveData.postValue(new Pair<>(null, null));
239+
}
240+
});
241+
}
242+
243+
public void loadChecksums(@NonNull Path path) {
244+
ThreadUtils.postOnBackgroundThread(() -> {
245+
try {
246+
mChecksumsLiveData.postValue(generateChecksums(path));
247+
} catch (Exception e) {
248+
e.printStackTrace();
249+
mChecksumsLiveData.postValue(Collections.emptyMap());
250+
}
251+
});
252+
}
253+
254+
private Map<String, String> generateChecksums(@NonNull Path path) throws Exception {
255+
String[] nativeAlgo = new String[]{"MD5", "SHA-1", "SHA-256", "SHA-512"};
256+
String[] bcAlgo = new String[]{"SHA3-256", "SHA3-512"};
257+
MessageDigest[] messageDigests = new MessageDigest[nativeAlgo.length + bcAlgo.length];
258+
String[] algoNames = new String[]{"MD5 (unreliable)", "SHA-1 (unreliable)", "SHA-256", "SHA-512", "SHA3-256", "SHA3-512"};
259+
for (int i = 0; i < nativeAlgo.length; ++i) {
260+
messageDigests[i] = MessageDigest.getInstance(nativeAlgo[i]);
261+
}
262+
for (int i = 0; i < bcAlgo.length; ++i) {
263+
messageDigests[nativeAlgo.length + i] = MessageDigest.getInstance(bcAlgo[i], BouncyCastleProvider.PROVIDER_NAME);
264+
}
265+
CRC32 crc32 = new CRC32();
266+
try (InputStream is = path.openInputStream()) {
267+
byte[] buffer = new byte[IoUtils.DEFAULT_BUFFER_SIZE];
268+
int length;
269+
while ((length = is.read(buffer)) != -1) {
270+
for (MessageDigest messageDigest : messageDigests) {
271+
messageDigest.update(buffer, 0, length);
272+
}
273+
crc32.update(buffer, 0, length);
274+
}
275+
}
276+
Map<String, String> checksums = new LinkedHashMap<>(messageDigests.length + 1);
277+
checksums.put("CRC32 (insecure)", HexEncoding.encodeToString(DigestUtils.longToBytes(crc32.getValue()), false));
278+
for (int i = 0; i < messageDigests.length; ++i) {
279+
checksums.put(algoNames[i], HexEncoding.encodeToString(messageDigests[i].digest(), false));
280+
}
281+
return checksums;
282+
}
283+
}
284+
}

0 commit comments

Comments
 (0)