diff --git a/Mifare Classic Tool/app/build.gradle b/Mifare Classic Tool/app/build.gradle index f98555f2..3cf5b7ab 100755 --- a/Mifare Classic Tool/app/build.gradle +++ b/Mifare Classic Tool/app/build.gradle @@ -25,4 +25,5 @@ android { dependencies { implementation 'androidx.legacy:legacy-support-core-utils:1.0.0' + implementation 'com.android.support:design:28.0.0' } diff --git a/Mifare Classic Tool/app/src/main/AndroidManifest.xml b/Mifare Classic Tool/app/src/main/AndroidManifest.xml index e6f3b900..bf578ea7 100644 --- a/Mifare Classic Tool/app/src/main/AndroidManifest.xml +++ b/Mifare Classic Tool/app/src/main/AndroidManifest.xml @@ -113,6 +113,12 @@ android:icon="@drawable/write_tag" android:label="@string/title_activity_write_tag" > + + . + */ + +package de.syss.MifareClassicTool.Activities; + +import android.app.Activity; +import android.content.Intent; + +import androidx.fragment.app.FragmentActivity; + +import de.syss.MifareClassicTool.Common; + +/** + * A FragmentActivity implementing the NFC foreground dispatch system overwriting + * onResume() and onPause(). New Intents will be treated as new Tags. + * @see Common#enableNfcForegroundDispatch(Activity) + * @see Common#disableNfcForegroundDispatch(Activity) + * @see Common#treatAsNewTag(Intent, android.content.Context) + * + */ + +public class BasicFragmentActivity extends FragmentActivity { + + /** + * Enable NFC foreground dispatch system. + * @see Common#disableNfcForegroundDispatch(Activity) + */ + @Override + public void onResume() { + super.onResume(); + Common.setPendingComponentName(this.getComponentName()); + Common.enableNfcForegroundDispatch(this); + } + + /** + * Disable NFC foreground dispatch system. + * @see Common#disableNfcForegroundDispatch(Activity) + */ + @Override + public void onPause() { + Common.disableNfcForegroundDispatch(this); + super.onPause(); + } + + /** + * Handle new Intent as a new tag Intent and if the tag/device does not + * support MIFARE Classic, then run {@link TagInfoTool}. + * @see Common#treatAsNewTag(Intent, android.content.Context) + * @see TagInfoTool + */ + @Override + public void onNewIntent(Intent intent) { + int typeCheck = Common.treatAsNewTag(intent, this); + if (typeCheck == -1 || typeCheck == -2) { + // Device or tag does not support MIFARE Classic. + // Run the only thing that is possible: The tag info tool. + Intent i = new Intent(this, TagInfoTool.class); + startActivity(i); + } + } +} diff --git a/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Activities/MainMenu.java b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Activities/MainMenu.java index ca744358..b8ea0e7f 100644 --- a/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Activities/MainMenu.java +++ b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Activities/MainMenu.java @@ -755,6 +755,18 @@ public void onShowWriteTag(View view) { startActivity(intent); } + /** + * Show the {@link WriteTagFragmentActivity}. + * @param view The View object that triggered the method + * (in this case the write tag button). + * @see WriteTagFragmentActivity + */ + public void onShowWriteTagFragmentActivity(View view) { + Intent intent = new Intent(this, WriteTagFragmentActivity.class); + startActivity(intent); + } + + /** * Show the {@link HelpAndInfo}. * @param view The View object that triggered the method diff --git a/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Activities/WriteTagFragmentActivity.java b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Activities/WriteTagFragmentActivity.java new file mode 100644 index 00000000..a0687593 --- /dev/null +++ b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Activities/WriteTagFragmentActivity.java @@ -0,0 +1,1507 @@ +package de.syss.MifareClassicTool.Activities; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.Toast; +import android.nfc.tech.MifareClassic; +import android.util.SparseArray; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.SimpleAdapter; +import android.widget.TextView; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.tabs.TabLayout; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import de.syss.MifareClassicTool.Common; +import de.syss.MifareClassicTool.MCReader; +import de.syss.MifareClassicTool.R; +import de.syss.MifareClassicTool.Fragments.FactoryFormatFragment; +import de.syss.MifareClassicTool.Fragments.ValueBlockFragment; +import de.syss.MifareClassicTool.Fragments.WriteBlockFragment; +import de.syss.MifareClassicTool.Fragments.WriteDumpFragment; + +/** + * Write data to tag. The user can choose to write + * a single block of data or to write a dump to a tag providing its keys + * or to factory format a tag. + * @author Gerhard Klostermeier + */ + +public class WriteTagFragmentActivity extends BasicFragmentActivity implements WriteBlockFragment.OnWriteBlockListener, WriteDumpFragment.OnWriteDumpListener, FactoryFormatFragment.OnFactoryFormatListener, ValueBlockFragment.OnWriteValueListener { + + /** + * The corresponding Intent will contain a dump. Headers + * (e.g. "Sector: 1") are marked with a "+"-symbol (e.g. "+Sector: 1"). + */ + public final static String EXTRA_DUMP = "de.syss.MifareClassicTool.Activity.DUMP"; + private static final int FC_WRITE_DUMP = 1; + private static final int CKM_WRITE_DUMP = 2; + private static final int CKM_WRITE_BLOCK = 3; + private static final int CKM_FACTORY_FORMAT = 4; + private static final int CKM_WRITE_NEW_VALUE = 5; + private HashMap> mDumpWithPos; + private boolean mWriteDumpFromEditor = false; + private String[] mDumpFromEditor; + private Toolbar toolbar; + private TabLayout tabLayout; + private ViewPager viewPager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_write_tag_fragment_adapter); + + /** + * Set Toolbar (R.id.toolbar) title in layout + * ({@link de.syss.MifareClassicTool.R.layout#activity_write_tag_fragment_adapter}) + */ + + toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar.setTitle(getResources().getString(R.string.title_activity_write_tag)); + + /** + * Setup ViewPager to add ViewPagerAdapter + * {@link #setupViewPager} + * {@link ViewPagerAdapter} + */ + + viewPager = (ViewPager) findViewById(R.id.viewpager); + setupViewPager(viewPager); + + /** + * Set TabLayout (R.id.tabLayout) with ViewPager (R.id.viewPager) in layout + * ({@link de.syss.MifareClassicTool.R.layout#activity_write_tag_fragment_adapter}) + */ + + tabLayout = (TabLayout) findViewById(R.id.tabs); + tabLayout.setupWithViewPager(viewPager); + + + // Restore mDumpWithPos and the "write to manufacturer block"-state. + if (savedInstanceState != null) { + Fragment fragment = getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + if (fragment instanceof WriteDumpFragment){ + ((WriteDumpFragment) fragment).getCheckBoxWriteTagDumpWriteManuf().setChecked( + savedInstanceState.getBoolean("write_manuf_block", false)); + } + Serializable s = savedInstanceState + .getSerializable("dump_with_pos"); + if (s instanceof HashMap) { + mDumpWithPos = (HashMap>) s; + } + } + + Intent i = getIntent(); + if (i.hasExtra(EXTRA_DUMP)) { + // Write dump directly from editor. + mDumpFromEditor = i.getStringArrayExtra(EXTRA_DUMP); + mWriteDumpFromEditor = true; + // Update button text. + Button writeDumpButton = findViewById( + R.id.buttonWriteTagDump); + writeDumpButton.setText(R.string.action_write_dump); + } + + } + + /** + * Add fragments to {@link ViewPagerAdapter} and then set adapter to viewpager + * @param viewPager + * in this case R.id.viewpager in layout + * ({@link de.syss.MifareClassicTool.R.layout#activity_write_tag_fragment_adapter}) + */ + + private void setupViewPager(ViewPager viewPager) { + ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager()); + adapter.addFrag(new WriteBlockFragment(), getString(R.string.action_wirte_block)); + adapter.addFrag(new WriteDumpFragment(), getString(R.string.action_write_dump_clone)); + adapter.addFrag(new FactoryFormatFragment(), getString(R.string.action_factory_format)); + adapter.addFrag(new ValueBlockFragment(), getString(R.string.text_incr_decr_value_block)); + viewPager.setAdapter(adapter); + } + + class ViewPagerAdapter extends FragmentPagerAdapter { + private final List mFragmentList = new ArrayList<>(); + private final List mFragmentTitleList = new ArrayList<>(); + + public ViewPagerAdapter(FragmentManager manager) { + super(manager); + } + + @Override + public Fragment getItem(int position) { + return mFragmentList.get(position); + } + + @Override + public int getCount() { + return mFragmentList.size(); + } + + public void addFrag(Fragment fragment, String title) { + mFragmentList.add(fragment); + mFragmentTitleList.add(title); + } + + @Override + public CharSequence getPageTitle(int position) { + return mFragmentTitleList.get(position); + } + } + + /** + * Fragments are attached to activity and listener (this) + * should be set to each fragment + * @param fragment One of the four fragments in the ViewPagerAdapter + */ + + @Override + public void onAttachFragment(Fragment fragment) { + if (fragment instanceof WriteBlockFragment) { + WriteBlockFragment writeBlockFragment = (WriteBlockFragment) fragment; + writeBlockFragment.setOnWriteBlockListener(this); + } else if (fragment instanceof WriteDumpFragment) { + WriteDumpFragment writeDumpFragment = (WriteDumpFragment) fragment; + writeDumpFragment.setOnWriteBlockListener(this); + } else if (fragment instanceof FactoryFormatFragment) { + FactoryFormatFragment factoryFormatFragment = (FactoryFormatFragment) fragment; + factoryFormatFragment.setOnFactoryFormatListener(this); + } else if (fragment instanceof ValueBlockFragment) { + ValueBlockFragment valueBlockFragment = (ValueBlockFragment) fragment; + valueBlockFragment.setOnWriteValueListener(this); + } + } + + + /** + * Interface method implementation + * {@link WriteBlockFragment.OnWriteBlockListener} + */ + + @Override + public void onWriteBlockButtonClick() { + onWriteBlock(); + } + + /** + * Interface methods implementation + * {@link WriteDumpFragment.OnWriteDumpListener} + */ + + @Override + public void onWriteDumpButtonClick() { + onWriteDump(); + } + + @Override + public void onShowStaticACInfoClick() { + onShowStaticACInfo(); + } + + @Override + public void onShowWriteManufInfoClick() { + onShowWriteManufInfo(); + } + + /** + * Interface method implementation + * {@link FactoryFormatFragment.OnFactoryFormatListener} + */ + + @Override + public void onFactoryFormatButtonClick() { + onFactoryFormat(); + } + + /** + * Interface method implementation + * {@link ValueBlockFragment.OnWriteValueListener} + */ + + @Override + public void onWriteValueButtonClick() { + onWriteValue(); + } + + + /** + * Check the user input and, if necessary, the BCC value + * ({@link #checkBCC(boolean)}). If everythin is O.K., show the + * {@link KeyMapCreator} with predefined mapping range (se + * {@link #createKeyMapForBlock(int, boolean)}). + * After a key map was created, {@link #writeBlock()} will be triggered. + * @see KeyMapCreator + * @see #checkBCC(boolean) + * @see #createKeyMapForBlock(int, boolean) + */ + public void onWriteBlock() { + // Check input. + EditText mSectorTextBlock = null; + EditText mBlockTextBlock = null; + EditText mDataText = null; + Fragment fragment = getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + if (fragment instanceof WriteBlockFragment) { + mSectorTextBlock = ((WriteBlockFragment) fragment).getmSectorTextBlock(); + mBlockTextBlock = ((WriteBlockFragment) fragment).getmBlockTextBlock(); + mDataText = ((WriteBlockFragment) fragment).getmDataText(); + } + if (mSectorTextBlock == null || + mBlockTextBlock == null || + mDataText == null || + !checkSectorAndBlock(mSectorTextBlock, mBlockTextBlock)) { + return; + } + String data = mDataText.getText().toString(); + if (!Common.isHexAnd16Byte(data, this)) { + return; + } + + final int sector = Integer.parseInt( + mSectorTextBlock.getText().toString()); + final int block = Integer.parseInt( + mBlockTextBlock.getText().toString()); + + if (!isSectorInRage(this, true)) { + return; + } + + if (block == 3 || block == 15) { + // Warning. This is a sector trailer. + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_sector_trailer_warning_title) + .setMessage(R.string.dialog_sector_trailer_warning) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(R.string.action_i_know_what_i_am_doing, + (dialog, which) -> { + // Show key map creator. + createKeyMapForBlock(sector, false); + }) + .setNegativeButton(R.string.action_cancel, + (dialog, id) -> { + // Do nothing. + }).show(); + } else if (sector == 0 && block == 0) { + // Is the BCC valid? + int bccCheck = checkBCC(true); + if (bccCheck == 0 || bccCheck > 2) { + // Warning. Writing to manufacturer block. + showWriteManufInfo(true); + } + } else { + createKeyMapForBlock(sector, false); + } + } + + /** + * Open key map creator. + * @see KeyMapCreator + */ + public void onFactoryFormat() { + // Show key map creator. + Intent intent = new Intent(this, KeyMapCreator.class); + intent.putExtra(KeyMapCreator.EXTRA_KEYS_DIR, + Common.getFileFromStorage(Common.HOME_DIR + "/" + + Common.KEYS_DIR).getAbsolutePath()); + intent.putExtra(KeyMapCreator.EXTRA_SECTOR_CHOOSER, false); + intent.putExtra(KeyMapCreator.EXTRA_BUTTON_TEXT, + getString(R.string.action_create_key_map_and_factory_format)); + startActivityForResult(intent, CKM_FACTORY_FORMAT); + } + + + /** + * Check the user input of the sector and the block field. This is a + * helper function for {@link #onWriteBlock()} and + * {@link #onWriteBlock()}. + * @param sector Sector input field. + * @param block Block input field. + * @return True if both values are okay. False otherwise. + */ + private boolean checkSectorAndBlock(EditText sector, EditText block) { + if (sector.getText().toString().equals("") + || block.getText().toString().equals("")) { + // Error, location not fully set. + Toast.makeText(this, R.string.info_data_location_not_set, + Toast.LENGTH_LONG).show(); + return false; + } + int sectorNr = Integer.parseInt(sector.getText().toString()); + int blockNr = Integer.parseInt(block.getText().toString()); + if (sectorNr > KeyMapCreator.MAX_SECTOR_COUNT-1 + || sectorNr < 0) { + // Error, sector is out of range for any MIFARE tag. + Toast.makeText(this, R.string.info_sector_out_of_range, + Toast.LENGTH_LONG).show(); + return false; + } + if (blockNr > KeyMapCreator.MAX_BLOCK_COUNT_PER_SECTOR-1 + || blockNr < 0) { + // Error, block is out of range for any MIFARE tag. + Toast.makeText(this, R.string.info_block_out_of_range, + Toast.LENGTH_LONG).show(); + return false; + } + return true; + } + + /** + * Check if the BCC of the dump ({@link #mDumpWithPos}) or of the block + * ({@link WriteBlockFragment#getmDataText()}) is valid and show a error message if needed. + * This check is only for 4 byte UIDs. + * @param isWriteBlock If Ture, the UID and BCC are taken from + * the {@link WriteBlockFragment#getmDataText()} input field. If False, the UID and BCC + * is taken from the {@link #mDumpWithPos} dump. + * @return
    + *
  • 0 - Everything is O.K.
  • + *
  • 1 - BCC is not valid.
  • + *
  • 2 - There is no tag.
  • + *
  • 3 - UID is not 4 bytes long.
  • + *
  • 4 - Dump does not contain the first sector.
  • + *
  • 5 - Dump does not contain the block 0.
  • + *
+ */ + private int checkBCC(boolean isWriteBlock) { + MCReader reader = Common.checkForTagAndCreateReader(this); + if (reader == null) { + // Error. There is no tag. + return 2; + } + reader.close(); + + int uidLen = Common.getUID().length; + if (uidLen != 4) { + // UID is not 4 bytes long. The BCC does not matter for + // tags with 7 byte UIDs. + return 3; + } + + byte bcc; + byte[] uid; + EditText mDataText = null; + // The length of UID of the dump or the block 0 is expected to match + // the UID length of the current tag. In this case 4 byte. + if (isWriteBlock) { + WriteBlockFragment writeBlockFragment = (WriteBlockFragment)getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + bcc = Common.hexStringToByteArray( + writeBlockFragment.getmDataText().getText().toString() + .substring(8, 10))[0]; + uid = Common.hexStringToByteArray( + writeBlockFragment.getmDataText().getText().toString() + .substring(0, 8)); + } else { + // Has to be called after mDumpWithPos is properly initialized. + // (After checkDumpAndShowSectorChooserDialog().) + HashMap sector0 = mDumpWithPos.get(0); + if (sector0 == null) { + // Error. There is no sector 0 in this dump. Checking the BCC + // is therefore irrelevant. + return 4; + } + byte[] block0 = sector0.get(0); + if (block0 == null) { + // Error. There is no block 0 in sector 0. Checking the BCC is + // therefore irrelevant. + return 5; + } + bcc = block0[4]; + uid = new byte[uidLen]; + System.arraycopy(block0, 0, uid, 0, uidLen); + } + boolean isValidBcc; + try { + isValidBcc = Common.isValidBCC(uid, bcc); + } catch (IllegalArgumentException e) { + // This should never happen, because we already know that the + // length of the UID is 4 byte. + return 3; + } + if (!isValidBcc) { + // Error. BCC is not valid. Show error message. + Toast.makeText(this, R.string.info_bcc_not_valid, + Toast.LENGTH_LONG).show(); + return 1; + } + // Everything was O.K. + return 0; + } + + /** + * Check if the chosen sector or last sector of a dump is in the + * range of valid sectors (according to {@link Preferences}). + * @param context The context in error messages are displayed. + * @return True if the sector is in range, False if not. Also, + * if there was no tag False will be returned. + */ + private boolean isSectorInRage(Context context, boolean isWriteBlock) { + MCReader reader = Common.checkForTagAndCreateReader(this); + if (reader == null) { + return false; + } + int lastValidSector = reader.getSectorCount() - 1; + int lastSector; + reader.close(); + // Initialize last sector. + if (isWriteBlock) { + WriteBlockFragment writeBlockFragment = (WriteBlockFragment)getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + lastSector = Integer.parseInt( + writeBlockFragment.getmSectorTextBlock().getText().toString()); + } else { + lastSector = Collections.max(mDumpWithPos.keySet()); + } + + // Is last sector in range? + if (lastSector > lastValidSector) { + // Error. Tag too small for dump. + Toast.makeText(context, R.string.info_tag_too_small, + Toast.LENGTH_LONG).show(); + reader.close(); + return false; + } + return true; + } + + /** + * Helper function for {@link #onWriteBlock()} and + * {@link #onWriteValue()} to show + * the {@link KeyMapCreator}. + * @param sector The sector for the mapping range of + * {@link KeyMapCreator} + * @param isValueBlock If true, the key map will be created for a Value + * Block ({@link #writeValueBlock()}). + * @see KeyMapCreator + * @see #onWriteBlock() + * @see #onWriteValue() + */ + private void createKeyMapForBlock(int sector, boolean isValueBlock) { + Intent intent = new Intent(this, KeyMapCreator.class); + intent.putExtra(KeyMapCreator.EXTRA_KEYS_DIR, + Common.getFileFromStorage(Common.HOME_DIR + "/" + + Common.KEYS_DIR).getAbsolutePath()); + intent.putExtra(KeyMapCreator.EXTRA_SECTOR_CHOOSER, false); + intent.putExtra(KeyMapCreator.EXTRA_SECTOR_CHOOSER_FROM, sector); + intent.putExtra(KeyMapCreator.EXTRA_SECTOR_CHOOSER_TO, sector); + if (isValueBlock) { + intent.putExtra(KeyMapCreator.EXTRA_BUTTON_TEXT, getString( + R.string.action_create_key_map_and_write_value_block)); + startActivityForResult(intent, CKM_WRITE_NEW_VALUE); + } else { + intent.putExtra(KeyMapCreator.EXTRA_BUTTON_TEXT, getString( + R.string.action_create_key_map_and_write_block)); + startActivityForResult(intent, CKM_WRITE_BLOCK); + } + } + + + /** + * Display information about writing to the manufacturer block and + * optionally create a key map for the first sector. + * @param createKeyMap If true {@link #createKeyMapForBlock(int, boolean)} + * will be triggered the time the user confirms the dialog. + */ + private void showWriteManufInfo(final boolean createKeyMap) { + // Warning. Writing to the manufacturer block is not normal. + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.dialog_block0_writing_title); + dialog.setMessage(R.string.dialog_block0_writing); + dialog.setIcon(android.R.drawable.ic_dialog_info); + + int buttonID = R.string.action_ok; + if (createKeyMap) { + buttonID = R.string.action_i_know_what_i_am_doing; + dialog.setNegativeButton(R.string.action_cancel, + (dialog12, which) -> { + // Do nothing. + }); + } + dialog.setPositiveButton(buttonID, + (dialog1, which) -> { + // Do nothing or create a key map. + if (createKeyMap) { + createKeyMapForBlock(0, false); + } + }); + dialog.show(); + } + + + + /** + * Handle incoming results from {@link KeyMapCreator} or + * {@link FileChooser}. + * @see #writeBlock() + * @see #checkTag() + * @see #checkDumpAndShowSectorChooserDialog(String[]) + * @see #createFactoryFormattedDump() + * @see #writeValueBlock() + */ + @Override + public void onActivityResult(int requestCode, + int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + int ckmError = -1; + + switch(requestCode) { + case FC_WRITE_DUMP: + if (resultCode == Activity.RESULT_OK) { + // Read dump and create keys. + readDumpFromFile(data.getStringExtra( + FileChooser.EXTRA_CHOSEN_FILE)); + } + break; + case CKM_WRITE_DUMP: + if (resultCode != Activity.RESULT_OK) { + // Error. + ckmError = resultCode; + } else { + checkTag(); + } + break; + case CKM_FACTORY_FORMAT: + if (resultCode != Activity.RESULT_OK) { + // Error. + ckmError = resultCode; + } else { + createFactoryFormattedDump(); + } + break; + case CKM_WRITE_BLOCK: + if (resultCode != Activity.RESULT_OK) { + // Error. + ckmError = resultCode; + } else { + // Write block. + writeBlock(); + } + break; + case CKM_WRITE_NEW_VALUE: + if (resultCode != Activity.RESULT_OK) { + // Error. + ckmError = resultCode; + } else { + // Write block. + writeValueBlock(); + } + break; + + } + + // Error handling for the return value of KeyMapCreator. + // So far, only error nr. 4 needs to be handled. + switch (ckmError) { + case 4: + // Error. Path from the calling intend was null. + // (This is really strange and should not occur.) + Toast.makeText(this, R.string.info_strange_error, + Toast.LENGTH_LONG).show(); + } + } + + + /** + * Called from {@link #onActivityResult(int, int, Intent)} + * after a key map was created, this method tries to write the given + * data to the tag. Possible errors are displayed to the user via Toast. + * @see #onActivityResult(int, int, Intent) + * @see #onWriteBlock() + */ + private void writeBlock() { + MCReader reader = Common.checkForTagAndCreateReader(this); + if (reader == null) { + return; + } + WriteBlockFragment writeBlockFragment = (WriteBlockFragment)getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + int sector = Integer.parseInt(writeBlockFragment.getmSectorTextBlock().getText().toString()); + int block = Integer.parseInt(writeBlockFragment.getmBlockTextBlock().getText().toString()); + byte[][] keys = Common.getKeyMap().get(sector); + int result = -1; + + if (keys[1] != null) { + result = reader.writeBlock(sector, block, + Common.hexStringToByteArray(writeBlockFragment.getmDataText().getText().toString()), + keys[1], true); + } + // Error while writing? Maybe tag has default factory settings -> + // try to write with key a (if there is one). + if (result == -1 && keys[0] != null) { + result = reader.writeBlock(sector, block, + Common.hexStringToByteArray(writeBlockFragment.getmDataText().getText().toString()), + keys[0], false); + } + reader.close(); + + // Error handling. + switch (result) { + case 2: + Toast.makeText(this, R.string.info_block_not_in_sector, + Toast.LENGTH_LONG).show(); + return; + case -1: + Toast.makeText(this, R.string.info_error_writing_block, + Toast.LENGTH_LONG).show(); + return; + } + Toast.makeText(this, R.string.info_write_successful, + Toast.LENGTH_LONG).show(); + finish(); + } + + /** + * Read the dump (skipping all blocks with unknown data "-") and + * call {@link #checkDumpAndShowSectorChooserDialog(String[])}. + * @param pathToDump path and filename of the dump + * (selected by {@link FileChooser}). + * @see #checkDumpAndShowSectorChooserDialog(String[]) + */ + private void readDumpFromFile(String pathToDump) { + // Read dump. + File file = new File(pathToDump); + String[] dump = Common.readFileLineByLine(file, false, this); + checkDumpAndShowSectorChooserDialog(dump); + } + + /** + * Triggered after the dump was selected (by {@link FileChooser}) + * and read (by {@link #readDumpFromFile(String)}), this method saves + * the data including its position in {@link #mDumpWithPos}. + * If the "use static Access Condition" option is enabled, all the ACs + * will be replaced by the static ones. Also, the BCC value is + * check if necessary ({@link #checkBCC(boolean)}). After all this it + * will show a dialog in which the user can choose the sectors he wants + * to write. When the sectors are chosen, this method calls + * {@link #createKeyMapForDump()} to create a key map for the present tag. + * @param dump Dump selected by {@link FileChooser} or directly + * from the {@link DumpEditor} (via an Intent with{@link #EXTRA_DUMP})). + * @see KeyMapCreator + * @see #createKeyMapForDump() + * @see #checkBCC(boolean) + */ + private void checkDumpAndShowSectorChooserDialog(final String[] dump) { + int err = Common.isValidDump(dump, false); + if (err != 0) { + // Error. + Common.isValidDumpErrorToast(err, this); + return; + } + + initDumpWithPosFromDump(dump); + + // Create and show sector chooser dialog + // (let the user select the sectors which will be written). + View dialogLayout = getLayoutInflater().inflate( + R.layout.dialog_write_sectors, + findViewById(android.R.id.content), false); + LinearLayout llCheckBoxes = dialogLayout.findViewById( + R.id.linearLayoutWriteSectorsCheckBoxes); + Button selectAll = dialogLayout.findViewById( + R.id.buttonWriteSectorsSelectAll); + Button selectNone = dialogLayout.findViewById( + R.id.buttonWriteSectorsSelectNone); + Integer[] sectors = mDumpWithPos.keySet().toArray( + new Integer[mDumpWithPos.size()]); + Arrays.sort(sectors); + final Context context = this; + final CheckBox[] sectorBoxes = new CheckBox[mDumpWithPos.size()]; + for (int i = 0; i< sectors.length; i++) { + sectorBoxes[i] = new CheckBox(this); + sectorBoxes[i].setChecked(true); + sectorBoxes[i].setTag(sectors[i]); + sectorBoxes[i].setText(getString(R.string.text_sector) + + " " + sectors[i]); + llCheckBoxes.addView(sectorBoxes[i]); + } + View.OnClickListener listener = v -> { + String tag = v.getTag().toString(); + for (CheckBox box : sectorBoxes) { + box.setChecked(tag.equals("all")); + } + }; + selectAll.setOnClickListener(listener); + selectNone.setOnClickListener(listener); + + final AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle(R.string.dialog_write_sectors_title) + .setIcon(android.R.drawable.ic_menu_edit) + .setView(dialogLayout) + .setPositiveButton(R.string.action_ok, + (dialog12, which) -> { + // Do nothing here because we override this button later + // to change the close behaviour. However, we still need + // this because on older versions of Android unless we + // pass a handler the button doesn't get instantiated + }) + .setNegativeButton(R.string.action_cancel, + (dialog1, which) -> { + // Do nothing. + }) + .create(); + dialog.show(); + final Context con = this; + + // Override/define behavior for positive button click. + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener( + v -> { + // Re-Init mDumpWithPos in order to remove unwanted sectors. + initDumpWithPosFromDump(dump); + boolean writeBlock0 = false; + Fragment fragment = getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + CheckBox mWriteManufBlock = null; + if (fragment instanceof WriteDumpFragment){ + mWriteManufBlock = ((WriteDumpFragment) fragment).getCheckBoxWriteTagDumpWriteManuf(); + } + for (CheckBox box : sectorBoxes) { + int sector = Integer.parseInt(box.getTag().toString()); + if (!box.isChecked()) { + mDumpWithPos.remove(sector); + } else if (sector == 0 && box.isChecked() + && mWriteManufBlock != null + && mWriteManufBlock.isChecked()) { + writeBlock0 = true; + } + } + if (mDumpWithPos.size() == 0) { + // Error. There is nothing to write. + Toast.makeText(context, R.string.info_nothing_to_write, + Toast.LENGTH_LONG).show(); + return; + } + + // Check if last sector is out of range. + if (!isSectorInRage(con, false)) { + return; + } + + // Do a BCC check if sector 0 is chosen and writing to + // the manufacturer block was enabled. + if (writeBlock0) { + int bccCheck = checkBCC(false); + if (bccCheck == 2) { + // Error. Redo. + return; + } else if (bccCheck == 1) { + // Error in BCC. Exit. + dialog.dismiss(); + return; + } + } + // Create key map. + createKeyMapForDump(); + dialog.dismiss(); + }); + } + + /** + * Regular behavior: Check input, open a file chooser ({@link FileChooser}) + * to select a dump and wait for its result in + * {@link #onActivityResult(int, int, Intent)}. + * This method triggers the call chain: open {@link FileChooser} + * (this method) -> read dump ({@link #readDumpFromFile(String)}) + * -> check dump ({@link #checkDumpAndShowSectorChooserDialog(String[])}) -> + * open {@link KeyMapCreator} ({@link #createKeyMapForDump()}) + * -> run {@link #checkTag()} -> run + * {@link #writeDump(HashMap, SparseArray)}.
+ * Behavior if the dump is already there (from the {@link DumpEditor}): + * The same as before except the call chain will directly start from + * {@link #checkDumpAndShowSectorChooserDialog(String[])}.
+ * (The static Access Conditions will be checked in any case, if the + * option is enabled.) + * @see FileChooser + * @see #onActivityResult(int, int, Intent) + */ + public void onWriteDump() { + // Check the static Access Condition option. + Fragment fragment = getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + if (fragment instanceof WriteDumpFragment){ + if (((WriteDumpFragment) fragment).getCheckBoxWriteTagDumpStaticAC().isChecked()) { + String ac = ((WriteDumpFragment) fragment).getEditTextWriteTagDumpStaticAC().getText().toString(); + if (!ac.matches("[0-9A-Fa-f]+")) { + // Error, not hex. + Toast.makeText(this, R.string.info_ac_not_hex, + Toast.LENGTH_LONG).show(); + return; + } + if (ac.length() != 6) { + // Error, not 3 byte (6 chars). + Toast.makeText(this, R.string.info_ac_not_3_byte, + Toast.LENGTH_LONG).show(); + return; + } + } + } + + if (mWriteDumpFromEditor) { + // Write dump directly from the dump editor. + // (Dump has already been chosen.) + checkDumpAndShowSectorChooserDialog(mDumpFromEditor); + } else { + // Show file chooser (chose dump). + Intent intent = new Intent(this, FileChooser.class); + intent.putExtra(FileChooser.EXTRA_DIR, + Common.getFileFromStorage(Common.HOME_DIR + "/" + + Common.DUMPS_DIR).getAbsolutePath()); + intent.putExtra(FileChooser.EXTRA_TITLE, + getString(R.string.text_open_dump_title)); + intent.putExtra(FileChooser.EXTRA_CHOOSER_TEXT, + getString(R.string.text_choose_dump_to_write)); + intent.putExtra(FileChooser.EXTRA_BUTTON_TEXT, + getString(R.string.action_write_full_dump)); + startActivityForResult(intent, FC_WRITE_DUMP); + } + } + + /** + * Initialize {@link #mDumpWithPos} with the data from a dump. + * Transform the simple dump array into a structure (mDumpWithPos) + * where the sector and block information are known additionally. + * Blocks containing unknown data ("-") are dropped. + * @param dump The dump to initialize the mDumpWithPos with. + */ + private void initDumpWithPosFromDump(String[] dump) { + mDumpWithPos = new HashMap<>(); + int sector = 0; + int block = 0; + // Transform the simple dump array into a structure (mDumpWithPos) + // where the sector and block information are known additionally. + // Blocks containing unknown data ("-") are dropped. + for (int i = 0; i < dump.length; i++) { + if (dump[i].startsWith("+")) { + String[] tmp = dump[i].split(": "); + sector = Integer.parseInt(tmp[tmp.length-1]); + block = 0; + mDumpWithPos.put(sector, new HashMap<>()); + } else if (!dump[i].contains("-")) { + // Use static Access Conditions for all sectors? + CheckBox mEnableStaticAC = null; + EditText mStaticAC = null; + Fragment fragment = getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + if (fragment instanceof WriteDumpFragment){ + mEnableStaticAC = ((WriteDumpFragment) fragment).getCheckBoxWriteTagDumpStaticAC(); + mStaticAC = ((WriteDumpFragment) fragment).getEditTextWriteTagDumpStaticAC(); + } + if (mEnableStaticAC != null && mEnableStaticAC.isChecked() + && (i+1 == dump.length || dump[i+1].startsWith("+")) + && mStaticAC != null) { + // This is a Sector Trailer. Replace its ACs + // with the static ones. + String newBlock = dump[i].substring(0, 12) + + mStaticAC.getText().toString() + + dump[i].substring(18, dump[i].length()); + dump[i] = newBlock; + } + mDumpWithPos.get(sector).put(block++, + Common.hexStringToByteArray(dump[i])); + } else { + block++; + } + } + } + + /** + * Check if the tag is suitable for the dump ({@link #mDumpWithPos}). + * This is done in three steps. The first check determines if the dump + * fits on the tag (size check). The second check determines if the keys for + * relevant sectors are known (key check). At last this method will check + * whether the keys with write privileges are known and if some blocks + * are read-only (write check).
+ * If some of these checks "fail", the user will get a report dialog + * with the two options to cancel the whole write process or to + * write as much as possible(call {@link #writeDump(HashMap, + * SparseArray)}). + * @see MCReader#isWritableOnPositions(HashMap, SparseArray) + * @see Common#getOperationInfoForBlock(byte, byte, + * byte, de.syss.MifareClassicTool.Common.Operations, boolean, boolean) + * @see #writeDump(HashMap, SparseArray) + */ + private void checkTag() { + // Create reader. + MCReader reader = Common.checkForTagAndCreateReader(this); + if (reader == null) { + return; + } + + // Check if tag is correct size for dump. + if (reader.getSectorCount()-1 < Collections.max( + mDumpWithPos.keySet())) { + // Error. Tag too small for dump. + Toast.makeText(this, R.string.info_tag_too_small, + Toast.LENGTH_LONG).show(); + reader.close(); + return; + } + + // Check if tag is writable on needed blocks. + // Reformat for reader.isWritableOnPosition(...). + final SparseArray keyMap = + Common.getKeyMap(); + HashMap dataPos = + new HashMap<>(mDumpWithPos.size()); + for (int sector : mDumpWithPos.keySet()) { + int i = 0; + int[] blocks = new int[mDumpWithPos.get(sector).size()]; + for (int block : mDumpWithPos.get(sector).keySet()) { + blocks[i++] = block; + } + dataPos.put(sector, blocks); + } + HashMap> writeOnPos = + reader.isWritableOnPositions(dataPos, keyMap); + reader.close(); + + if (writeOnPos == null) { + // Error while checking for keys with write privileges. + Toast.makeText(this, R.string.info_check_ac_error, + Toast.LENGTH_LONG).show(); + return; + } + + // Skip dialog: + // Build a dialog showing all sectors and blocks containing data + // that can not be overwritten with the reason why they are not + // writable. The user can chose to skip all these blocks/sectors + // or to cancel the whole write procedure. + List> list = new + ArrayList<>(); + final HashMap> writeOnPosSafe = + new HashMap<>( + mDumpWithPos.size()); + // Keys that are missing completely (mDumpWithPos vs. keyMap). + HashSet sectors = new HashSet<>(); + for (int sector : mDumpWithPos.keySet()) { + if (keyMap.indexOfKey(sector) < 0) { + // Problem. Keys for sector not found. + addToList(list, getString(R.string.text_sector) + ": " + sector, + getString(R.string.text_keys_not_known)); + } else { + sectors.add(sector); + } + } + // Keys with write privileges that are missing or some + // blocks (block-parts) are read-only (writeOnPos vs. keyMap). + for (int sector : sectors) { + if (writeOnPos.get(sector) == null) { + // Error. Sector is dead (IO Error) or ACs are invalid. + addToList(list, getString(R.string.text_sector) + ": " + sector, + getString(R.string.text_invalid_ac_or_sector_dead)); + continue; + } + byte[][] keys = keyMap.get(sector); + Set blocks = mDumpWithPos.get(sector).keySet(); + for (int block : blocks) { + boolean isSafeForWriting = true; + Fragment fragment = getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + if (fragment instanceof WriteDumpFragment){ + if (!((WriteDumpFragment) fragment).getCheckBoxWriteTagDumpWriteManuf().isChecked() + && sector == 0 && block == 0) { + // Block 0 is read-only. This is normal. + // Do not add an entry to the dialog and skip the + // "write info" check (except for some + // special (non-original) MIFARE tags). + continue; + } + } + String position = getString(R.string.text_sector) + ": " + + sector + ", " + getString(R.string.text_block) + + ": " + block; + int writeInfo = writeOnPos.get(sector).get(block); + switch (writeInfo) { + case 0: + // Problem. Block is read-only. + addToList(list, position, getString( + R.string.text_block_read_only)); + isSafeForWriting = false; + break; + case 1: + if (keys[0] == null) { + // Problem. Key with write privileges (A) not known. + addToList(list, position, getString( + R.string.text_write_key_a_not_known)); + isSafeForWriting = false; + } + break; + case 2: + if (keys[1] == null) { + // Problem. Key with write privileges (B) not known. + addToList(list, position, getString( + R.string.text_write_key_b_not_known)); + isSafeForWriting = false; + } + break; + case 3: + // No Problem. Both keys have write privileges. + // Set to key A or B depending on which one is available. + writeInfo = (keys[0] != null) ? 1 : 2; + break; + case 4: + if (keys[0] == null) { + // Problem. Key with write privileges (A) not known. + addToList(list, position, getString( + R.string.text_write_key_a_not_known)); + isSafeForWriting = false; + } else { + // Problem. ACs are read-only. + addToList(list, position, getString( + R.string.text_ac_read_only)); + } + break; + case 5: + if (keys[1] == null) { + // Problem. Key with write privileges (B) not known. + addToList(list, position, getString( + R.string.text_write_key_b_not_known)); + isSafeForWriting = false; + } else { + // Problem. ACs are read-only. + addToList(list, position, getString( + R.string.text_ac_read_only)); + } + break; + case 6: + if (keys[1] == null) { + // Problem. Key with write privileges (B) not known. + addToList(list, position, getString( + R.string.text_write_key_b_not_known)); + isSafeForWriting = false; + } else { + // Problem. Keys are read-only. + addToList(list, position, getString( + R.string.text_keys_read_only)); + } + break; + case -1: + // Error. Some strange error occurred. Maybe due to some + // corrupted ACs... + addToList(list, position, getString( + R.string.text_strange_error)); + isSafeForWriting = false; + } + // Add if safe for writing. + if (isSafeForWriting) { + if (writeOnPosSafe.get(sector) == null) { + // Create sector. + HashMap blockInfo = + new HashMap<>(); + blockInfo.put(block, writeInfo); + writeOnPosSafe.put(sector, blockInfo); + } else { + // Add to sector. + writeOnPosSafe.get(sector).put(block, writeInfo); + } + } + } + } + + // Show skip/cancel dialog (if needed). + if (list.size() != 0) { + // If the user skips all sectors/blocks that are not writable, + // the writeTag() method will be called. + LinearLayout ll = new LinearLayout(this); + int pad = Common.dpToPx(5); + ll.setPadding(pad, pad, pad, pad); + ll.setOrientation(LinearLayout.VERTICAL); + TextView textView = new TextView(this); + textView.setText(R.string.dialog_not_writable); + textView.setTextAppearance(this, + android.R.style.TextAppearance_Medium); + ListView listView = new ListView(this); + ll.addView(textView); + ll.addView(listView); + String[] from = new String[] {"position", "reason"}; + int[] to = new int[] {android.R.id.text1, android.R.id.text2}; + ListAdapter adapter = new SimpleAdapter(this, list, + android.R.layout.two_line_list_item, from, to); + listView.setAdapter(adapter); + + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_not_writable_title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setView(ll) + .setPositiveButton(R.string.action_skip_blocks, + (dialog, which) -> { + // Skip not writable blocks and start writing. + writeDump(writeOnPosSafe, keyMap); + }) + .setNegativeButton(R.string.action_cancel_all, + (dialog, which) -> { + // Do nothing. + }) + .show(); + } else { + // Write. + writeDump(writeOnPosSafe, keyMap); + } + } + + /** + * Create an factory formatted, empty dump with a size matching + * the current tag size and then call {@link #checkTag()}. + * Factory (default) MIFARE Classic Access Conditions are: 0xFF0780XX + * XX = General purpose byte (GPB): Most of the time 0x69. At the end of + * an Tag XX = 0xBC. + * @see #checkTag() + */ + private void createFactoryFormattedDump() { + // This function is directly called after a key map was created. + // So Common.getTag() will return den current present tag + // (and its size/sector count). + mDumpWithPos = new HashMap<>(); + int sectors = MifareClassic.get(Common.getTag()).getSectorCount(); + byte[] emptyBlock = new byte[] + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + byte[] normalSectorTrailer = new byte[] {-1, -1, -1, -1, -1, -1, + -1, 7, -128, 105, -1, -1, -1, -1, -1, -1}; + byte[] lastSectorTrailer = new byte[] {-1, -1, -1, -1, -1, -1, + -1, 7, -128, -68, -1, -1, -1, -1, -1, -1}; + // Empty 4 block sector. + HashMap empty4BlockSector = + new HashMap<>(4); + for (int i = 0; i < 3; i++) { + empty4BlockSector.put(i, emptyBlock); + } + empty4BlockSector.put(3, normalSectorTrailer); + // Empty 16 block sector. + HashMap empty16BlockSector = + new HashMap<>(16); + for (int i = 0; i < 15; i++) { + empty16BlockSector.put(i, emptyBlock); + } + empty16BlockSector.put(15, normalSectorTrailer); + // Last sector. + HashMap lastSector; + + // Sector 0. + HashMap firstSector = + new HashMap<>(4); + firstSector.put(1, emptyBlock); + firstSector.put(2, emptyBlock); + firstSector.put(3, normalSectorTrailer); + mDumpWithPos.put(0, firstSector); + // Sector 1 - (max.) 31. + for (int i = 1; i < sectors && i < 32; i++) { + mDumpWithPos.put(i, empty4BlockSector); + } + // Sector 32 - 39. + if (sectors == 40) { + // Add the large sectors (containing 16 blocks) + // of a MIFARE Classic 4k tag. + for (int i = 32; i < sectors && i < 39; i++) { + mDumpWithPos.put(i, empty16BlockSector); + } + // In the last sector the Sector Trailer is different. + lastSector = new HashMap<>(empty16BlockSector); + lastSector.put(15, lastSectorTrailer); + } else { + // In the last sector the Sector Trailer is different. + lastSector = new HashMap<>(empty4BlockSector); + lastSector.put(3, lastSectorTrailer); + } + mDumpWithPos.put(sectors - 1, lastSector); + checkTag(); + } + + /** + * A helper function for {@link #checkTag()} adding an item to + * the list of all blocks with write issues. + * This list will be displayed to the user in a dialog before writing. + * @param list The list in which to add the key-value-pair. + * @param position The key (position) for the list item + * (e.g. "Sector 2, Block 3"). + * @param reason The value (reason) for the list item + * (e.g. "Block is read-only"). + */ + private void addToList(List> list, + String position, String reason) { + HashMap item = new HashMap<>(); + item.put( "position", position); + item.put( "reason", reason); + list.add(item); + } + + /** + * This method is triggered by {@link #checkTag()} and writes a dump + * to a tag. + * @param writeOnPos A map within a map (all with type = Integer). + * The key of the outer map is the sector number and the value is another + * map with key = block number and value = write information. The write + * information must be filtered (by {@link #checkTag()}) return values + * of {@link MCReader#isWritableOnPositions(HashMap, SparseArray)}.
+ * Attention: This method does not any checking. The position and write + * information must be checked by {@link #checkTag()}. + * @param keyMap A key map generated by {@link KeyMapCreator}. + */ + private void writeDump( + final HashMap> writeOnPos, + final SparseArray keyMap) { + // Check for write data. + if (writeOnPos.size() == 0) { + // Nothing to write. Exit. + Toast.makeText(this, R.string.info_nothing_to_write, + Toast.LENGTH_LONG).show(); + return; + } + + // Create reader. + final MCReader reader = Common.checkForTagAndCreateReader(this); + if (reader == null) { + return; + } + + // Display don't remove warning. + LinearLayout ll = new LinearLayout(this); + int pad = Common.dpToPx(10); + ll.setPadding(pad, pad, pad, pad); + ll.setGravity(Gravity.CENTER); + ProgressBar progressBar = new ProgressBar(this); + progressBar.setIndeterminate(true); + pad = Common.dpToPx(5); + progressBar.setPadding(0, 0, pad, 0); + TextView tv = new TextView(this); + tv.setText(getString(R.string.dialog_wait_write_tag)); + tv.setTextSize(18); + ll.addView(progressBar); + ll.addView(tv); + final AlertDialog warning = new AlertDialog.Builder(this) + .setTitle(R.string.dialog_wait_write_tag_title) + .setView(ll) + .create(); + warning.show(); + + + // Start writing in new thread. + final Activity a = this; + final Handler handler = new Handler(); + new Thread(() -> { + // Write dump to tag. + for (int sector : writeOnPos.keySet()) { + byte[][] keys = keyMap.get(sector); + for (int block : writeOnPos.get(sector).keySet()) { + // Select key with write privileges. + byte writeKey[] = null; + boolean useAsKeyB = true; + int wi = writeOnPos.get(sector).get(block); + if (wi == 1 || wi == 4) { + writeKey = keys[0]; // Write with key A. + useAsKeyB = false; + } else if (wi == 2 || wi == 5 || wi == 6) { + writeKey = keys[1]; // Write with key B. + } + + // Write block. + int result = reader.writeBlock(sector, block, + mDumpWithPos.get(sector).get(block), + writeKey, useAsKeyB); + + if (result != 0) { + // Error. Some error while writing. + handler.post(() -> Toast.makeText(a, + R.string.info_write_error, + Toast.LENGTH_LONG).show()); + reader.close(); + warning.cancel(); + return; + } + } + } + // Finished writing. + reader.close(); + warning.cancel(); + handler.post(() -> Toast.makeText(a, R.string.info_write_successful, + Toast.LENGTH_LONG).show()); + a.finish(); + }).start(); + } + + /** + * Display information about using custom Access Conditions for all + * sectors of the dump. + */ + public void onShowStaticACInfo() { + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_static_ac_title) + .setMessage(R.string.dialog_static_ac) + .setIcon(android.R.drawable.ic_dialog_info) + .setPositiveButton(R.string.action_ok, + (dialog, which) -> { + // Do nothing. + }).show(); + } + + /** + * Display information about writing to the manufacturer block. + * @see #showWriteManufInfo(boolean) + */ + public void onShowWriteManufInfo() { + showWriteManufInfo(false); + } + + /** + * Create a key map for the dump ({@link #mDumpWithPos}). + * @see KeyMapCreator + */ + private void createKeyMapForDump() { + // Show key map creator. + Intent intent = new Intent(this, KeyMapCreator.class); + intent.putExtra(KeyMapCreator.EXTRA_KEYS_DIR, + Common.getFileFromStorage(Common.HOME_DIR + "/" + + Common.KEYS_DIR).getAbsolutePath()); + intent.putExtra(KeyMapCreator.EXTRA_SECTOR_CHOOSER, false); + intent.putExtra(KeyMapCreator.EXTRA_SECTOR_CHOOSER_FROM, + (int) Collections.min(mDumpWithPos.keySet())); + intent.putExtra(KeyMapCreator.EXTRA_SECTOR_CHOOSER_TO, + (int) Collections.max(mDumpWithPos.keySet())); + intent.putExtra(KeyMapCreator.EXTRA_BUTTON_TEXT, + getString(R.string.action_create_key_map_and_write_dump)); + startActivityForResult(intent, CKM_WRITE_DUMP); + } + + /** + * Check the user input and (if correct) show the + * {@link KeyMapCreator} with predefined mapping range + * (see {@link #createKeyMapForBlock(int, boolean)}). + * After a key map was created {@link #writeValueBlock()} will be triggered. + * @see KeyMapCreator + * @see #checkSectorAndBlock(android.widget.EditText, + * android.widget.EditText) + */ + public void onWriteValue() { + // Check input. + Fragment fragment = getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + EditText mSectorTextVB = null; + EditText mBlockTextVB = null; + EditText mNewValueTextVB = null; + if (fragment instanceof ValueBlockFragment){ + mSectorTextVB = ((ValueBlockFragment) fragment).getEditTextWriteTagValueBlockSector(); + mBlockTextVB = ((ValueBlockFragment) fragment).getEditTextWriteTagValueBlockBlock(); + mNewValueTextVB = ((ValueBlockFragment) fragment).getEditTextWriteTagValueBlockValue(); + } + if (mSectorTextVB == null || + mBlockTextVB == null || + mNewValueTextVB == null || + !checkSectorAndBlock(mSectorTextVB, mBlockTextVB)) { + return; + } + + int sector = Integer.parseInt(mSectorTextVB.getText().toString()); + int block = Integer.parseInt(mBlockTextVB.getText().toString()); + if (block == 3 || block == 15 || (sector == 0 && block == 0)) { + // Error. Block can't be a Value Block. + Toast.makeText(this, R.string.info_not_vb, + Toast.LENGTH_LONG).show(); + return; + } + + try { + Integer.parseInt(mNewValueTextVB.getText().toString()); + } catch (Exception e) { + // Error. Value is too big. + Toast.makeText(this, R.string.info_value_too_big, + Toast.LENGTH_LONG).show(); + return; + } + + createKeyMapForBlock(sector, true); + } + + /** + * Called from {@link #onActivityResult(int, int, Intent)} + * after a key map was created, this method tries to increment or + * decrement the Value Block. Possible errors are displayed to the + * user via Toast. + * @see #onActivityResult(int, int, Intent) + * @see #onWriteValue() + */ + private void writeValueBlock() { + // Write the new value (incr./decr. + transfer). + MCReader reader = Common.checkForTagAndCreateReader(this); + if (reader == null) { + return; + } + Fragment fragment = getSupportFragmentManager().findFragmentByTag(getCurrentFragmentTag()); + EditText mSectorTextVB = null; + EditText mBlockTextVB = null; + EditText mNewValueTextVB = null; + RadioButton mIncreaseVB = null; + if (fragment instanceof ValueBlockFragment){ + mSectorTextVB = ((ValueBlockFragment) fragment).getEditTextWriteTagValueBlockSector(); + mBlockTextVB = ((ValueBlockFragment) fragment).getEditTextWriteTagValueBlockBlock(); + mNewValueTextVB = ((ValueBlockFragment) fragment).getEditTextWriteTagValueBlockValue(); + mIncreaseVB = ((ValueBlockFragment) fragment).getRadioButtonWriteTagWriteValueBlockIncr(); + } + if (mNewValueTextVB != null && mSectorTextVB != null && mBlockTextVB != null && mIncreaseVB != null) { + int value = Integer.parseInt(mNewValueTextVB.getText().toString()); + int sector = Integer.parseInt(mSectorTextVB.getText().toString()); + int block = Integer.parseInt(mBlockTextVB.getText().toString()); + + byte[][] keys = Common.getKeyMap().get(sector); + int result = -1; + + if (keys[1] != null) { + result = reader.writeValueBlock(sector, block, value, + mIncreaseVB.isChecked(), + keys[1], true); + } + // Error while writing? Maybe tag has default factory settings -> + // try to write with key a (if there is one). + if (result == -1 && keys[0] != null) { + result = reader.writeValueBlock(sector, block, value, + mIncreaseVB.isChecked(), + keys[0], false); + } + reader.close(); + + // Error handling. + switch (result) { + case 2: + Toast.makeText(this, R.string.info_block_not_in_sector, + Toast.LENGTH_LONG).show(); + return; + case -1: + Toast.makeText(this, R.string.info_error_writing_value_block, + Toast.LENGTH_LONG).show(); + return; + } + Toast.makeText(this, R.string.info_write_successful, + Toast.LENGTH_LONG).show(); + finish(); + + } else { + return; + } + } + + /** + * Get currently displaying fragment's tag + * @return + */ + + private String getCurrentFragmentTag() { + return "android:switcher:" + R.id.viewpager + ":" + viewPager.getCurrentItem(); + } + +} diff --git a/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/FactoryFormatFragment.java b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/FactoryFormatFragment.java new file mode 100644 index 00000000..c81ead08 --- /dev/null +++ b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/FactoryFormatFragment.java @@ -0,0 +1,80 @@ +package de.syss.MifareClassicTool.Fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import androidx.fragment.app.Fragment; + +import de.syss.MifareClassicTool.Activities.WriteTagFragmentActivity; +import de.syss.MifareClassicTool.R; + +public class FactoryFormatFragment extends Fragment { + + /** + * Constructor used by fragment manager activity + * {@link WriteTagFragmentActivity} + */ + + public FactoryFormatFragment() {} + private OnFactoryFormatListener callback; + private Button buttonWriteTagFactoryFormat; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Lifecycle's method that inflates fragment layout + * OnClickListeners should be set to layout views here + * @param inflater + * @param container + * @param savedInstanceState + * @return + */ + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_factory_format, container, false); + buttonWriteTagFactoryFormat = view.findViewById(R.id.buttonWriteTagFactoryFormat); + buttonWriteTagFactoryFormat.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onFactoryFormat(); + } + }); + return view; + } + + /** + * Interface used to communicate with fragment manager activity + * {@link WriteTagFragmentActivity} + */ + + public interface OnFactoryFormatListener { + void onFactoryFormatButtonClick(); + } + + /** + * Interface setter used by fragment manager activity + * {@link WriteTagFragmentActivity} + * @param callback WriteTagFragmentActivity class + */ + + public void setOnFactoryFormatListener(OnFactoryFormatListener callback) { + this.callback = callback; + } + + /** + * Method triggered by buttonWriteTagFactoryFormat clickListener + */ + + public void onFactoryFormat() { + callback.onFactoryFormatButtonClick(); + } +} diff --git a/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/ValueBlockFragment.java b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/ValueBlockFragment.java new file mode 100644 index 00000000..4b460839 --- /dev/null +++ b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/ValueBlockFragment.java @@ -0,0 +1,111 @@ +package de.syss.MifareClassicTool.Fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.RadioButton; + +import androidx.fragment.app.Fragment; + +import de.syss.MifareClassicTool.Activities.WriteTagFragmentActivity; +import de.syss.MifareClassicTool.R; + +public class ValueBlockFragment extends Fragment { + + /** + * Constructor used by fragment manager activity + * {@link WriteTagFragmentActivity} + */ + + public ValueBlockFragment() {} + private OnWriteValueListener callback; + private EditText editTextWriteTagValueBlockSector; + private EditText editTextWriteTagValueBlockBlock; + private EditText editTextWriteTagValueBlockValue; + private RadioButton radioButtonWriteTagWriteValueBlockIncr; + private Button buttonWriteTagValueBlock; + + /** + * Getters used by fragment manager activity + * + * @return + */ + + public EditText getEditTextWriteTagValueBlockSector() { + return editTextWriteTagValueBlockSector; + } + + public EditText getEditTextWriteTagValueBlockBlock() { + return editTextWriteTagValueBlockBlock; + } + + public EditText getEditTextWriteTagValueBlockValue() { + return editTextWriteTagValueBlockValue; + } + + public RadioButton getRadioButtonWriteTagWriteValueBlockIncr() { + return radioButtonWriteTagWriteValueBlockIncr; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Lifecycle's method that inflates fragment layout + * OnClickListeners should be set to layout views here + * @param inflater + * @param container + * @param savedInstanceState + * @return + */ + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_value_block, container, false); + editTextWriteTagValueBlockSector = view.findViewById(R.id.editTextWriteTagValueBlockSector); + editTextWriteTagValueBlockBlock = view.findViewById(R.id.editTextWriteTagValueBlockBlock); + editTextWriteTagValueBlockValue = view.findViewById(R.id.editTextWriteTagValueBlockValue); + radioButtonWriteTagWriteValueBlockIncr = view.findViewById(R.id.radioButtonWriteTagWriteValueBlockIncr); + buttonWriteTagValueBlock = view.findViewById(R.id.buttonWriteTagValueBlock); + buttonWriteTagValueBlock.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onWriteValue(); + } + }); + return view; + } + + /** + * Interface used to communicate with fragment manager activity + * {@link WriteTagFragmentActivity} + */ + + public interface OnWriteValueListener { + void onWriteValueButtonClick(); + } + + /** + * Interface setter used by fragment manager activity + * {@link WriteTagFragmentActivity} + * @param callback WriteTagFragmentActivity class + */ + + public void setOnWriteValueListener(OnWriteValueListener callback) { + this.callback = callback; + } + + /** + * Method triggered by buttonWriteTagValueBlock clickListener + */ + + public void onWriteValue() { + callback.onWriteValueButtonClick(); + } +} diff --git a/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/WriteBlockFragment.java b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/WriteBlockFragment.java new file mode 100644 index 00000000..bfb13314 --- /dev/null +++ b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/WriteBlockFragment.java @@ -0,0 +1,108 @@ +package de.syss.MifareClassicTool.Fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; + +import de.syss.MifareClassicTool.Activities.WriteTagFragmentActivity; + +import androidx.fragment.app.Fragment; + +import de.syss.MifareClassicTool.R; + +public class WriteBlockFragment extends Fragment { + + /** + * Constructor used by fragment manager activity + * {@link WriteTagFragmentActivity} + */ + + public WriteBlockFragment() {} + private OnWriteBlockListener callback; + private Button buttonWriteTagBlock; + private EditText mSectorTextBlock; + private EditText mBlockTextBlock; + private EditText mDataText; + + /** + * Getters used by fragment manager activity + * + * @return + */ + + public EditText getmSectorTextBlock() { + return mSectorTextBlock; + } + + public EditText getmBlockTextBlock() { + return mBlockTextBlock; + } + + public EditText getmDataText() { + return mDataText; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Lifecycle's method that inflates fragment layout + * OnClickListeners should be set to layout views here + * @param inflater + * @param container + * @param savedInstanceState + * @return + */ + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_write_block, container, false); + + buttonWriteTagBlock = view.findViewById(R.id.buttonWriteTagBlock); + mSectorTextBlock = view.findViewById(R.id.editTextWriteTagSector); + mBlockTextBlock = view.findViewById(R.id.editTextWriteTagBlock); + mDataText = view.findViewById(R.id.editTextWriteTagData); + + buttonWriteTagBlock.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onWriteBlock(); + } + }); + + return view; + } + + /** + * Interface used to communicate with fragment manager activity + * {@link WriteTagFragmentActivity} + */ + + public interface OnWriteBlockListener { + void onWriteBlockButtonClick(); + } + + /** + * Interface setter used by fragment manager activity + * {@link WriteTagFragmentActivity} + * @param callback WriteTagFragmentActivity class + */ + + public void setOnWriteBlockListener(OnWriteBlockListener callback) { + this.callback = callback; + } + + /** + * Method triggered by buttonWriteTagBlock clickListener + */ + public void onWriteBlock() { + callback.onWriteBlockButtonClick(); + } + + +} diff --git a/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/WriteDumpFragment.java b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/WriteDumpFragment.java new file mode 100644 index 00000000..57c533ba --- /dev/null +++ b/Mifare Classic Tool/app/src/main/java/de/syss/MifareClassicTool/Fragments/WriteDumpFragment.java @@ -0,0 +1,153 @@ +package de.syss.MifareClassicTool.Fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import androidx.fragment.app.Fragment; + +import de.syss.MifareClassicTool.Activities.WriteTagFragmentActivity; +import de.syss.MifareClassicTool.R; + +public class WriteDumpFragment extends Fragment { + + /** + * Constructor used by fragment manager activity + * {@link WriteTagFragmentActivity} + */ + + public WriteDumpFragment() {} + private OnWriteDumpListener callback; + private CheckBox checkBoxWriteTagDumpWriteManuf; + private CheckBox checkBoxWriteTagDumpStaticAC; + private CheckBox checkBoxWriteTagDumpOptions; + private EditText editTextWriteTagDumpStaticAC; + private Button buttonWriteTagDump; + private ImageButton imageButtonWriteTagDumpStaticACInfo; + private ImageButton imageButtonWriteTagDumpWriteManufInfo; + + /** + * Getters used by fragment manager activity + * + * @return + */ + + public CheckBox getCheckBoxWriteTagDumpWriteManuf() { + return checkBoxWriteTagDumpWriteManuf; + } + + public CheckBox getCheckBoxWriteTagDumpStaticAC() { + return checkBoxWriteTagDumpStaticAC; + } + + public EditText getEditTextWriteTagDumpStaticAC() { + return editTextWriteTagDumpStaticAC; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Lifecycle's method that inflates fragment layout + * OnClickListeners should be set to layout views here + * @param inflater + * @param container + * @param savedInstanceState + * @return + */ + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_write_dump, container, false); + LinearLayout ll = view.findViewById(R.id.linearLayoutWriteTagDumpOptions); + checkBoxWriteTagDumpWriteManuf = view.findViewById(R.id.checkBoxWriteTagDumpWriteManuf); + checkBoxWriteTagDumpStaticAC = view.findViewById(R.id.checkBoxWriteTagDumpStaticAC); + checkBoxWriteTagDumpOptions = view.findViewById(R.id.checkBoxWriteTagDumpOptions); + editTextWriteTagDumpStaticAC = view.findViewById(R.id.editTextWriteTagDumpStaticAC); + buttonWriteTagDump = view.findViewById(R.id.buttonWriteTagDump); + imageButtonWriteTagDumpStaticACInfo = view.findViewById(R.id.imageButtonWriteTagDumpStaticACInfo); + imageButtonWriteTagDumpWriteManufInfo = view.findViewById(R.id.imageButtonWriteTagDumpWriteManufInfo); + checkBoxWriteTagDumpOptions.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + ll.setVisibility(View.VISIBLE); + } else { + ll.setVisibility(View.GONE); + } + } + }); + buttonWriteTagDump.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onWriteDump(); + } + }); + imageButtonWriteTagDumpStaticACInfo.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onShowStaticACInfo(); + } + }); + imageButtonWriteTagDumpWriteManufInfo.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onShowWriteManufInfo(); + } + }); + return view; + } + + /** + * Interface used to communicate with fragment manager activity + * {@link WriteTagFragmentActivity} + */ + + public interface OnWriteDumpListener { + void onWriteDumpButtonClick(); + void onShowStaticACInfoClick(); + void onShowWriteManufInfoClick(); + } + + /** + * Interface setter used by fragment manager activity + * {@link WriteTagFragmentActivity} + * @param callback WriteTagFragmentActivity class + */ + + public void setOnWriteBlockListener(OnWriteDumpListener callback) { + this.callback = callback; + } + + /** + * Method triggered by buttonWriteTagDump clickListener + */ + public void onWriteDump() { + callback.onWriteDumpButtonClick(); + } + + /** + * Method triggered by imageButtonWriteTagDumpStaticACInfo clickListener + */ + public void onShowStaticACInfo() { + callback.onShowStaticACInfoClick(); + } + + /** + * Method triggered by imageButtonWriteTagDumpStaticACInfo clickListener + */ + public void onShowWriteManufInfo() { + callback.onShowWriteManufInfoClick(); + } + +} diff --git a/Mifare Classic Tool/app/src/main/res/layout/activity_main_menu.xml b/Mifare Classic Tool/app/src/main/res/layout/activity_main_menu.xml index 1e26a1a6..bdba7187 100644 --- a/Mifare Classic Tool/app/src/main/res/layout/activity_main_menu.xml +++ b/Mifare Classic Tool/app/src/main/res/layout/activity_main_menu.xml @@ -64,7 +64,7 @@ android:layout_height="match_parent" android:layout_weight="0.5" android:drawableTop="@drawable/write_tag" - android:onClick="onShowWriteTag" + android:onClick="onShowWriteTagFragmentActivity" android:text="@string/action_write_tag" /> diff --git a/Mifare Classic Tool/app/src/main/res/layout/activity_write_tag_fragment_adapter.xml b/Mifare Classic Tool/app/src/main/res/layout/activity_write_tag_fragment_adapter.xml new file mode 100644 index 00000000..920b9116 --- /dev/null +++ b/Mifare Classic Tool/app/src/main/res/layout/activity_write_tag_fragment_adapter.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Mifare Classic Tool/app/src/main/res/layout/fragment_factory_format.xml b/Mifare Classic Tool/app/src/main/res/layout/fragment_factory_format.xml new file mode 100644 index 00000000..2d2d7ee1 --- /dev/null +++ b/Mifare Classic Tool/app/src/main/res/layout/fragment_factory_format.xml @@ -0,0 +1,56 @@ + + + + + + + + + + +