diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java index 7aaa3a49..c3fa94f5 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java @@ -6,6 +6,8 @@ import de.davis.passwordmanager.database.ElementType; import de.davis.passwordmanager.database.entities.details.ElementDetail; import de.davis.passwordmanager.utils.CreditCardUtil; +import de.davis.passwordmanager.utils.card.Card; +import de.davis.passwordmanager.utils.card.CardFactory; public class CreditCardDetails implements ElementDetail { @@ -22,7 +24,7 @@ public CreditCardDetails(Name cardholder, String expirationDate, String cardNumb this.expirationDate = expirationDate; this.cardholder = cardholder; - this.cardNumber = cardNumber.replaceAll("\\s", ""); + this.cardNumber = cardNumber; this.cvv = cvv; } @@ -35,7 +37,7 @@ public void setCardholder(Name cardholder) { } public String getCardNumber() { - return cardNumber; + return getCard().getRawNumber(); } public void setCardNumber(String cardNumber) { @@ -43,11 +45,11 @@ public void setCardNumber(String cardNumber) { } public String getSecretNumber(){ - return getFormattedNumber().replaceAll("(\\d{4}\\s){3}", "**** **** **** "); + return getCard().mask(); } public String getFormattedNumber(){ - return CreditCardUtil.formatNumber(getCardNumber()); + return getCard().getCardNumber(); } public String getCvv() { @@ -71,6 +73,10 @@ public ElementType getElementType() { return ElementType.CREDIT_CARD; } + public Card getCard(){ + return CardFactory.INSTANCE.createFromCardNumber(cardNumber); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/app/src/main/java/de/davis/passwordmanager/listeners/text/CreditCardNumberTextWatcher.java b/app/src/main/java/de/davis/passwordmanager/listeners/text/CreditCardNumberTextWatcher.java index f79fae59..b4a3c34c 100644 --- a/app/src/main/java/de/davis/passwordmanager/listeners/text/CreditCardNumberTextWatcher.java +++ b/app/src/main/java/de/davis/passwordmanager/listeners/text/CreditCardNumberTextWatcher.java @@ -1,14 +1,31 @@ package de.davis.passwordmanager.listeners.text; import android.text.Editable; +import android.text.InputFilter; import android.text.TextWatcher; +import android.widget.EditText; + +import java.util.function.Consumer; import de.davis.passwordmanager.utils.CreditCardUtil; +import de.davis.passwordmanager.utils.card.CardType; +import de.davis.passwordmanager.utils.card.Formatter; public class CreditCardNumberTextWatcher implements TextWatcher { + private final EditText cardNumberEditText; private boolean changing; + private Consumer onTypeDetected; + + public CreditCardNumberTextWatcher(EditText cardNumberEditText) { + this.cardNumberEditText = cardNumberEditText; + } + + public void setOnTypeDetected(Consumer onTypeDetected) { + this.onTypeDetected = onTypeDetected; + } + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @@ -22,7 +39,32 @@ public void afterTextChanged(Editable s) { changing = true; - s.replace(0, s.length(), CreditCardUtil.formatNumber(s.toString())); + CardType type = CardType.Companion.getTypeByNumber(s.toString()); + int length = type.getLengthRange().getEndInclusive(); + if(type.getFormatter() instanceof Formatter.FourDigitChunkFormatter) + length += Math.floorDiv(length - 1, 4); + else + length += 2; // FourSixRemainderChunkFormatter can only add up to 2 more spaces + + String formatted = CreditCardUtil.formatNumber(s.toString()); + + if(formatted.length() > length) + formatted = formatted.substring(0, length); + + cardNumberEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(length)}); + if(onTypeDetected != null) + onTypeDetected.accept(type); + + + int selectionEnd = cardNumberEditText.getSelectionEnd(); + boolean isLast = cardNumberEditText.length() == selectionEnd; + + s.replace(0, s.length(), formatted); + + if(isLast) + cardNumberEditText.setSelection(formatted.length()); + else + cardNumberEditText.setSelection(Math.min(formatted.length(), selectionEnd)); changing = false; } diff --git a/app/src/main/java/de/davis/passwordmanager/services/autofill/builder/SuggestedDatasetBuilder.kt b/app/src/main/java/de/davis/passwordmanager/services/autofill/builder/SuggestedDatasetBuilder.kt index 467ad0d1..b6dc187c 100644 --- a/app/src/main/java/de/davis/passwordmanager/services/autofill/builder/SuggestedDatasetBuilder.kt +++ b/app/src/main/java/de/davis/passwordmanager/services/autofill/builder/SuggestedDatasetBuilder.kt @@ -14,11 +14,11 @@ object SuggestedDatasetBuilder { builder: (TextProvider, requestCode: Int) -> Dataset ): List = url?.let { url -> SecureElementManager.getSecureElements(typeId) - .take(n) .filter { (it.detail as PasswordDetails).origin.couldBeUrl(url) || it.title.couldBeUrl(url) } + .take(n) .mapIndexed { index, element -> builder(element.getTextProvider(), index) } } ?: emptyList() diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/SecureElementViewHolder.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/SecureElementViewHolder.java index 72763c79..b306dd8d 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/SecureElementViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/SecureElementViewHolder.java @@ -29,6 +29,7 @@ import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails; import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.ui.views.OptionBottomSheet; +import de.davis.passwordmanager.utils.card.CardType; public class SecureElementViewHolder extends BasicViewHolder { @@ -73,11 +74,19 @@ public void bindGeneral(@NonNull SecureElement item, String filter, OnItemClicke image.setImageDrawable(item.getIcon(context)); if(item.getElementType() == ElementType.PASSWORD){ + type.setText(item.getElementType().getTitle()); info.setText(((PasswordDetails)item.getDetail()).getStrength().getString()); info.setTextColor(((PasswordDetails)item.getDetail()).getStrength().getColor(context)); }else{ CreditCardDetails details = (CreditCardDetails) item.getDetail(); - setShortenedTextIfNeeded(info, details.getSecretNumber(), details.getSecretNumber().substring(15, 19)); + CardType cardType = details.getCard().getType(); + if(cardType == CardType.Unknown) + type.setText(item.getElementType().getTitle()); + else + type.setText(cardType.name().replaceAll("([a-z])([A-Z])", "$1 $2")); + + String secret = details.getSecretNumber(); + setShortenedTextIfNeeded(info, secret, secret.substring(secret.lastIndexOf(" "))); info.setTextColor(MaterialColors.getColor(itemView.getContext(), com.google.android.material.R.attr.colorOnSurface, Color.BLACK)); } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java index 6fcde5bc..c7819a40 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java @@ -4,6 +4,7 @@ import android.graphics.Color; import android.os.Bundle; import android.provider.Settings; +import android.text.InputFilter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -36,6 +37,7 @@ import de.davis.passwordmanager.text.method.CreditCardNumberTransformationMethod; import de.davis.passwordmanager.ui.elements.CreateSecureElementActivity; import de.davis.passwordmanager.utils.CreditCardUtil; +import de.davis.passwordmanager.utils.card.CardType; public class CreateCreditCardActivity extends CreateSecureElementActivity { @@ -63,10 +65,20 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).addTextChangedListener(new ExpiryDateTextWatcher()); - Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).addTextChangedListener(new CreditCardNumberTextWatcher()); - binding.textInputLayoutCardNumber.getEditText().setTransformationMethod(CreditCardNumberTransformationMethod.getInstance()); + CreditCardNumberTextWatcher textWatcher = new CreditCardNumberTextWatcher(binding.cardNumber); + binding.cardNumber.addTextChangedListener(textWatcher); + binding.cardNumber.setTransformationMethod(CreditCardNumberTransformationMethod.getInstance()); binding.textInputLayoutCardNumber.setEndIconOnClickListener(new OnCreditCardEndIconClickListener(binding.textInputLayoutCardNumber)); + + textWatcher.setOnTypeDetected(cardType -> { + int cvvMaxLength = cardType == CardType.AmericanExpress ? 4 : 3; + binding.cardCVV.setFilters(new InputFilter[]{new InputFilter.LengthFilter(cvvMaxLength)}); + + String cvv = Objects.requireNonNull(binding.cardCVV.getText()).toString(); + binding.cardCVV.setText(cvv.subSequence(0, Math.min(cvv.length(), cvvMaxLength))); + }); + nfcManager = new NfcManager(this) { @Override protected void cardReceived(EmvCard card, CommunicationException e) { @@ -100,10 +112,10 @@ public void fillInElement(@NonNull SecureElement element) { binding.textInputLayoutTitle.getEditText().setText(element.getTitle()); CreditCardDetails details = (CreditCardDetails) element.getDetail(); - binding.textInputLayoutUsername.getEditText().setText(details.getCardholder().getFullName()); - binding.textInputLayoutCardNumber.getEditText().setText(details.getCardNumber()); - binding.textInputLayoutCardCVV.getEditText().setText(details.getCvv()); - binding.textInputLayoutCardDate.getEditText().setText(details.getExpirationDate()); + binding.cardHolder.setText(details.getCardholder().getFullName()); + binding.cardNumber.setText(details.getCardNumber()); + binding.cardCVV.setText(details.getCvv()); + binding.expirationDate.setText(details.getExpirationDate()); } @Override @@ -154,8 +166,8 @@ public CreateSecureElementActivity.Result check() { result.setSuccess(true); String title = Objects.requireNonNull(binding.textInputLayoutTitle.getEditText()).getText().toString(); - String creditCardNumber = Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).getText().toString(); - String expiryDate = Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).getText().toString(); + String creditCardNumber = Objects.requireNonNull(binding.cardNumber.getText()).toString(); + String expiryDate = Objects.requireNonNull(binding.expirationDate.getText()).toString(); if(title.isBlank()){ binding.textInputLayoutTitle.setError(getString(R.string.is_not_filled_in)); @@ -169,8 +181,8 @@ public CreateSecureElementActivity.Result check() { }else binding.textInputLayoutCardNumber.setErrorEnabled(false); - if(!CreditCardUtil.isValidCardNumberLength(creditCardNumber)){ - binding.textInputLayoutCardNumber.setError(getString(R.string.invalid_card_number_length)); + if(!CreditCardUtil.isValidCardNumberLength(creditCardNumber) || !CreditCardUtil.isValidCheckSum(creditCardNumber)){ + binding.textInputLayoutCardNumber.setError(getString(R.string.invalid_card)); result.setSuccess(false); }else binding.textInputLayoutCardNumber.setErrorEnabled(false); @@ -192,11 +204,11 @@ public CreateSecureElementActivity.Result check() { @Override protected SecureElement toElement() { String title = Objects.requireNonNull(binding.textInputLayoutTitle.getEditText()).getText().toString().trim(); - String creditCardNumber = Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).getText().toString().trim(); - String expiryDate = Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).getText().toString().trim(); - String cvv = Objects.requireNonNull(binding.textInputLayoutCardCVV.getEditText()).getText().toString().trim(); + String creditCardNumber = Objects.requireNonNull(binding.cardNumber.getText()).toString().trim(); + String expiryDate = Objects.requireNonNull(binding.expirationDate.getText()).toString().trim(); + String cvv = Objects.requireNonNull(binding.cardCVV.getText()).toString().trim(); - Name name = Name.fromFullName(Objects.requireNonNull(binding.textInputLayoutUsername.getEditText()).getText().toString()); + Name name = Name.fromFullName(Objects.requireNonNull(binding.cardHolder.getText()).toString()); CreditCardDetails details = new CreditCardDetails(name, expiryDate, creditCardNumber, cvv); SecureElement card = getElement() == null ? @@ -220,9 +232,9 @@ private void insertCard(EmvCard card){ String cardNumber = card.getCardNumber(); String expireString = CreditCardUtil.formatDate(card.getExpireDate()); - Objects.requireNonNull(binding.textInputLayoutUsername.getEditText()).setText(name.getFullName()); - Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).setText(cardNumber); - Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).setText(expireString); + binding.cardHolder.setText(name.getFullName()); + binding.cardNumber.setText(cardNumber); + binding.expirationDate.setText(expireString); } private void setNfcMessageSuccess(@StringRes int stringRes){ diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java index 0b14b87c..c009ef19 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java @@ -47,7 +47,7 @@ public void fillInElement(@NonNull SecureElement creditCard) { TextInputLayout til = view.findViewById(R.id.textInputLayout); EditText et = til.getEditText(); et.setKeyListener(DigitsKeyListener.getInstance("0123456789 ")); - et.addTextChangedListener(new CreditCardNumberTextWatcher()); + et.addTextChangedListener(new CreditCardNumberTextWatcher(et)); til.setEndIconOnClickListener(new OnCreditCardEndIconClickListener(til)); }); binding.cardNumber.setTransformationMethod(CreditCardNumberTransformationMethod.getInstance()); diff --git a/app/src/main/java/de/davis/passwordmanager/utils/CreditCardUtil.java b/app/src/main/java/de/davis/passwordmanager/utils/CreditCardUtil.java index da5992fa..97c3b86e 100644 --- a/app/src/main/java/de/davis/passwordmanager/utils/CreditCardUtil.java +++ b/app/src/main/java/de/davis/passwordmanager/utils/CreditCardUtil.java @@ -6,6 +6,9 @@ import java.util.Date; import java.util.Locale; +import de.davis.passwordmanager.utils.card.Card; +import de.davis.passwordmanager.utils.card.CardFactory; + public class CreditCardUtil { public static boolean isValidDateFormat(String formatted){ @@ -24,14 +27,24 @@ public static boolean isValidCardNumberLength(String cardNumber){ if(cardNumber == null) return false; - String formatted = cardNumber.replaceAll("\\s", ""); + Card card = CardFactory.INSTANCE.createFromCardNumber(cardNumber); + + return card.isValidLength(); + } + + public static boolean isValidCheckSum(String cardNumber){ + if(cardNumber == null) + return false; + + Card card = CardFactory.INSTANCE.createFromCardNumber(cardNumber); - return formatted.length() == 16; + return card.isValidLuhnNumber(); } public static String formatNumber(String s){ - String f = s.replaceAll("\\s", "").replaceAll("\\d{4}", "$0 "); - return f.endsWith(" ") ? f.substring(0, f.length() -1) : f; + Card card = CardFactory.INSTANCE.createFromCardNumber(s); + + return card.getCardNumber(); } public static String formatDate(Date date){ diff --git a/app/src/main/java/de/davis/passwordmanager/utils/card/Card.kt b/app/src/main/java/de/davis/passwordmanager/utils/card/Card.kt new file mode 100644 index 00000000..9e16a759 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/utils/card/Card.kt @@ -0,0 +1,13 @@ +package de.davis.passwordmanager.utils.card + +import de.davis.passwordmanager.utils.card.algorithm.LuhnAlgorithm + +class Card(val rawNumber: String, val type: CardType) { + val cardNumber: String = type.formatter.format(rawNumber) + + fun mask() = type.formatter.format(rawNumber.replace("\\d(?=\\d{4})".toRegex(), "•")) + + fun isValidLuhnNumber(): Boolean = LuhnAlgorithm.isValid(rawNumber) + + fun isValidLength(): Boolean = type.lengthRange.contains(rawNumber.length) +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/utils/card/CardFactory.kt b/app/src/main/java/de/davis/passwordmanager/utils/card/CardFactory.kt new file mode 100644 index 00000000..3159d973 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/utils/card/CardFactory.kt @@ -0,0 +1,7 @@ +package de.davis.passwordmanager.utils.card + +object CardFactory { + fun createFromCardNumber(cardNumber: String): Card { + return Card(cardNumber.replace(" ", ""), CardType.getTypeByNumber(cardNumber)) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/utils/card/CardType.kt b/app/src/main/java/de/davis/passwordmanager/utils/card/CardType.kt new file mode 100644 index 00000000..7bbaa067 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/utils/card/CardType.kt @@ -0,0 +1,22 @@ +package de.davis.passwordmanager.utils.card + +enum class CardType( + val prefixes: List, + val lengthRange: IntRange, + val formatter: Formatter +) { + Visa(listOf("4"), 16..16, Formatter.FourDigitChunkFormatter), + MasterCard((1..5).map { "5$it" }, 16..16, Formatter.FourDigitChunkFormatter), + AmericanExpress(listOf("34", "37"), 15..15, Formatter.FourSixRemainderChunkFormatter), + Discover(listOf("6011", "65"), 16..16, Formatter.FourDigitChunkFormatter), + JCB(listOf("35"), 16..19, Formatter.FourDigitChunkFormatter), + DinnersClub(listOf("36", "38", "39"), 14..14, Formatter.FourSixRemainderChunkFormatter), + + Unknown(emptyList(), 14..19, Formatter.FourDigitChunkFormatter); + + companion object { + fun getTypeByNumber(cardNumber: String) = entries.firstOrNull { + it.prefixes.any { prefix -> cardNumber.startsWith(prefix) } + } ?: Unknown + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/utils/card/Formatter.kt b/app/src/main/java/de/davis/passwordmanager/utils/card/Formatter.kt new file mode 100644 index 00000000..9c0a8e12 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/utils/card/Formatter.kt @@ -0,0 +1,26 @@ +package de.davis.passwordmanager.utils.card + +sealed class Formatter { + + data object FourDigitChunkFormatter : Formatter() { + + override fun runFormat(cardNumber: String): String = cardNumber.chunked(4).joinToString(" ") + } + + + data object FourSixRemainderChunkFormatter : Formatter() { + + override fun runFormat(cardNumber: String): String = when { + cardNumber.length <= 4 -> cardNumber + cardNumber.length <= 10 -> cardNumber.take(4) + " " + cardNumber.substring(4) + else -> cardNumber.substring(0, 4) + " " + cardNumber.substring(4, 10) + + " " + cardNumber.substring(10) + } + } + + protected abstract fun runFormat(cardNumber: String): String + + fun format(cardNumber: String): String { + return runFormat(cardNumber.replace(" ", "")) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/utils/card/algorithm/LuhnAlgorithm.kt b/app/src/main/java/de/davis/passwordmanager/utils/card/algorithm/LuhnAlgorithm.kt new file mode 100644 index 00000000..7c902b90 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/utils/card/algorithm/LuhnAlgorithm.kt @@ -0,0 +1,12 @@ +package de.davis.passwordmanager.utils.card.algorithm + +object LuhnAlgorithm { + + fun isValid(cardNumber: String): Boolean { + val sanitizedNumber = cardNumber.filter { it.isDigit() } + return sanitizedNumber.reversed().mapIndexed { index, c -> + val digit = c.digitToInt() + if (index % 2 == 1) (digit * 2).let { if (it > 9) it - 9 else it } else digit + }.sum() % 10 == 0 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_create_creditcard.xml b/app/src/main/res/layout/activity_create_creditcard.xml index 23f3dcba..b081b36e 100644 --- a/app/src/main/res/layout/activity_create_creditcard.xml +++ b/app/src/main/res/layout/activity_create_creditcard.xml @@ -58,7 +58,7 @@ app:startIconDrawable="@drawable/ic_baseline_person_24"> @@ -95,7 +95,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberPassword" - android:maxLength="3" /> + android:maxLength="4" /> Passwort ist inkorrekt Passwort stimmt nicht überein Dieses Feld muss ausgefüllt sein - Kreditkartennummer muss 16 Zahlen beinhalten + Ungültige Kartennummer Ungültiges Datum Ungültige URL diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae549a13..e725f8ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,7 +38,7 @@ Incorrect password Password does not match This must be filled in - Card number must have 16 numbers + Invalid card number Invalid date Invalid url