diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/security/PublicKey.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/security/PublicKey.kt index 12c911fba4..dde15b764a 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/security/PublicKey.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/model/objects/security/PublicKey.kt @@ -1,6 +1,7 @@ package com.github.dedis.popstellar.model.objects.security import com.github.dedis.popstellar.model.Immutable +import com.github.dedis.popstellar.utility.GeneralUtils import com.google.crypto.tink.PublicKeyVerify import com.google.crypto.tink.subtle.Ed25519Verify import java.security.GeneralSecurityException @@ -14,13 +15,16 @@ import timber.log.Timber @Immutable class PublicKey : Base64URLData { private val verifier: PublicKeyVerify + @Transient private var label: String constructor(data: ByteArray) : super(data) { verifier = Ed25519Verify(data) + label = GeneralUtils.generateUsernameFromBase64(this.encoded) } constructor(data: String) : super(data) { verifier = Ed25519Verify(this.data) + label = GeneralUtils.generateUsernameFromBase64(this.encoded) } fun verify(signature: Signature, data: Base64URLData): Boolean { @@ -33,6 +37,14 @@ class PublicKey : Base64URLData { } } + /** + * Function that return the username of the public key The username is deterministic and is + * computed from the hash of the public key + */ + fun getLabel(): String { + return label + } + /** * Function that compute the hash of a public key * @@ -52,5 +64,13 @@ class PublicKey : Base64URLData { companion object { private val TAG = PublicKey::class.java.simpleName + + fun findPublicKeyFromUsername( + username: String, + publicKeys: List, + default: PublicKey + ): PublicKey { + return publicKeys.firstOrNull { it.getLabel() == username } ?: default + } } } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashIssueFragment.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashIssueFragment.kt index 71dea44b6d..f6d602eb45 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashIssueFragment.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashIssueFragment.kt @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment import com.github.dedis.popstellar.R import com.github.dedis.popstellar.databinding.DigitalCashIssueFragmentBinding import com.github.dedis.popstellar.model.objects.security.PublicKey +import com.github.dedis.popstellar.model.objects.security.PublicKey.Companion.findPublicKeyFromUsername import com.github.dedis.popstellar.ui.lao.LaoActivity.Companion.obtainDigitalCashViewModel import com.github.dedis.popstellar.ui.lao.LaoActivity.Companion.obtainViewModel import com.github.dedis.popstellar.ui.lao.LaoActivity.Companion.setCurrentFragment @@ -74,14 +75,19 @@ class DigitalCashIssueFragment : Fragment() { private fun issueCoins() { /*Take the amount entered by the user*/ val currentAmount = binding.digitalCashIssueAmount.text.toString() - val currentPublicKeySelected = binding.digitalCashIssueSpinner.editText!!.text.toString() + val currentPublicKeySelected = + findPublicKeyFromUsername( + binding.digitalCashIssueSpinner.editText!!.text.toString(), + digitalCashViewModel.attendeesFromTheRollCallList, + digitalCashViewModel.validToken.publicKey) val radioGroup = binding.digitalCashIssueSelect.checkedRadioButtonId if (digitalCashViewModel.canPerformTransaction( - currentAmount, currentPublicKeySelected, radioGroup)) { + currentAmount, currentPublicKeySelected.encoded, radioGroup)) { try { val issueMap = - computeMapForPostTransaction(currentAmount, currentPublicKeySelected, radioGroup) + computeMapForPostTransaction( + currentAmount, currentPublicKeySelected.encoded, radioGroup) if (issueMap.isEmpty()) { displayToast(radioGroup) } else { @@ -165,7 +171,7 @@ class DigitalCashIssueFragment : Fragment() { /* Roll Call attendees to which we can send*/ var myArray: List try { - myArray = digitalCashViewModel.attendeesFromTheRollCallList + myArray = digitalCashViewModel.attendeesFromTheRollCallList.map { it.getLabel() } } catch (e: NoRollCallException) { Timber.tag(TAG).e(getString(R.string.error_no_rollcall_closed_in_LAO)) Toast.makeText( diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashReceiptFragment.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashReceiptFragment.kt index 61c5cea1b5..ae27fbef67 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashReceiptFragment.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashReceiptFragment.kt @@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment import com.github.dedis.popstellar.R import com.github.dedis.popstellar.SingleEvent import com.github.dedis.popstellar.databinding.DigitalCashReceiptFragmentBinding +import com.github.dedis.popstellar.model.objects.security.PublicKey import com.github.dedis.popstellar.ui.lao.LaoActivity.Companion.addBackNavigationCallback import com.github.dedis.popstellar.ui.lao.LaoActivity.Companion.obtainDigitalCashViewModel import com.github.dedis.popstellar.ui.lao.LaoActivity.Companion.obtainViewModel @@ -51,11 +52,12 @@ class DigitalCashReceiptFragment : Fragment() { } digitalCashViewModel.getUpdateReceiptAddressEvent().observe(viewLifecycleOwner) { - stringEvent: SingleEvent -> - val address = stringEvent.contentIfNotHandled - if (address != null) { + stringEvent: SingleEvent -> + val pk = stringEvent.contentIfNotHandled + if (pk != null) { binding.digitalCashReceiptBeneficiary.text = - String.format(resources.getString(R.string.digital_cash_beneficiary_address), address) + String.format( + resources.getString(R.string.digital_cash_beneficiary_address), pk.getLabel()) } } } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashReceiveFragment.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashReceiveFragment.kt index 11eb534540..994ff7b1f5 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashReceiveFragment.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashReceiveFragment.kt @@ -60,7 +60,7 @@ class DigitalCashReceiveFragment : Fragment() { val token = digitalCashViewModel.validToken val publicKey = token.publicKey - binding.digitalCashReceiveAddress.text = publicKey.encoded + binding.digitalCashReceiveAddress.text = publicKey.getLabel() val tokenData = PopTokenData(token.publicKey) val myBitmap = diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashSendFragment.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashSendFragment.kt index e435e5c9cf..cd9ab59aa6 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashSendFragment.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashSendFragment.kt @@ -11,6 +11,7 @@ import com.github.dedis.popstellar.R import com.github.dedis.popstellar.SingleEvent import com.github.dedis.popstellar.databinding.DigitalCashSendFragmentBinding import com.github.dedis.popstellar.model.objects.security.PublicKey +import com.github.dedis.popstellar.model.objects.security.PublicKey.Companion.findPublicKeyFromUsername import com.github.dedis.popstellar.ui.lao.LaoActivity.Companion.obtainDigitalCashViewModel import com.github.dedis.popstellar.ui.lao.LaoActivity.Companion.obtainViewModel import com.github.dedis.popstellar.ui.lao.LaoActivity.Companion.setCurrentFragment @@ -60,15 +61,20 @@ class DigitalCashSendFragment : Fragment() { val event = booleanEvent.contentIfNotHandled if (event != null) { val currentAmount = binding.digitalCashSendAmount.text.toString() - val currentPublicKeySelected = binding.digitalCashSendSpinner.editText?.text.toString() + val currentPublicKeySelected = + findPublicKeyFromUsername( + binding.digitalCashSendSpinner.editText?.text.toString(), + digitalCashViewModel.attendeesFromTheRollCallList, + digitalCashViewModel.validToken.publicKey) if (digitalCashViewModel.canPerformTransaction( - currentAmount, currentPublicKeySelected, -1)) { + currentAmount, currentPublicKeySelected.encoded, -1)) { try { val token = digitalCashViewModel.validToken if (canPostTransaction(token.publicKey, currentAmount.toInt())) { laoViewModel.addDisposable( - postTransaction(Collections.singletonMap(currentPublicKeySelected, currentAmount)) + postTransaction( + Collections.singletonMap(currentPublicKeySelected.encoded, currentAmount)) .subscribe( { digitalCashViewModel.updateReceiptAddressEvent(currentPublicKeySelected) @@ -124,7 +130,8 @@ class DigitalCashSendFragment : Fragment() { /* Roll Call attendees to which we can send */ var myArray: MutableList try { - myArray = digitalCashViewModel.attendeesFromTheRollCallList.toMutableList() + myArray = + digitalCashViewModel.attendeesFromTheRollCallList.map { it.getLabel() }.toMutableList() } catch (e: NoRollCallException) { Timber.tag(TAG).d(e) Toast.makeText( @@ -155,7 +162,7 @@ class DigitalCashSendFragment : Fragment() { */ private fun removeOwnToken(members: MutableList) { try { - members.remove(digitalCashViewModel.validToken.publicKey.encoded) + members.remove(digitalCashViewModel.validToken.publicKey.getLabel()) } catch (e: KeyException) { Timber.tag(TAG).e(e, resources.getString(R.string.error_retrieve_own_token)) } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashViewModel.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashViewModel.kt index f3c352033f..5a736f6e15 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashViewModel.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashViewModel.kt @@ -40,7 +40,6 @@ import io.reactivex.Single import java.nio.charset.StandardCharsets import java.security.GeneralSecurityException import java.util.Collections -import java.util.stream.Collectors import javax.inject.Inject import timber.log.Timber @@ -69,7 +68,7 @@ constructor( private val postTransactionEvent = MutableLiveData>() /* Update the receipt after sending a transaction */ - private val updateReceiptAddressEvent = MutableLiveData>() + private val updateReceiptAddressEvent = MutableLiveData>() private val updateReceiptAmountEvent = MutableLiveData>() fun getPostTransactionEvent(): LiveData> { @@ -80,11 +79,11 @@ constructor( postTransactionEvent.postValue(SingleEvent(true)) } - fun getUpdateReceiptAddressEvent(): LiveData> { + fun getUpdateReceiptAddressEvent(): LiveData> { return updateReceiptAddressEvent } - fun updateReceiptAddressEvent(address: String?) { + fun updateReceiptAddressEvent(address: PublicKey?) { updateReceiptAddressEvent.postValue(SingleEvent(address)) } @@ -237,9 +236,8 @@ constructor( get() = lao.organizer @get:Throws(NoRollCallException::class) - val attendeesFromTheRollCallList: List - get() = - attendeesFromLastRollCall.stream().map(Base64URLData::encoded).collect(Collectors.toList()) + val attendeesFromTheRollCallList: List + get() = attendeesFromLastRollCall.toList() @get:Throws(UnknownLaoException::class) val lao: LaoView diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/HistoryListAdapter.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/HistoryListAdapter.kt index 14bbdeb706..ad1a7ce36a 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/HistoryListAdapter.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/digitalcash/HistoryListAdapter.kt @@ -51,7 +51,10 @@ class HistoryListAdapter( holder.transactionIdValue.text = transactionId holder.transactionProvenanceTitle.setText( if (element.isSender) R.string.digital_cash_to else R.string.digital_cash_from) - holder.transactionProvenanceValue.text = element.senderOrReceiver + holder.transactionProvenanceValue.text = + if (element.senderOrReceiver.encoded == viewModel.organizer.encoded) + element.senderOrReceiver.encoded + " (organizer)" + else element.senderOrReceiver.getLabel() val listener = View.OnClickListener { @@ -96,7 +99,6 @@ class HistoryListAdapter( // To know if we are in input or not. We assume that no two different person val isSender = transactionObject.isSender(ownKey) val isIssuance = transactionObject.isCoinBaseTransaction - transactionHistoryElements.addAll( transactionObject.outputs .stream() // If we are in input, we want all output except us. If we are not in input, @@ -108,8 +110,11 @@ class HistoryListAdapter( } .map { outputObject: OutputObject -> TransactionHistoryElement( - if (isSender) outputObject.pubKeyHash - else transactionObject.inputs[0].pubKey.encoded, + // TODO : was previously outputObject.pubKeyHash, but was wrong (hash is smaller + // than 32 bytes and is clearly not the receivers public key) + // I set it to ownKey so that it works for now, but should actually show the + // public key of the receiver | Maxime @Kaz-ookid 06.2025 + if (isSender) ownKey else PublicKey(transactionObject.inputs[0].pubKey.encoded), outputObject.value.toString(), transactionObject.transactionId, !isIssuance && isSender) @@ -125,9 +130,9 @@ class HistoryListAdapter( val transactionTypeTitle: TextView = itemView.findViewById(R.id.history_transaction_type_title) val transactionTypeValue: TextView = itemView.findViewById(R.id.history_transaction_type_value) val transactionProvenanceTitle: TextView = - itemView.findViewById(R.id.history_transaction_provenance_value) - val transactionProvenanceValue: TextView = itemView.findViewById(R.id.history_transaction_provenance_title) + val transactionProvenanceValue: TextView = + itemView.findViewById(R.id.history_transaction_provenance_value) val transactionIdValue: TextView = itemView.findViewById(R.id.history_transaction_transaction_id_value) val detailLayout: ConstraintLayout = @@ -136,14 +141,14 @@ class HistoryListAdapter( } private class TransactionHistoryElement( - val senderOrReceiver: String, + val senderOrReceiver: PublicKey, val value: String, val id: String, val isSender: Boolean ) { override fun toString(): String { - return "TransactionHistoryElement{senderOrReceiver='$senderOrReceiver', value='$value', " + + return "TransactionHistoryElement{senderOrReceiverHash='${senderOrReceiver.encoded}', senderOrReceiverUsername='${senderOrReceiver.getLabel()}',value='$value', " + "id='$id', isSender=$isSender}" } } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallArrayAdapter.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallArrayAdapter.kt index 74f098b098..28ca5bce06 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallArrayAdapter.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallArrayAdapter.kt @@ -1,6 +1,7 @@ package com.github.dedis.popstellar.ui.lao.event.rollcall import android.content.Context +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter @@ -8,29 +9,56 @@ import android.widget.TextView import androidx.core.content.ContextCompat import com.github.dedis.popstellar.R import com.github.dedis.popstellar.model.objects.security.PoPToken +import com.github.dedis.popstellar.model.objects.security.PublicKey class RollCallArrayAdapter( private val context: Context, private val layout: Int, - private val attendeesList: List, + private val attendeesList: List, private val myToken: PoPToken?, private val fragment: RollCallFragment -) : ArrayAdapter(context, layout, attendeesList) { +) : ArrayAdapter(context, layout, attendeesList) { init { - fragment.isAttendeeListSorted(attendeesList, context) + fragment.isAttendeeListSorted(attendeesList.map { it.encoded }, context) } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = super.getView(position, convertView, parent) + val view: View + val holder: ViewHolder - // highlights our token in the list - val currentToken = getItem(position) - if (myToken != null && currentToken == myToken.publicKey.encoded) { - val colorAccent = ContextCompat.getColor(context, R.color.colorAccent) - (view as TextView).setTextColor(colorAccent) + if (convertView == null) { + view = LayoutInflater.from(context).inflate(R.layout.list_item_attendee, parent, false) + holder = ViewHolder(view) + view.tag = holder + } else { + view = convertView + holder = view.tag as ViewHolder + } + + val publicKey = getItem(position) + if (publicKey != null) { + holder.usernameTextView.text = publicKey.getLabel() + holder.hashTextView.text = publicKey.encoded + + // Set the default color + val defaultColor = ContextCompat.getColor(context, R.color.textOnBackground) + holder.usernameTextView.setTextColor(defaultColor) + holder.hashTextView.setTextColor(defaultColor) + + // highlights our token in the list + if (myToken != null && publicKey.encoded == myToken.publicKey.encoded) { + val colorAccent = ContextCompat.getColor(context, R.color.colorAccent) + holder.usernameTextView.setTextColor(colorAccent) + holder.hashTextView.setTextColor(colorAccent) + } } return view } + + private class ViewHolder(view: View) { + val usernameTextView: TextView = view.findViewById(R.id.username_text_view) + val hashTextView: TextView = view.findViewById(R.id.hash_text_view) + } } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallFragment.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallFragment.kt index e23865a64f..2a03db5769 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallFragment.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallFragment.kt @@ -248,7 +248,7 @@ class RollCallFragment : AbstractEventFragment { binding.rollCallAttendeesText.visibility = visibility binding.listViewAttendees.visibility = visibility - var attendeesList: List? = null + var attendeesList: List? = null if (isOrganizer && rollCall.isOpen) { // Show the list of all time scanned attendees if the roll call is opened // and the user is the organizer @@ -256,8 +256,7 @@ class RollCallFragment : AbstractEventFragment { rollCallViewModel .getAttendees() .stream() - .map(PublicKey::encoded) - .sorted(compareBy(String::toString)) + .sorted(compareBy { it.encoded }) .collect(Collectors.toList()) binding.rollCallAttendeesText.text = @@ -266,7 +265,7 @@ class RollCallFragment : AbstractEventFragment { rollCallViewModel.getAttendees().size) } else if (rollCall.isClosed) { val orderedAttendees: MutableSet = LinkedHashSet(rollCall.attendees) - attendeesList = orderedAttendees.stream().map(PublicKey::encoded).collect(Collectors.toList()) + attendeesList = orderedAttendees.stream().collect(Collectors.toList()) // Show the list of attendees if the roll call has ended binding.rollCallAttendeesText.text = @@ -295,10 +294,12 @@ class RollCallFragment : AbstractEventFragment { private fun retrieveAndDisplayPublicKey() { val popToken = popToken ?: return val pk = popToken.publicKey.encoded + val pkUsername = popToken.publicKey.getLabel() Timber.tag(TAG).d("key displayed is %s", pk) // Set the QR visible only if the rollcall is opened and the user isn't the organizer if (rollCall.isOpen) { + binding.rollCallPopTokenUsername.text = pkUsername binding.rollCallPopTokenText.text = pk binding.rollCallPkQrCode.visibility = View.VISIBLE binding.rollCallPopTokenText.visibility = View.VISIBLE diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListAdapter.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListAdapter.kt index d42fff6d47..d0528235f8 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListAdapter.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListAdapter.kt @@ -83,6 +83,7 @@ class ChirpListAdapter( previousDisposable?.dispose() val sender = chirp.sender + val senderUsername = sender.getLabel() val timestamp = chirp.timestamp val text: String val itemUsername = view.findViewById(R.id.social_media_username) @@ -186,7 +187,7 @@ class ChirpListAdapter( text = chirp.text } - itemUsername.text = sender.encoded + itemUsername.text = senderUsername itemTime.text = DateUtils.getRelativeTimeSpanString(timestamp * 1000) itemText.text = text diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/token/TokenFragment.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/token/TokenFragment.kt index 294087f243..5f4fb7e2f0 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/token/TokenFragment.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/ui/lao/token/TokenFragment.kt @@ -73,6 +73,7 @@ class TokenFragment : Fragment() { .bitmap() binding.tokenQrCode.setImageBitmap(bitmap) + binding.tokenTextUsernameView.text = poPToken.publicKey.getLabel() binding.tokenTextView.text = poPToken.publicKey.encoded clipboardManager.setupCopyButton(binding.tokenCopyButton, binding.tokenTextView, "Token") } catch (e: Exception) { diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.kt index 4a17c9b4e9..5a1883d85a 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/Constants.kt @@ -61,4 +61,16 @@ object Constants { /** Orientation down along the x axis */ const val ORIENTATION_DOWN = 180f + + /** Number of Digits in a Mnemonic Username */ + const val USERNAME_DIGITS = 4 + + /** Number of Words in a Mnemonic Username */ + const val MNEMONIC_USERNAME_WORDS = 2 + + /** Empty Username Placeholder */ + const val EMPTY_USERNAME = "emptyBase64" + + /** Default Username Placeholder */ + const val DEFAULT_USERNAME = "defaultUsername" } diff --git a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/GeneralUtils.kt b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/GeneralUtils.kt index 9e4e31182c..c5c0330120 100644 --- a/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/GeneralUtils.kt +++ b/fe2-android/app/src/main/java/com/github/dedis/popstellar/utility/GeneralUtils.kt @@ -5,6 +5,10 @@ import android.app.Application import android.os.Bundle import androidx.lifecycle.Lifecycle import com.github.dedis.popstellar.model.objects.security.Base64URLData +import com.github.dedis.popstellar.utility.Constants.DEFAULT_USERNAME +import com.github.dedis.popstellar.utility.Constants.EMPTY_USERNAME +import com.github.dedis.popstellar.utility.Constants.MNEMONIC_USERNAME_WORDS +import com.github.dedis.popstellar.utility.Constants.USERNAME_DIGITS import io.github.novacrypto.bip39.MnemonicGenerator import io.github.novacrypto.bip39.wordlists.English import java.security.MessageDigest @@ -74,7 +78,7 @@ object GeneralUtils { * * @param input base64 string * @param numberOfWords number of mnemonic words we want to generate - * @return two mnemonic words + * @return numberOfWords mnemonic words */ @JvmStatic fun generateMnemonicWordFromBase64(input: String, numberOfWords: Int): String { @@ -106,14 +110,48 @@ object GeneralUtils { sb.append(s) } - sb.toString().split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + sb.toString().split(" ").dropLastWhile { it.isEmpty() }.toTypedArray() } catch (e: NoSuchAlgorithmException) { Timber.tag(TAG) - .e( - e, - "Error generating the mnemonic for the base64 string %s", - Base64URLData(data).encoded) + .e(e, "Error generating mnemonic for base64 string %s", Base64URLData(data).encoded) emptyArray() } } + + /** + * This function generates a unique and memorable username from a base64 string. The username is + * composed of two words and a 4 digits number. The result is deterministic. The 4 digits number + * is the first 4 digits found in the base64 string, starting from the left. + * + * @param input base64 string. + * @return a username composed of two words and a 4 digits number. + */ + @JvmStatic + fun generateUsernameFromBase64(input: String): String { + if (input.isEmpty()) { + Timber.tag(TAG).w("Empty input for username generation") + return EMPTY_USERNAME + } + + val number = getFirstNumberDigits(input) + val words = generateMnemonicWordFromBase64(input, MNEMONIC_USERNAME_WORDS) + if (words.isEmpty()) { + Timber.tag(TAG).w("Empty words for username generation for base64 string %s", input) + return "$DEFAULT_USERNAME$number" + } + + val (word1, word2) = words.split(" ") + val capitalizedWord1 = + word1.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + val capitalizedWord2 = + word2.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + + return "$capitalizedWord1$capitalizedWord2$number" + } + + /** Filters the digits from a base64 string and returns the first n digits. */ + private fun getFirstNumberDigits(b64: String, nbDigits: Int = USERNAME_DIGITS): String { + val digits = b64.filter { it.isDigit() } + return digits.take(nbDigits).padStart(nbDigits, '0') + } } diff --git a/fe2-android/app/src/main/res/layout/list_item_attendee.xml b/fe2-android/app/src/main/res/layout/list_item_attendee.xml new file mode 100644 index 0000000000..e3a0546c36 --- /dev/null +++ b/fe2-android/app/src/main/res/layout/list_item_attendee.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/fe2-android/app/src/main/res/layout/roll_call_fragment.xml b/fe2-android/app/src/main/res/layout/roll_call_fragment.xml index b6e6afe9a1..1ab38393df 100644 --- a/fe2-android/app/src/main/res/layout/roll_call_fragment.xml +++ b/fe2-android/app/src/main/res/layout/roll_call_fragment.xml @@ -75,13 +75,22 @@ app:layout_constraintTop_toBottomOf="@+id/roll_call_end_time" /> + + + + Scanned tokens : %1$d Description Location + Username Attendees list is not sorted, risk of de-anonymization" diff --git a/fe2-android/app/src/test/framework/common/java/com/github/dedis/popstellar/testutils/pages/lao/digitalcash/HistoryPageObject.java b/fe2-android/app/src/test/framework/common/java/com/github/dedis/popstellar/testutils/pages/lao/digitalcash/HistoryPageObject.java index dddf3043b5..890cf5a4fc 100644 --- a/fe2-android/app/src/test/framework/common/java/com/github/dedis/popstellar/testutils/pages/lao/digitalcash/HistoryPageObject.java +++ b/fe2-android/app/src/test/framework/common/java/com/github/dedis/popstellar/testutils/pages/lao/digitalcash/HistoryPageObject.java @@ -14,4 +14,29 @@ private HistoryPageObject() { public static int fragmentDigitalCashHistoryId() { return R.id.fragment_digital_cash_history; } + + @IdRes + public static int transactionCardView() { + return R.id.transaction_card_view; + } + + @IdRes + public static int transactionProvenanceTitle() { + return R.id.history_transaction_provenance_title; + } + + @IdRes + public static int transactionProvenanceValue() { + return R.id.history_transaction_provenance_value; + } + + @IdRes + public static int transactionIdValue() { + return R.id.history_transaction_transaction_id_value; + } + + @IdRes + public static int transactionIdTitle() { + return R.id.history_transaction_transaction_id_title; + } } diff --git a/fe2-android/app/src/test/framework/common/java/com/github/dedis/popstellar/testutils/pages/lao/digitalcash/IssuePageObject.java b/fe2-android/app/src/test/framework/common/java/com/github/dedis/popstellar/testutils/pages/lao/digitalcash/IssuePageObject.java index e6e115bb05..43dfa23382 100644 --- a/fe2-android/app/src/test/framework/common/java/com/github/dedis/popstellar/testutils/pages/lao/digitalcash/IssuePageObject.java +++ b/fe2-android/app/src/test/framework/common/java/com/github/dedis/popstellar/testutils/pages/lao/digitalcash/IssuePageObject.java @@ -1,6 +1,10 @@ package com.github.dedis.popstellar.testutils.pages.lao.digitalcash; +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.matcher.ViewMatchers.withId; + import androidx.annotation.IdRes; +import androidx.test.espresso.ViewInteraction; import com.github.dedis.popstellar.R; @@ -14,4 +18,20 @@ private IssuePageObject() { public static int fragmentDigitalCashIssueId() { return R.id.fragment_digital_cash_issue; } + + public static ViewInteraction issueButton() { + return onView(withId(R.id.digital_cash_issue_issue)); + } + + public static ViewInteraction issueAmount() { + return onView(withId(R.id.digital_cash_issue_amount)); + } + + public static ViewInteraction radioButtonAttendees() { + return onView(withId(R.id.radioButtonAttendees)); + } + + public static ViewInteraction spinner() { + return onView(withId(R.id.digital_cash_issue_spinner_tv)); + } } diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/LaoActivityTest.kt b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/LaoActivityTest.kt index 477409f0dd..828f7a3afe 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/LaoActivityTest.kt +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/LaoActivityTest.kt @@ -12,6 +12,8 @@ import com.github.dedis.popstellar.testutils.Base64DataUtils import com.github.dedis.popstellar.testutils.BundleBuilder import com.github.dedis.popstellar.testutils.IntentUtils import com.github.dedis.popstellar.testutils.pages.lao.LaoActivityPageObject +import com.github.dedis.popstellar.utility.GeneralUtils.generateMnemonicWordFromBase64 +import com.github.dedis.popstellar.utility.GeneralUtils.generateUsernameFromBase64 import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import java.time.Instant diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashActivityTest.kt b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashActivityTest.kt index 4e057f7ce7..c4d2f16fa0 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashActivityTest.kt +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/digitalcash/DigitalCashActivityTest.kt @@ -1,9 +1,12 @@ package com.github.dedis.popstellar.ui.lao.digitalcash import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.dedis.popstellar.model.network.method.message.data.digitalcash.Output import com.github.dedis.popstellar.model.network.method.message.data.digitalcash.ScriptOutput @@ -44,10 +47,6 @@ import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import io.reactivex.Completable import io.reactivex.subjects.BehaviorSubject -import java.nio.charset.StandardCharsets -import java.security.GeneralSecurityException -import java.util.Collections -import javax.inject.Inject import org.junit.Rule import org.junit.Test import org.junit.rules.ExternalResource @@ -57,6 +56,10 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoTestRule +import java.nio.charset.StandardCharsets +import java.security.GeneralSecurityException +import java.util.Collections +import javax.inject.Inject @HiltAndroidTest @RunWith(AndroidJUnit4::class) @@ -179,15 +182,15 @@ class DigitalCashActivityTest { DigitalCashPageObject.sendButton().perform(ViewActions.click()) LaoActivityPageObject.fragmentContainer() .check( - ViewAssertions.matches( - ViewMatchers.withChild(ViewMatchers.withId(SendPageObject.fragmentDigitalCashSendId())) + matches( + ViewMatchers.withChild(withId(SendPageObject.fragmentDigitalCashSendId())) ) ) SendPageObject.sendButtonToReceipt().perform(ViewActions.click()) LaoActivityPageObject.fragmentContainer() .check( - ViewAssertions.matches( - ViewMatchers.withChild(ViewMatchers.withId(SendPageObject.fragmentDigitalCashSendId())) + matches( + ViewMatchers.withChild(withId(SendPageObject.fragmentDigitalCashSendId())) ) ) } @@ -197,33 +200,79 @@ class DigitalCashActivityTest { DigitalCashPageObject.historyButton().perform(ViewActions.click()) LaoActivityPageObject.fragmentContainer() .check( - ViewAssertions.matches( + matches( ViewMatchers.withChild( - ViewMatchers.withId(HistoryPageObject.fragmentDigitalCashHistoryId()) + withId(HistoryPageObject.fragmentDigitalCashHistoryId()) ) ) ) } + @Test + fun historyElementsAreExpandable() { + // Ensure the Digital Cash screen is displayed + DigitalCashPageObject.historyButton().perform(ViewActions.click()) + + // Click on the first transaction + onView(withId(HistoryPageObject.transactionCardView())) + .perform(ViewActions.click()) + + // Check if the transaction details are displayed + onView(withId(HistoryPageObject.transactionProvenanceTitle()) + ) + .check(matches(isDisplayed())) + onView(withId(HistoryPageObject.transactionProvenanceValue()) + ) + .check(matches(isDisplayed())) + onView(withId(HistoryPageObject.transactionIdValue()) + ) + .check(matches(isDisplayed())) + onView(withId(HistoryPageObject.transactionIdTitle()) + ) + .check(matches(isDisplayed())) + } + @Test fun issueButtonGoesToIssue() { DigitalCashPageObject.issueButton().perform(ViewActions.click()) LaoActivityPageObject.fragmentContainer() .check( - ViewAssertions.matches( - ViewMatchers.withChild(ViewMatchers.withId(IssuePageObject.fragmentDigitalCashIssueId())) + matches( + ViewMatchers.withChild(withId(IssuePageObject.fragmentDigitalCashIssueId())) ) ) } + @Test + fun issueButtonsWork(){ + DigitalCashPageObject.issueButton().perform(ViewActions.click()) + LaoActivityPageObject.fragmentContainer() + .check( + matches( + ViewMatchers.withChild(withId(IssuePageObject.fragmentDigitalCashIssueId())) + ) + ) + + // open the spinner + IssuePageObject.spinner().perform(ViewActions.click()) + //close the spinner + IssuePageObject.spinner().perform(ViewActions.click()) + // select the radio button + IssuePageObject.radioButtonAttendees().perform(ViewActions.click()) + // input amount + IssuePageObject.issueAmount().perform(ViewActions.typeText("500")) + // click issue button + IssuePageObject.issueButton().perform(ViewActions.click()) + } + @Test fun receiveButtonGoesToReceive() { DigitalCashPageObject.receiveButton().perform(ViewActions.click()) LaoActivityPageObject.fragmentContainer() .check( - ViewAssertions.matches( + matches( ViewMatchers.withChild( - ViewMatchers.withId(ReceivePageObject.fragmentDigitalCashReceiveId()) + withId(ReceivePageObject.fragmentDigitalCashReceiveId()) ) ) ) @@ -235,9 +284,9 @@ class DigitalCashActivityTest { DigitalCashPageObject.historyButton().perform(ViewActions.click()) LaoActivityPageObject.fragmentContainer() .check( - ViewAssertions.matches( + matches( ViewMatchers.withChild( - ViewMatchers.withId(DigitalCashPageObject.fragmentDigitalCashHomeId()) + withId(DigitalCashPageObject.fragmentDigitalCashHomeId()) ) ) ) diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallArrayAdapterTest.kt b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallArrayAdapterTest.kt index 9846703e28..cdb1ef9a6e 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallArrayAdapterTest.kt +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/event/rollcall/RollCallArrayAdapterTest.kt @@ -1,13 +1,15 @@ package com.github.dedis.popstellar.ui.lao.event.rollcall import android.content.Context +import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup +import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat import androidx.test.core.app.ApplicationProvider import com.github.dedis.popstellar.R import com.github.dedis.popstellar.model.objects.security.PoPToken +import com.github.dedis.popstellar.model.objects.security.PublicKey import net.i2p.crypto.eddsa.Utils import org.junit.Assert import org.junit.Before @@ -28,40 +30,55 @@ class RollCallArrayAdapterTest { val context = ApplicationProvider.getApplicationContext() private val MY_PRIVATE_KEY = - Utils.hexToBytes("3b28b4ab2fe355a13d7b24f90816ff0676f7978bf462fc84f1d5d948b119ec66") + Utils.hexToBytes("3b28b4ab2fe355a13d7b24f90816ff0676f7978bf462fc84f1d5d948b119ec66") private val MY_PUBLIC_KEY = - Utils.hexToBytes("e5cdb393fe6e0abacd99d521400968083a982400b6ac3e0a1e8f6018d1554bd7") + Utils.hexToBytes("e5cdb393fe6e0abacd99d521400968083a982400b6ac3e0a1e8f6018d1554bd7") private val OTHER_PRIVATE_KEY = - Utils.hexToBytes("cf74d353042400806ee94c3e77eef983d9a1434d21c0a7568f203f5b091dde1d") + Utils.hexToBytes("cf74d353042400806ee94c3e77eef983d9a1434d21c0a7568f203f5b091dde1d") private val OTHER_PUBLIC_KEY = - Utils.hexToBytes("6015ae4d770294f94e651a9fd6ba9c6a11e5c80803c63ee472ad525f4c3523a6") + Utils.hexToBytes("6015ae4d770294f94e651a9fd6ba9c6a11e5c80803c63ee472ad525f4c3523a6") - private lateinit var attendeesList: List + private lateinit var attendeesList: List @Before fun setup() { // Setting up a list of two tokens and the view val myToken = PoPToken(MY_PRIVATE_KEY, MY_PUBLIC_KEY) val otherToken = PoPToken(OTHER_PRIVATE_KEY, OTHER_PUBLIC_KEY) - attendeesList = listOf(myToken.publicKey.encoded, otherToken.publicKey.encoded) - adapter = RollCallArrayAdapter(context, R.id.valid_token_layout_text, attendeesList, myToken, mock(RollCallFragment::class.java)) - mockView = TextView(context) - val colorAccent = ContextCompat.getColor(context, R.color.textOnBackground) - (mockView as TextView).setTextColor(colorAccent) + attendeesList = listOf(myToken.publicKey, otherToken.publicKey) + adapter = RollCallArrayAdapter(context, R.layout.list_item_attendee, attendeesList, myToken, mock(RollCallFragment::class.java)) + + // Use the correct layout for mockView + val inflater = LayoutInflater.from(context) + val parent = LinearLayout(context) + mockView = inflater.inflate(R.layout.list_item_attendee, parent, false) } @Test fun verifyOurTokenIsHighlighted() { - val view = adapter.getView(0, mockView, mock(ViewGroup::class.java)) as TextView + val parent = LinearLayout(context) + val view = adapter.getView(0, null, parent) + val usernameTextView = view.findViewById(R.id.username_text_view) + val hashTextView = view.findViewById(R.id.hash_text_view) val color = ContextCompat.getColor(context, R.color.colorAccent) - Assert.assertEquals(color, view.currentTextColor) + + Assert.assertEquals(color, usernameTextView.currentTextColor) + Assert.assertEquals(color, hashTextView.currentTextColor) } @Test fun verifyOtherTokenIsNotHighlighted() { - val view = adapter.getView(1, mockView, mock(ViewGroup::class.java)) as TextView + val parent = LinearLayout(context) + val view = adapter.getView(1, null, parent) + val usernameTextView = view.findViewById(R.id.username_text_view) + val hashTextView = view.findViewById(R.id.hash_text_view) val color = ContextCompat.getColor(context, R.color.textOnBackground) - Assert.assertEquals(color, view.currentTextColor) - } + Assert.assertEquals(color, usernameTextView.currentTextColor) + Assert.assertEquals(color, hashTextView.currentTextColor) + } } + + + + diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListAdapterTest.kt b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListAdapterTest.kt index 23bbdbc216..c134a184a8 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListAdapterTest.kt +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListAdapterTest.kt @@ -219,7 +219,7 @@ class ChirpListAdapterTest { // Check the user is matching correctly val user = view1.findViewById(R.id.social_media_username) Assert.assertNotNull(user) - Assert.assertEquals(SENDER_1.encoded, user.text.toString()) + Assert.assertEquals(SENDER_1.getLabel(), user.text.toString()) // Check the time is matching correctly val time = view1.findViewById(R.id.social_media_time) diff --git a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListFragmentTest.kt b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListFragmentTest.kt index 8ae049821c..77b2d897e8 100644 --- a/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListFragmentTest.kt +++ b/fe2-android/app/src/test/ui/robolectric/com/github/dedis/popstellar/ui/lao/socialmedia/ChirpListFragmentTest.kt @@ -236,7 +236,7 @@ class ChirpListFragmentTest { // Check the user is matching correctly val user = view1.findViewById(R.id.social_media_username) Assert.assertNotNull(user) - Assert.assertEquals(SENDER_1.encoded, user.text.toString()) + Assert.assertEquals(SENDER_1.getLabel(), user.text.toString()) // Check the time is matching correctly val time = view1.findViewById(R.id.social_media_time) diff --git a/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/model/objects/security/KeyPairTest.kt b/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/model/objects/security/KeyPairTest.kt index edb33bfdd2..ee1ad4a1f2 100644 --- a/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/model/objects/security/KeyPairTest.kt +++ b/fe2-android/app/src/test/unit/java/com/github/dedis/popstellar/model/objects/security/KeyPairTest.kt @@ -1,6 +1,9 @@ package com.github.dedis.popstellar.model.objects.security import com.github.dedis.popstellar.testutils.MockitoKotlinHelpers +import com.github.dedis.popstellar.utility.Constants.EMPTY_USERNAME +import com.github.dedis.popstellar.utility.Constants.USERNAME_DIGITS +import com.github.dedis.popstellar.utility.GeneralUtils import java.security.GeneralSecurityException import net.i2p.crypto.eddsa.Utils import org.junit.Assert @@ -50,6 +53,27 @@ class KeyPairTest { Assert.assertEquals("SGnNfF533PBEUMYPMqBSQY83z5U=", pk.computeHash()) } + @Test + fun pubKeyUsernameDigits() { + val pk = PublicKey("oKHk3AivbpNXk_SfFcHDaVHcCcY8IBfHE7auXJ7h4ms=") + val digits = "3877" + // last 4 characters of the hash are the 4 first numerical digits of the hash + Assert.assertEquals(digits, pk.getLabel().substring(pk.getLabel().length - USERNAME_DIGITS)) + } + + @Test + fun pubKeyUsernameHashContainsLessDigits() { + val pk = PublicKey("oKHk3AivbpNXk_SfFcHDaVHcCcY8IBfHE7auXJmhmms=") + val digits = "0387" + // If the Hash contains less than 4 digits, the username will be padded with 0 + Assert.assertEquals(digits, pk.getLabel().substring(pk.getLabel().length - USERNAME_DIGITS)) + } + + @Test + fun generateUsernameFromBase64EmptyInput() { + Assert.assertEquals(EMPTY_USERNAME, GeneralUtils.generateUsernameFromBase64("")) + } + companion object { private val SIGNATURE = Signature("U0lHTkFUVVJF") private val DATA = Base64URLData("REFUQQ==")