|
| 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