diff --git a/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt b/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt index 0a0c02cc1..563d9122d 100644 --- a/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt +++ b/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt @@ -18,15 +18,18 @@ import nl.tudelft.ipv8.android.IPv8Android import nl.tudelft.ipv8.android.keyvault.AndroidCryptoProvider import nl.tudelft.ipv8.android.messaging.bluetooth.BluetoothLeDiscovery import nl.tudelft.ipv8.android.peerdiscovery.NetworkServiceDiscovery -import nl.tudelft.ipv8.attestation.schema.SchemaManager +import nl.tudelft.ipv8.attestation.common.AuthorityManager +import nl.tudelft.ipv8.attestation.common.SchemaManager +import nl.tudelft.ipv8.attestation.communication.CommunicationManager +import nl.tudelft.ipv8.attestation.identity.store.IdentitySQLiteStore +import nl.tudelft.ipv8.attestation.revocation.AuthoritySQLiteStore import nl.tudelft.ipv8.attestation.trustchain.* import nl.tudelft.ipv8.attestation.trustchain.store.TrustChainSQLiteStore import nl.tudelft.ipv8.attestation.trustchain.store.TrustChainStore import nl.tudelft.ipv8.attestation.trustchain.validation.TransactionValidator import nl.tudelft.ipv8.attestation.trustchain.validation.ValidationResult -import nl.tudelft.ipv8.attestation.wallet.AttestationCommunity -import nl.tudelft.ipv8.attestation.wallet.AttestationSQLiteStore import nl.tudelft.ipv8.attestation.wallet.cryptography.bonehexact.BonehPrivateKey +import nl.tudelft.ipv8.attestation.wallet.store.AttestationSQLiteStore import nl.tudelft.ipv8.keyvault.PrivateKey import nl.tudelft.ipv8.keyvault.defaultCryptoProvider import nl.tudelft.ipv8.messaging.tftp.TFTPCommunity @@ -48,6 +51,7 @@ import nl.tudelft.trustchain.gossipML.RecommenderCommunity import nl.tudelft.trustchain.gossipML.db.RecommenderStore import nl.tudelft.trustchain.peerchat.community.PeerChatCommunity import nl.tudelft.trustchain.peerchat.db.PeerChatStore +import nl.tudelft.trustchain.ssi.Communication import nl.tudelft.trustchain.voting.VotingCommunity import nl.tudelft.gossipML.sqldelight.Database as MLDatabase @@ -70,7 +74,6 @@ class TrustChainApplication : Application() { createEuroTokenCommunity(), createTFTPCommunity(), createDemoCommunity(), - createWalletCommunity(), createMarketCommunity(), createCoinCommunity(), createVotingCommunity(), @@ -86,6 +89,7 @@ class TrustChainApplication : Application() { .setServiceClass(TrustChainService::class.java) .init() + initCommunicationManager() initWallet() initTrustChain() } @@ -100,6 +104,26 @@ class TrustChainApplication : Application() { } } + private fun initCommunicationManager() { + val driver: SqlDriver = AndroidSqliteDriver(Database.Schema, this, "wallet.db") + val database = Database(driver) + val authorityStore = AuthoritySQLiteStore(database) + val attestationStore = AttestationSQLiteStore(database) + val identityStore = IdentitySQLiteStore(database) + + val authorityManager = AuthorityManager(authorityStore) + val communicationManager = CommunicationManager( + IPv8Android.getInstance(), + attestationStore, + identityStore, + authorityManager, + ::storePseudonym, + ::loadPseudonym + ) + + Communication.Factory(communicationManager) + } + private fun initTrustChain() { val ipv8 = IPv8Android.getInstance() val trustchain = ipv8.getOverlay()!! @@ -170,18 +194,6 @@ class TrustChainApplication : Application() { ) } - private fun createWalletCommunity(): OverlayConfiguration { - val driver: SqlDriver = AndroidSqliteDriver(Database.Schema, this, "wallet.db") - val database = Database(driver) - val store = AttestationSQLiteStore(database) - val randomWalk = RandomWalk.Factory() - - return OverlayConfiguration( - AttestationCommunity.Factory(store), - listOf(randomWalk) - ) - } - private fun createDiscoveryCommunity(): OverlayConfiguration { val randomWalk = RandomWalk.Factory() val randomChurn = RandomChurn.Factory() @@ -339,6 +351,23 @@ class TrustChainApplication : Application() { } } + private fun loadPseudonym(name: String): PrivateKey? { + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + val privateKey = prefs.getString(name, null) + return if (privateKey == null) { + null + } else { + AndroidCryptoProvider.keyFromPrivateBin(privateKey.hexToBytes()) + } + } + + private fun storePseudonym(name: String, privateKey: PrivateKey) { + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + prefs.edit() + .putString(name, privateKey.keyToBin().toHex()) + .apply() + } + companion object { private const val PREF_PRIVATE_KEY = "private_key" private const val PREF_ID_METADATA_KEY = "id_metadata" diff --git a/common/src/main/java/nl/tudelft/trustchain/common/BaseActivity.kt b/common/src/main/java/nl/tudelft/trustchain/common/BaseActivity.kt index 015dec89a..560c98ab1 100644 --- a/common/src/main/java/nl/tudelft/trustchain/common/BaseActivity.kt +++ b/common/src/main/java/nl/tudelft/trustchain/common/BaseActivity.kt @@ -1,6 +1,7 @@ package nl.tudelft.trustchain.common import android.os.Bundle +import android.view.View import androidx.annotation.MenuRes import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible @@ -9,6 +10,7 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController +import com.google.android.material.bottomnavigation.BottomNavigationView import nl.tudelft.trustchain.common.databinding.ActivityBaseBinding import nl.tudelft.trustchain.common.util.viewBinding @@ -19,6 +21,8 @@ abstract class BaseActivity : AppCompatActivity() { AppBarConfiguration(setOf()) } + protected lateinit var bottomNavigation: BottomNavigationView + /** * The resource ID of the navigation graph. */ @@ -39,6 +43,7 @@ abstract class BaseActivity : AppCompatActivity() { setContentView(binding.root) + bottomNavigation = binding.bottomNavigation navController.setGraph(navigationGraph) // Setup ActionBar diff --git a/ig-ssi/build.gradle b/ig-ssi/build.gradle index a87dc1327..52f4c89ad 100644 --- a/ig-ssi/build.gradle +++ b/ig-ssi/build.gradle @@ -74,15 +74,18 @@ dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + implementation 'androidmads.library.qrgenearator:QRGenearator:1.0.4' + // Logging implementation 'io.github.microutils:kotlin-logging:1.7.7' implementation 'com.github.tony19:logback-android:2.0.0' implementation 'com.github.MattSkala:recyclerview-itemadapter:0.4' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Testing testImplementation 'junit:junit:4.12' diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/Communication.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/Communication.kt new file mode 100644 index 000000000..400f8d377 --- /dev/null +++ b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/Communication.kt @@ -0,0 +1,69 @@ +package nl.tudelft.trustchain.ssi + +import nl.tudelft.ipv8.attestation.communication.CommunicationChannel +import nl.tudelft.ipv8.attestation.communication.CommunicationManager + +const val DEFAULT_PSEUDONYM = "MY_PEER" +val DEFAULT_RENDEZVOUS_TOKEN = null + +object Communication { + + private var communicationManager: CommunicationManager? = null + private var activePseudonym: String? = null + private var activeRendezvousToken: String? = null + + val pseudonymLock = Object() + val tokenLock = Object() + + fun getInstance(): CommunicationManager { + return communicationManager + ?: throw IllegalStateException("CommunicationManager is not initialized.") + } + + fun load( + pseudonym: String = getActivePseudonym(), + rendezvous: String? = getActiveRendezvousToken() + ): CommunicationChannel { + var rendezvousToken = rendezvous + if (rendezvousToken == "") { + rendezvousToken = DEFAULT_RENDEZVOUS_TOKEN + } + if (rendezvousToken != activeRendezvousToken) { + setActiveRendezvousToken(rendezvousToken) + } + return getInstance().load(pseudonym, rendezvousToken) + } + + fun getActivePseudonym(): String { + return activePseudonym + ?: throw java.lang.IllegalStateException("ActivePseudonym is not initialized.") + } + + fun getActiveRendezvousToken(): String? { + return activeRendezvousToken + } + + fun setActivePseudonym(pseudonym: String) { + synchronized(pseudonymLock) { + this.activePseudonym = pseudonym + } + } + + fun setActiveRendezvousToken(token: String?) { + if (activeRendezvousToken != token) + synchronized(tokenLock) { + val manager = getInstance() + manager.unload(getActivePseudonym()) + this.activeRendezvousToken = token + this.load() + } + } + + class Factory(communicationManager: CommunicationManager) { + init { + Communication.communicationManager = communicationManager + activePseudonym = DEFAULT_PSEUDONYM + communicationManager.load(getActivePseudonym()) + } + } +} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/SSIMainActivity.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/SSIMainActivity.kt index 7606aab53..2a3fec926 100644 --- a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/SSIMainActivity.kt +++ b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/SSIMainActivity.kt @@ -1,135 +1,81 @@ package nl.tudelft.trustchain.ssi -import android.content.Context import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log import android.widget.* -import com.jaredrummler.blockingdialog.BlockingDialogManager +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import nl.tudelft.ipv8.Peer -import nl.tudelft.ipv8.android.IPv8Android -import nl.tudelft.ipv8.attestation.WalletAttestation -import nl.tudelft.ipv8.attestation.schema.* -import nl.tudelft.ipv8.attestation.wallet.AttestationCommunity +import nl.tudelft.ipv8.attestation.identity.datastructures.IdentityAttestation +import nl.tudelft.ipv8.util.toHex import nl.tudelft.trustchain.common.BaseActivity -import nl.tudelft.trustchain.ssi.dialogs.AttestationValueDialog -import org.json.JSONObject class SSIMainActivity : BaseActivity() { + override val navigationGraph = R.navigation.nav_graph_ssi + override val bottomNavigationMenu = R.menu.bottom_navigation_menu2 + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val community = IPv8Android.getInstance().getOverlay()!! - community.setAttestationRequestCallback(::attestationRequestCallback) - community.setAttestationRequestCompleteCallback(::attestationRequestCompleteCallbackWrapper) - community.setAttestationChunkCallback(::attestationChunkCallback) + val channel = Communication.load() + channel.revocationOverlay.setRevocationUpdateCallback(this::revocationUpdateCallback) + + Communication.getInstance().setAttestationCallback(this::attestationCallback) // Register own key as trusted authority. - community.trustedAuthorityManager.addTrustedAuthority(IPv8Android.getInstance().myPeer.publicKey) + channel.authorityManager.addTrustedAuthority(channel.myPeer.publicKey) + this.notificationHandler() } - private fun attestationChunkCallback(peer: Peer, i: Int) { - Log.i("ig-ssi", "Received attestation chunk $i from ${peer.mid}.") + @Suppress("UNUSED_PARAMETER") + fun attestationCallback(peer: Peer, attestation: IdentityAttestation) { Handler(Looper.getMainLooper()).post { Toast.makeText( applicationContext, - "Received attestation chunk $i from ${peer.mid}.", - Toast.LENGTH_SHORT + "Successfully received attestation from ${peer.mid}.", + Toast.LENGTH_LONG ) .show() } } - // Default callback, currently overwritten in DatabaseFragment. - private fun attestationRequestCompleteCallbackWrapper( - forPeer: Peer, - attributeName: String, - attestation: WalletAttestation, - attributeHash: ByteArray, - idFormat: String, - fromPeer: Peer?, - metaData: String?, - signature: ByteArray? - ) { - attestationRequestCompleteCallback( - forPeer, - attributeName, - attestation, - attributeHash, - idFormat, - fromPeer, - metaData, - signature, - applicationContext - ) + private fun notificationHandler() { + Handler(Looper.getMainLooper()).post { + lifecycleScope.launchWhenCreated { + while (isActive) { + val channel = Communication.load() + + val notificationAmount = + channel.verifyRequests.size + channel.attestationRequests.size + val notificationBadge = bottomNavigation.getOrCreateBadge(R.id.requestsFragment) + notificationBadge.number = notificationAmount + notificationBadge.isVisible = notificationBadge.number > 0 + + delay(100) + } + } + } } - @Suppress("UNUSED_PARAMETER") - private fun attestationRequestCallback( - peer: Peer, - attributeName: String, - metadata: String - ): ByteArray { - Log.i("ig-ssi", "Attestation: called") - val parsedMetadata = JSONObject(metadata) - val idFormat = parsedMetadata.optString("id_format", ID_METADATA) - val input = - BlockingDialogManager.getInstance() - .showAndWait(this, AttestationValueDialog(attributeName, idFormat)) - ?: throw RuntimeException("User cancelled dialog.") - Log.i("ig-ssi", "Signing attestation with value $input with format $idFormat.") + private fun attestationChunkCallback(peer: Peer, i: Int) { + Log.i("ig-ssi", "Received attestation chunk $i from ${peer.mid}.") Handler(Looper.getMainLooper()).post { Toast.makeText( applicationContext, - "Signing attestation for $attributeName for peer ${peer.mid} ...", - Toast.LENGTH_LONG + "Received attestation chunk $i from ${peer.mid}.", + Toast.LENGTH_SHORT ) .show() } - return when (idFormat) { - "id_metadata_range_18plus" -> byteArrayOf(input.toByte()) - else -> input.toByteArray() - } } - override val navigationGraph = R.navigation.nav_graph_ssi - override val bottomNavigationMenu = R.menu.bottom_navigation_menu2 -} - -@Suppress("UNUSED_PARAMETER") -fun attestationRequestCompleteCallback( - forPeer: Peer, - attributeName: String, - attestation: WalletAttestation, - attributeHash: ByteArray, - idFormat: String, - fromPeer: Peer?, - metaData: String?, - signature: ByteArray?, - context: Context -) { - if (fromPeer == null) { - Log.i( - "ig-ssi", - "Signed attestation for attribute $attributeName for peer ${forPeer.mid}." - ) - Handler(Looper.getMainLooper()).post { - Toast.makeText( - context, - "Successfully sent attestation for $attributeName to peer ${forPeer.mid}", - Toast.LENGTH_LONG - ) - .show() - } - } else { - Log.i( - "ig-ssi", - "Received attestation for attribute $attributeName with metadata: $metaData." - ) + private fun revocationUpdateCallback(publicKeyHash: ByteArray, version: Long, amount: Int) { Handler(Looper.getMainLooper()).post { Toast.makeText( - context, - "Received Attestation for $attributeName", + applicationContext, + "Received $amount revocation(s) from ${publicKeyHash.toHex()}, version $version.", Toast.LENGTH_LONG ) .show() diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/attestations/Callbacks.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/attestations/Callbacks.kt new file mode 100644 index 000000000..53e270c84 --- /dev/null +++ b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/attestations/Callbacks.kt @@ -0,0 +1,51 @@ +package nl.tudelft.trustchain.ssi.attestations + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.widget.Toast +import nl.tudelft.ipv8.Peer +import nl.tudelft.ipv8.attestation.wallet.cryptography.WalletAttestation + + +@Suppress("UNUSED_PARAMETER") +fun attestationRequestCompleteCallback( + forPeer: Peer, + attributeName: String, + attestation: WalletAttestation, + attributeHash: ByteArray, + idFormat: String, + fromPeer: Peer?, + metaData: String?, + signature: ByteArray?, + context: Context +) { + if (fromPeer == null) { + Log.i( + "ig-ssi", + "Signed attestation for attribute $attributeName for peer ${forPeer.mid}." + ) + Handler(Looper.getMainLooper()).post { + Toast.makeText( + context, + "Successfully sent attestation for $attributeName to peer ${forPeer.mid}", + Toast.LENGTH_LONG + ) + .show() + } + } else { + Log.i( + "ig-ssi", + "Received attestation for attribute $attributeName with metadata: $metaData." + ) + Handler(Looper.getMainLooper()).post { + Toast.makeText( + context, + "Received Attestation for $attributeName", + Toast.LENGTH_LONG + ) + .show() + } + } +} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/attestations/Consts.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/attestations/Consts.kt new file mode 100644 index 000000000..6b5e22c34 --- /dev/null +++ b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/attestations/Consts.kt @@ -0,0 +1,19 @@ +package nl.tudelft.trustchain.ssi.attestations + +object Metadata { + const val PRESENTATION = "presentation" + const val PRESENTATION_REQUEST = "presentation_request" + const val AUTHORITY = "authority" + const val ATTESTATION = "attestation" + const val ATTESTATION_HASH = "attestationHash" + const val POINTER = "pointer" + const val SIGNATURE = "signature" + const val METADATA = "metadata" + const val SUBJECT = "subject" + const val TIMESTAMP = "timestamp" + const val CHALLENGE = "challenge" + const val KEY_HASH = "keyHash" + const val ATTESTORS = "attestors" + const val RENDEZVOUS = "rendezvous" + +} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/database/DatabaseFragment.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/database/DatabaseFragment.kt deleted file mode 100644 index d5a1c9fb0..000000000 --- a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/database/DatabaseFragment.kt +++ /dev/null @@ -1,334 +0,0 @@ -package nl.tudelft.trustchain.ssi.database - -import android.content.DialogInterface -import android.graphics.Bitmap -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.view.View -import android.widget.LinearLayout -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.core.content.res.ResourcesCompat -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import com.mattskala.itemadapter.ItemAdapter -import kotlinx.android.synthetic.main.fragment_database.* -import kotlinx.coroutines.* -import nl.tudelft.ipv8.Peer -import nl.tudelft.ipv8.android.IPv8Android -import nl.tudelft.ipv8.attestation.WalletAttestation -import nl.tudelft.ipv8.attestation.schema.SchemaManager -import nl.tudelft.ipv8.attestation.wallet.AttestationCommunity -import nl.tudelft.ipv8.util.toHex -import nl.tudelft.trustchain.common.ui.BaseFragment -import nl.tudelft.trustchain.common.util.QRCodeUtils -import nl.tudelft.trustchain.common.util.viewBinding -import nl.tudelft.trustchain.ssi.R -import nl.tudelft.trustchain.ssi.attestationRequestCompleteCallback -import nl.tudelft.trustchain.ssi.databinding.FragmentDatabaseBinding -import nl.tudelft.trustchain.ssi.dialogs.PresentAttestationDialog -import org.json.JSONObject - -class DatabaseFragment : BaseFragment(R.layout.fragment_database) { - - private val adapter = ItemAdapter() - private val binding by viewBinding(FragmentDatabaseBinding::bind) - - lateinit var bitmap: Bitmap - - private val qrCodeUtils by lazy { - QRCodeUtils(requireContext()) - } - - private var areFABsVisible = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - adapter.registerRenderer( - DatabaseItemRenderer( - { - setDatabaseItemAction(it) - }, - { - RemoveAttestationDialog(it, ::loadDatabaseEntries).show( - parentFragmentManager, - this.tag - ) - } - ) - ) - IPv8Android.getInstance().getOverlay()!! - .setAttestationRequestCompleteCallback( - ::databaseAttestationRequestCompleteCallback - ) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.recyclerView.adapter = adapter - binding.recyclerView.layoutManager = LinearLayoutManager(context) - binding.recyclerView.addItemDecoration( - DividerItemDecoration( - context, - LinearLayout.VERTICAL - ) - ) - binding.myPublicKey.text = getIpv8().myPeer.mid - setQRCode() - setFABs() - loadDatabaseEntries() - } - - private fun loadDatabaseEntriesOnLoop() { - lifecycleScope.launchWhenStarted { - while (isActive) { - loadDatabaseEntries() - delay(1500) - } - } - } - - private fun loadDatabaseEntries() { - val attestationCommunity = - IPv8Android.getInstance().getOverlay()!! - val entries = attestationCommunity.database.getAllAttestations() - .mapIndexed { index, blob -> DatabaseItem(index, blob) }.sortedBy { - if (it.attestationBlob.metadata != null) { - return@sortedBy JSONObject(it.attestationBlob.metadata!!).optString("attribute") - } else { - return@sortedBy "" - } - } - - adapter.updateItems(entries) - databaseTitle.text = "Attestations" - txtAttestationCount.text = "${entries.size} entries" - val textColorResId = if (entries.isNotEmpty()) R.color.green else R.color.red - val textColor = ResourcesCompat.getColor(resources, textColorResId, null) - txtAttestationCount.setTextColor(textColor) - imgEmpty.isVisible = entries.isEmpty() - } - - private fun setDatabaseItemAction(it: DatabaseItem) { - var attributeName = "unknown attribute" - var attributeValue = "unknown attribute" - if (it.attestationBlob.metadata != null) { - val parsedMetadata = JSONObject(it.attestationBlob.metadata!!) - attributeName = parsedMetadata.optString("attribute", "unknown attribute") - attributeValue = parsedMetadata.optString("value", "unknown value") - } - val dialog = PresentAttestationDialog( - attributeName, - attributeValue - ) - dialog.show( - parentFragmentManager, - tag - ) - - // If the attestation contains no signature, show an error. - if (it.attestationBlob.signature == null) { - lifecycleScope.launch { - // Give ample time for the dialog to be rendered. - delay(500) - dialog.showError() - } - } else { - lifecycleScope.launch { - val metadata = it.attestationBlob.metadata - val manager = SchemaManager() - manager.registerDefaultSchemas() - val attestation = - manager.deserialize(it.attestationBlob.blob, it.attestationBlob.idFormat) - - val signature = it.attestationBlob.signature!!.toHex() - val attestorKey = it.attestationBlob.attestorKey!!.keyToBin().toHex() - val key = IPv8Android.getInstance().myPeer.publicKey.keyToBin().toHex() - - val data = JSONObject() - data.put("presentation", "attestation") - data.put("metadata", metadata) - data.put("attestationHash", attestation.getHash().toHex()) - data.put("signature", signature) - data.put("signee_key", key) - data.put("attestor_key", attestorKey) - Log.d( - "ig-ssi", - "Presenting Attestation as QRCode: Size ${data.toString().length}, Data: $data" - ) - val bitmap = withContext(Dispatchers.Default) { - qrCodeUtils.createQR(data.toString())!! - } - dialog.setQRCode(bitmap) - } - } - } - - private fun setFABs() { - binding.addAttestationFab.visibility = View.GONE - binding.addAuthorityFab.visibility = View.GONE - binding.scanAttestationFab.visibility = View.GONE - binding.addAttestationActionText.visibility = View.GONE - binding.addAuthorityActionTxt.visibility = View.GONE - binding.scanAttestationActionText.visibility = View.GONE - - binding.actionFab.setOnClickListener { - if (!areFABsVisible) { - binding.addAttestationFab.show() - binding.addAuthorityFab.show() - binding.scanAttestationFab.show() - binding.addAttestationActionText.visibility = View.VISIBLE - binding.addAuthorityActionTxt.visibility = View.VISIBLE - binding.scanAttestationActionText.visibility = View.VISIBLE - areFABsVisible = true - } else { - binding.addAttestationFab.hide() - binding.addAuthorityFab.hide() - binding.scanAttestationFab.hide() - binding.addAttestationActionText.visibility = View.GONE - binding.addAuthorityActionTxt.visibility = View.GONE - binding.scanAttestationActionText.visibility = View.GONE - areFABsVisible = false - } - } - - binding.addAttestationFab.setOnClickListener { - val bundle = bundleOf("qrCodeHint" to "Scan signee public key", "intent" to 0) - findNavController().navigate( - DatabaseFragmentDirections.actionDatabaseFragmentToVerificationFragment().actionId, - bundle - ) - } - - binding.addAuthorityFab.setOnClickListener { - val bundle = bundleOf("qrCodeHint" to "Scan signee public key", "intent" to 1) - findNavController().navigate( - DatabaseFragmentDirections.actionDatabaseFragmentToVerificationFragment().actionId, - bundle - ) - } - - binding.scanAttestationFab.setOnClickListener { - val bundle = bundleOf("qrCodeHint" to "Scan attestation", "intent" to 2) - findNavController().navigate( - DatabaseFragmentDirections.actionDatabaseFragmentToVerificationFragment().actionId, - bundle - ) - } - } - - private fun setQRCode() { - if (!this::bitmap.isInitialized) { - lifecycleScope.launch { - val data = JSONObject() - data.put("presentation", "authority") - val myPeer = IPv8Android.getInstance().myPeer - val publicKey = myPeer.publicKey.keyToBin().toHex() - data.put("public_key", publicKey) - IPv8Android.getInstance() - - bitmap = withContext(Dispatchers.Default) { - qrCodeUtils.createQR(data.toString(), 300)!! - } - try { - binding.qrCodePlaceHolder.visibility = View.GONE - binding.publicKeyQRCode.setImageBitmap(bitmap) - } catch (e: IllegalStateException) { - // This happens if we already switched screens. - } - } - } else { - binding.qrCodePlaceHolder.visibility = View.GONE - binding.publicKeyQRCode.setImageBitmap(bitmap) - } - - binding.publicKeyQRCode.setOnClickListener { - if (it.scaleX > 1) { - it.animate().scaleX(1f).scaleY(1f) - } else { - it.animate().scaleX(1.2f).scaleY(1.2f) - } - } - } - - @Suppress("UNUSED_PARAMETER") - private fun databaseAttestationRequestCompleteCallback( - forPeer: Peer, - attributeName: String, - attestation: WalletAttestation, - attributeHash: ByteArray, - idFormat: String, - fromPeer: Peer?, - metaData: String?, - signature: ByteArray? - ) { - attestationRequestCompleteCallback( - forPeer, - attributeName, - attestation, - attributeHash, - idFormat, - fromPeer, - metaData, - signature, - requireContext() - ) - Handler(Looper.getMainLooper()).post { - try { - loadDatabaseEntries() - } catch (e: NullPointerException) { - // We're no longer in this screen. - } - } - } -} - -class RemoveAttestationDialog(val item: DatabaseItem, val callback: (() -> Unit) = {}) : - DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { - return activity?.let { - // Use the Builder class for convenient dialog construction - val builder = AlertDialog.Builder(it) - builder.setView(view) - .setPositiveButton( - "Delete Attestation", - DialogInterface.OnClickListener { _, _ -> - IPv8Android.getInstance() - .getOverlay()!!.database.deleteAttestationByHash( - item.attestationBlob.attestationHash - ) - Toast.makeText( - requireContext(), - "Successfully deleted attestation", - Toast.LENGTH_LONG - ).show() - callback() - } - ) - .setNegativeButton( - R.string.cancel, - DialogInterface.OnClickListener { _, _ -> - Toast.makeText( - requireContext(), - "Cancelled deletion", - Toast.LENGTH_LONG - ).show() - } - ) - .setTitle("Delete Attestation permanently?") - .setIcon(R.drawable.ic_round_warning_amber_24) - .setMessage("Note: this action cannot be undone.") - // Create the AlertDialog object and return it - builder.create() - } ?: throw IllegalStateException("Activity cannot be null") - } -} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/database/DatabaseItem.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/database/DatabaseItem.kt deleted file mode 100644 index e92031659..000000000 --- a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/database/DatabaseItem.kt +++ /dev/null @@ -1,14 +0,0 @@ -package nl.tudelft.trustchain.ssi.database - -import com.mattskala.itemadapter.Item -import nl.tudelft.ipv8.attestation.wallet.AttestationBlob - -class DatabaseItem(val index: Int, val attestationBlob: AttestationBlob) : Item() { - override fun areItemsTheSame(other: Item): Boolean { - return other is DatabaseItem && this.attestationBlob.attestationHash.contentEquals(other.attestationBlob.attestationHash) - } - - override fun areContentsTheSame(other: Item): Boolean { - return false - } -} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/database/DatabaseItemRenderer.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/database/DatabaseItemRenderer.kt deleted file mode 100644 index aedf1b7cb..000000000 --- a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/database/DatabaseItemRenderer.kt +++ /dev/null @@ -1,45 +0,0 @@ -package nl.tudelft.trustchain.ssi.database - -import android.annotation.SuppressLint -import android.graphics.Color -import android.view.View -import com.mattskala.itemadapter.ItemLayoutRenderer -import kotlinx.android.synthetic.main.item_database.view.* -import nl.tudelft.ipv8.util.toHex -import nl.tudelft.trustchain.ssi.R -import org.json.JSONObject - -class DatabaseItemRenderer( - private val onItemClick: (DatabaseItem) -> Unit, - private val onRemoveButtonClick: (DatabaseItem) -> Unit, -) : ItemLayoutRenderer( - DatabaseItem::class.java -) { - - @SuppressLint("SetTextI18n") - override fun bindView(item: DatabaseItem, view: View) = with(view) { - if (item.attestationBlob.metadata != null) { - val metadata = JSONObject(item.attestationBlob.metadata!!) - attributeNameAndValue.text = - metadata.optString("attribute") + ": " + metadata.optString("value") - } else { - attributeNameAndValue.text = "MALFORMED ATTESTATION" - attributeNameAndValue.setTextColor(Color.RED) - } - hash.text = item.attestationBlob.attestationHash.copyOfRange(0, 20).toHex() - idformat.text = item.attestationBlob.idFormat - blob.text = item.attestationBlob.blob.copyOfRange(0, 20).toHex() - - removeButton.setOnClickListener { - onRemoveButtonClick(item) - } - - setOnClickListener { - onItemClick(item) - } - } - - override fun getLayoutResourceId(): Int { - return R.layout.item_database - } -} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/AttestationValueDialog.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/AttestationValueDialog.kt deleted file mode 100644 index edde4ef44..000000000 --- a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/AttestationValueDialog.kt +++ /dev/null @@ -1,62 +0,0 @@ -package nl.tudelft.trustchain.ssi.dialogs - -import android.annotation.SuppressLint -import android.content.DialogInterface -import android.os.Bundle -import android.text.Html -import android.text.InputType -import androidx.appcompat.app.AlertDialog -import com.google.android.material.textfield.TextInputEditText -import com.jaredrummler.blockingdialog.BlockingDialogFragment -import nl.tudelft.ipv8.attestation.schema.ID_METADATA_RANGE_18PLUS -import nl.tudelft.ipv8.attestation.schema.ID_METADATA_RANGE_UNDERAGE -import nl.tudelft.trustchain.ssi.R - -@SuppressLint("ValidFragment") -class AttestationValueDialog(private val attributeName: String, private val idFormat: String) : - BlockingDialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog? { - @Suppress("DEPRECATION") - return activity?.let { - val builder = AlertDialog.Builder(it) - val inflater = it.layoutInflater - val view = inflater.inflate(R.layout.attestation_value_dialog, null) - val attrInput = view.findViewById(R.id.value_input) - when (idFormat) { - ID_METADATA_RANGE_18PLUS -> attrInput.inputType = InputType.TYPE_CLASS_NUMBER - ID_METADATA_RANGE_UNDERAGE -> attrInput.inputType = InputType.TYPE_CLASS_NUMBER - else -> attrInput.inputType = InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE - } - - builder.setView(view) - .setPositiveButton( - R.string.fire, - null - ) - .setNegativeButton( - R.string.cancel, - DialogInterface.OnClickListener { _, _ -> - setResult("", true) - } - ) - .setTitle("Attestation Requested") - .setMessage(Html.fromHtml("An attestation has been requested for $attributeName with format $idFormat.")) - // Create the AlertDialog object and return it - val dialog = builder.create() - dialog.setOnShowListener { - val posBtn = dialog.getButton(AlertDialog.BUTTON_POSITIVE) - posBtn.setOnClickListener { - val inputValue = attrInput.text.toString() - if (inputValue != "") { - setResult(inputValue, false) - dialog.dismiss() - } else { - attrInput.error = "Enter a value." - } - } - } - dialog - } ?: throw IllegalStateException("Activity cannot be null") - } -} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/FireMissilesDialog.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/FireMissilesDialog.kt deleted file mode 100644 index b0cb3797b..000000000 --- a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/FireMissilesDialog.kt +++ /dev/null @@ -1,103 +0,0 @@ -package nl.tudelft.trustchain.ssi.dialogs - -import android.app.Dialog -import android.content.DialogInterface -import android.os.Bundle -import android.util.Log -import android.widget.ArrayAdapter -import android.widget.Spinner -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment -import com.google.android.material.textfield.TextInputEditText -import nl.tudelft.ipv8.Peer -import nl.tudelft.ipv8.android.IPv8Android -import nl.tudelft.ipv8.attestation.schema.ID_METADATA_BIG -import nl.tudelft.ipv8.attestation.schema.ID_METADATA_HUGE -import nl.tudelft.ipv8.attestation.wallet.AttestationCommunity -import nl.tudelft.trustchain.ssi.R -import java.util.* - -class FireMissilesDialog(private val peer: Peer) : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - return activity?.let { - // Use the Builder class for convenient dialog construction - val builder = AlertDialog.Builder(it) - val inflater = requireActivity().layoutInflater - val attestationCommunity = - IPv8Android.getInstance().getOverlay()!! - val view = inflater.inflate(R.layout.request_attestation_dialog, null) - val spinner = view.findViewById(R.id.idFormatSpinner) - val arrayAdapter = ArrayAdapter( - requireContext(), - R.layout.support_simple_spinner_dropdown_item, - attestationCommunity.schemaManager.getSchemaNames().sorted() - ) - arrayAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item) - spinner.adapter = arrayAdapter - spinner.setSelection(3, true) - - builder.setView(view) - .setPositiveButton( - R.string.fire, - null - ) - .setNegativeButton( - R.string.cancel, - DialogInterface.OnClickListener { _, _ -> } - ) - .setTitle("Request Attestation") - // Create the AlertDialog object and return it - val dialog = builder.create() - dialog.setOnShowListener { - val posBtn = dialog.getButton(AlertDialog.BUTTON_POSITIVE) - posBtn.setOnClickListener { - val attrInput = view.findViewById(R.id.attribute_input) - if (attrInput.text.toString() != "") { - val idFormat = spinner.selectedItem.toString() - val myPeer = IPv8Android.getInstance().myPeer - val key = when (idFormat) { - ID_METADATA_BIG -> myPeer.identityPrivateKeyBig - ID_METADATA_HUGE -> myPeer.identityPrivateKeyHuge - else -> myPeer.identityPrivateKeySmall - } - if (key == null) { - Log.e("ig-ssi", "Key was null on attestation request.") - dialog.dismiss() - AlertDialog.Builder(requireContext()) - .setTitle("Oops!") - .setMessage("The private keys are not fully initialized yet, try again in a few seconds.") // Specifying a listener allows you to take an action before dismissing the dialog. - .setPositiveButton( - "Ok" - ) { _, _ -> } - .setIcon(android.R.drawable.ic_dialog_alert) - .show() - } else { - attestationCommunity.requestAttestation( - peer, - attrInput.text.toString().toUpperCase(Locale.getDefault()), - key, - hashMapOf("id_format" to idFormat), - true - ) - Log.d( - "ig-ssi", - "Sending attestation for ${attrInput.text} to ${peer.mid}" - ) - dialog.dismiss() - Toast.makeText( - requireContext(), - "Requested attestation for ${attrInput.text} from ${peer.mid}", - Toast.LENGTH_LONG - ).show() - } - } else { - attrInput.error = "Please enter a claim name." - } - } - } - dialog - } ?: throw IllegalStateException("Activity cannot be null") - } -} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/PresentAttestationDialog.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/PresentAttestationDialog.kt deleted file mode 100644 index 8db560b1e..000000000 --- a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/PresentAttestationDialog.kt +++ /dev/null @@ -1,71 +0,0 @@ -package nl.tudelft.trustchain.ssi.dialogs - -import android.annotation.SuppressLint -import android.app.Dialog -import android.graphics.Bitmap -import android.os.Bundle -import android.text.Html -import android.view.View -import android.widget.ImageView -import android.widget.ProgressBar -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.core.view.isVisible -import androidx.fragment.app.DialogFragment -import nl.tudelft.trustchain.ssi.R - -class PresentAttestationDialog( - private val attributeName: String, - private val attributeValue: String -) : - DialogFragment() { - - private var mView: View? = null - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - return activity?.let { - val builder = AlertDialog.Builder(it) - val inflater = requireActivity().layoutInflater - - @SuppressLint("InflateParams") - val view = inflater.inflate(R.layout.present_attestation_dialog, null)!! - mView = view - builder.setView(mView) - - val dialog: Dialog - val title = "Attestation for ${attributeName.capitalize()}" - val message = "$attributeName: $attributeValue" - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - builder.setTitle(Html.fromHtml(title, Html.FROM_HTML_MODE_LEGACY)) - dialog = builder.create() - dialog.setMessage( - Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY) - ) - } else { - @Suppress("DEPRECATION") - builder.setTitle(Html.fromHtml(title)) - dialog = builder.create() - @Suppress("DEPRECATION") - dialog.setMessage( - Html.fromHtml(message) - ) - } - dialog - } ?: throw IllegalStateException("Activity cannot be null") - } - - fun setQRCode(bitmap: Bitmap) { - val progressBar = mView!!.findViewById(R.id.progressBar) - if (progressBar.isVisible) { - progressBar.visibility = View.GONE - } - mView!!.findViewById(R.id.qrCodeView).setImageBitmap(bitmap) - } - - fun showError() { - val textView = mView!!.findViewById(R.id.unsupportedAttestation) - val progressBar = mView!!.findViewById(R.id.progressBar) - progressBar.visibility = View.GONE - textView.visibility = View.VISIBLE - } -} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/VerifyAttestationDialog.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/VerifyAttestationDialog.kt deleted file mode 100644 index 6f031bb27..000000000 --- a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/dialogs/VerifyAttestationDialog.kt +++ /dev/null @@ -1,71 +0,0 @@ -package nl.tudelft.trustchain.ssi.dialogs - -import android.app.Dialog -import android.content.DialogInterface -import android.os.Bundle -import android.util.Log -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment -import com.google.android.material.textfield.TextInputEditText -import nl.tudelft.ipv8.IPv4Address -import nl.tudelft.ipv8.android.IPv8Android -import nl.tudelft.ipv8.attestation.wallet.AttestationBlob -import nl.tudelft.ipv8.attestation.wallet.AttestationCommunity -import nl.tudelft.trustchain.ssi.R - -class VerifyAttestationDialog(private val databaseBlob: AttestationBlob) : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - return activity?.let { - // Use the Builder class for convenient dialog construction - val builder = AlertDialog.Builder(it) - val inflater = requireActivity().layoutInflater - val attestationCommunity = - IPv8Android.getInstance().getOverlay()!! - val view = inflater.inflate(R.layout.verify_attestation_dialog, null) - builder.setView(view) - .setPositiveButton( - R.string.fire, - DialogInterface.OnClickListener { _, _ -> - val addressInput = - view.findViewById(R.id.peer_address_input).text.toString() - val attributeName = - view.findViewById(R.id.attribute_name_input).text.toString() - val input = addressInput.split(":").toTypedArray() - val ip = input[0] - val port = input[1].toInt() - var address: IPv4Address? = null - for (peer in attestationCommunity.getPeers()) { - if (peer.address.toString() == addressInput) { - address = IPv4Address(ip, port) - } - } - if (address == null) { - throw RuntimeException("IPv4 Address not found") - } - Log.d("ig-ssi", "Sending verify request") - attestationCommunity.verifyAttestationValues( - address, - databaseBlob.attestationHash, - arrayListOf(attributeName.toByteArray()), - ::verifyComplete, - databaseBlob.idFormat - ) - } - ) - - .setNegativeButton( - R.string.cancel, - DialogInterface.OnClickListener { _, _ -> } - ) - .setTitle("Request Attestation") - // Create the AlertDialog object and return it - builder.create() - } ?: throw IllegalStateException("Activity cannot be null") - } - - private fun verifyComplete(hash: ByteArray, values: List) { - Log.d("ig-ssi", "VerifyComplete for hash: $hash, with values:") - values.forEachIndexed { index, d -> Log.d("ig-ssi", "Value $index: $d") } - } -} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/database/DatabaseFragment.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/database/DatabaseFragment.kt new file mode 100644 index 000000000..408c3c8c5 --- /dev/null +++ b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/database/DatabaseFragment.kt @@ -0,0 +1,328 @@ +package nl.tudelft.trustchain.ssi.ui.database + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.graphics.Bitmap +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.widget.LinearLayout +import android.widget.Toast +import androidmads.library.qrgenearator.QRGContents +import androidmads.library.qrgenearator.QRGEncoder +import androidx.core.content.res.ResourcesCompat +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.mattskala.itemadapter.ItemAdapter +import kotlinx.android.synthetic.main.fragment_database.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import nl.tudelft.ipv8.attestation.communication.DEFAULT_TIME_OUT +import nl.tudelft.ipv8.attestation.wallet.consts.Metadata.PUBLIC_KEY +import nl.tudelft.trustchain.common.ui.BaseFragment +import nl.tudelft.trustchain.common.util.viewBinding +import nl.tudelft.trustchain.ssi.Communication +import nl.tudelft.trustchain.ssi.R +import nl.tudelft.trustchain.ssi.attestations.Metadata.AUTHORITY +import nl.tudelft.trustchain.ssi.attestations.Metadata.PRESENTATION +import nl.tudelft.trustchain.ssi.attestations.Metadata.RENDEZVOUS +import nl.tudelft.trustchain.ssi.databinding.FragmentDatabaseBinding +import nl.tudelft.trustchain.ssi.ui.dialogs.attestation.PresentAttestationDialog +import nl.tudelft.trustchain.ssi.ui.dialogs.attestation.RemoveAttestationDialog +import nl.tudelft.trustchain.ssi.ui.dialogs.misc.RendezvousDialog +import nl.tudelft.trustchain.ssi.util.encodeB64 +import nl.tudelft.trustchain.ssi.util.formatAttestationToJSON +import nl.tudelft.trustchain.ssi.util.formatValueToJSON +import nl.tudelft.trustchain.ssi.util.parseHtml +import org.json.JSONObject + +const val QR_CODE_VALUE_LIMIT = 200 + +class DatabaseFragment : BaseFragment(R.layout.fragment_database) { + + private val adapter = ItemAdapter() + private val binding by viewBinding(FragmentDatabaseBinding::bind) + + lateinit var bitmap: Bitmap + + private var areFABsVisible = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + adapter.registerRenderer( + DatabaseItemRenderer( + { + setDatabaseItemAction(it) + }, + { + RemoveAttestationDialog(it, ::loadDatabaseEntries).show( + parentFragmentManager, + this.tag + ) + } + ) + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setListView() + setPeerInfo() + setFABs() + loadDatabaseEntriesOnLoop() + binding.refreshLayout.setOnRefreshListener { + loadDatabaseEntries() + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + menu.clear() + inflater.inflate(R.menu.database_options_menu, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_settings -> { + RendezvousDialog(callback = this::setPeerInfo).show(parentFragmentManager, "ig-ssi") + return true + } + R.id.action_drop -> { + val channel = Communication.load() + AlertDialog.Builder(context) + .setTitle("Delete Identity") + .setMessage(parseHtml("Are you sure you want to delete your identity?\n This action cannot be undone.")) // Specifying a listener allows you to take an action before dismissing the dialog. + .setPositiveButton( + android.R.string.yes + ) { _, _ -> + channel.deleteIdentity() + Toast.makeText( + requireContext(), + "Successfully cleared all attestations.", + Toast.LENGTH_SHORT + ).show() + } + .setNegativeButton( + android.R.string.no + ) { _, _ -> + Toast.makeText( + requireContext(), + "Cancelled deletion.", + Toast.LENGTH_SHORT + ).show() + } + .setIcon(android.R.drawable.ic_dialog_alert) + .show() + channel.deleteIdentity() + } + else -> { + + } + } + return super.onOptionsItemSelected(item) + } + + private fun setListView() { + binding.recyclerView.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.recyclerView.addItemDecoration( + DividerItemDecoration( + context, + LinearLayout.VERTICAL + ) + ) + } + + private fun loadDatabaseEntriesOnLoop() { + lifecycleScope.launchWhenStarted { + while (isActive) { + loadDatabaseEntries() + delay(100) + } + } + } + + private fun loadDatabaseEntries() { + val channel = + Communication.load() + val entries = channel.getOfflineVerifiableAttributes() + .mapIndexed { index, blob -> DatabaseItem(index, blob) } + + adapter.updateItems(entries) + databaseTitle.text = getString(R.string.credentials) + txtAttestationCount.text = "${entries.size} entries" + val textColorResId = if (entries.isNotEmpty()) R.color.green else R.color.red + val textColor = ResourcesCompat.getColor(resources, textColorResId, null) + txtAttestationCount.setTextColor(textColor) + imgEmpty.isVisible = entries.isEmpty() + refreshLayout.isRefreshing = false + } + + private fun setDatabaseItemAction(it: DatabaseItem) { + val attributeName = it.attestation.attributeName + val attributeValue = it.attestation.attributeValue + + val dialog = PresentAttestationDialog( + attributeName, + String(attributeValue) + ) + dialog.show( + parentFragmentManager, + tag + ) + val channel = Communication.load() + lifecycleScope.launch { + var firstRun = true + var waitDisabled = false + while (isActive) { + val challenge = channel.generateChallenge() + + var secondaryQRCode: Bitmap? = null + val presentation: String + if (it.attestation.attributeValue.size > QR_CODE_VALUE_LIMIT) { + presentation = + formatAttestationToJSON(it.attestation, channel.myPeer.publicKey, challenge) + val secondaryPresentation = + formatValueToJSON( + it.attestation.attributeValue, + challenge + ) + secondaryQRCode = + QRGEncoder( + secondaryPresentation, + null, + QRGContents.Type.TEXT, + 1000 + ).bitmap + } else { + presentation = formatAttestationToJSON( + it.attestation, + channel.myPeer.publicKey, + challenge, + it.attestation.attributeValue + ) + } + Log.d( + "ig-ssi", + "Presenting Attestation as QRCode: Size ${ + presentation.length + }, Data: $presentation" + ) + val bitmap = withContext(Dispatchers.Default) { + QRGEncoder( + presentation, + null, + QRGContents.Type.TEXT, + 1000 + ).bitmap + } + if (!firstRun && !waitDisabled) { + delay(DEFAULT_TIME_OUT) + } else { + waitDisabled = false + } + dialog.setQRCodes(bitmap, secondaryQRCode) + dialog.startTimeout(DEFAULT_TIME_OUT) + if (firstRun) { + firstRun = false + waitDisabled = true + delay(DEFAULT_TIME_OUT) + } + } + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun setFABs() { + binding.addAttestationFab.visibility = View.GONE + binding.addAuthorityFab.visibility = View.GONE + binding.scanAttestationFab.visibility = View.GONE + binding.addAttestationActionText.visibility = View.GONE + binding.addAuthorityActionTxt.visibility = View.GONE + binding.scanAttestationActionText.visibility = View.GONE + + binding.actionFab.setOnClickListener { + if (!areFABsVisible) { + binding.addAttestationFab.show() + binding.addAuthorityFab.show() + binding.scanAttestationFab.show() + binding.addAttestationActionText.visibility = View.VISIBLE + binding.addAuthorityActionTxt.visibility = View.VISIBLE + binding.scanAttestationActionText.visibility = View.VISIBLE + areFABsVisible = true + } else { + binding.addAttestationFab.hide() + binding.addAuthorityFab.hide() + binding.scanAttestationFab.hide() + binding.addAttestationActionText.visibility = View.GONE + binding.addAuthorityActionTxt.visibility = View.GONE + binding.scanAttestationActionText.visibility = View.GONE + areFABsVisible = false + } + } + + binding.addAttestationFab.setOnClickListener { + val bundle = bundleOf("qrCodeHint" to "Scan signee public key", "intent" to 0) + findNavController().navigate( + DatabaseFragmentDirections.actionDatabaseFragmentToVerificationFragment().actionId, + bundle + ) + } + + binding.addAuthorityFab.setOnClickListener { + val bundle = bundleOf("qrCodeHint" to "Scan signee public key", "intent" to 1) + findNavController().navigate( + DatabaseFragmentDirections.actionDatabaseFragmentToVerificationFragment().actionId, + bundle + ) + } + + binding.scanAttestationFab.setOnClickListener { + val bundle = bundleOf("qrCodeHint" to "Scan attestation", "intent" to 2) + findNavController().navigate( + DatabaseFragmentDirections.actionDatabaseFragmentToVerificationFragment().actionId, + bundle + ) + } + } + + private fun setPeerInfo() { + binding.myPublicKey.text = Communication.load().myPeer.mid + binding.publicKeyQRCode.setOnClickListener { + if (it.scaleX > 1) { + it.animate().scaleX(1f).scaleY(1f) + } else { + it.animate().scaleX(1.2f).scaleY(1.2f) + } + } + + lifecycleScope.launch { + val data = JSONObject() + data.put(PRESENTATION, AUTHORITY) + val myPeer = Communication.load().myPeer + val publicKey = + encodeB64(myPeer.publicKey.keyToBin()) + data.put(PUBLIC_KEY, publicKey) + data.put(RENDEZVOUS, Communication.getActiveRendezvousToken()) + + bitmap = QRGEncoder(data.toString(), null, QRGContents.Type.TEXT, 1000).bitmap + try { + binding.qrCodePlaceHolder.visibility = View.GONE + binding.publicKeyQRCode.setImageBitmap(bitmap) + } catch (e: IllegalStateException) { + // This happens if we already switched screens. + } + } + } +} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/database/DatabaseItem.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/database/DatabaseItem.kt new file mode 100644 index 000000000..3dceb077c --- /dev/null +++ b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/database/DatabaseItem.kt @@ -0,0 +1,25 @@ +package nl.tudelft.trustchain.ssi.ui.database + +import com.mattskala.itemadapter.Item +import nl.tudelft.ipv8.attestation.communication.AttestationPresentation + +class DatabaseItem(val index: Int, val attestation: AttestationPresentation) : Item() { + + override fun equals(other: Any?): Boolean { + return other is DatabaseItem && this.index == other.index && this.attestation == other.attestation + } + + override fun areItemsTheSame(other: Item): Boolean { + return this == other + } + + override fun areContentsTheSame(other: Item): Boolean { + return this == other + } + + override fun hashCode(): Int { + var result = index + result = 31 * result + attestation.hashCode() + return result + } +} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/database/DatabaseItemRenderer.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/database/DatabaseItemRenderer.kt new file mode 100644 index 000000000..4e69a5ed2 --- /dev/null +++ b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/database/DatabaseItemRenderer.kt @@ -0,0 +1,55 @@ +package nl.tudelft.trustchain.ssi.ui.database + +import android.annotation.SuppressLint +import android.view.View +import com.mattskala.itemadapter.ItemLayoutRenderer +import kotlinx.android.synthetic.main.item_database.view.* +import nl.tudelft.ipv8.util.toHex +import nl.tudelft.trustchain.ssi.R +import nl.tudelft.trustchain.ssi.util.decodeImage +import nl.tudelft.trustchain.ssi.ui.dialogs.attestation.ID_PICTURE +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class DatabaseItemRenderer( + private val onItemClick: (DatabaseItem) -> Unit, + private val onRemoveButtonClick: (DatabaseItem) -> Unit, +) : ItemLayoutRenderer( + DatabaseItem::class.java +) { + + @SuppressLint("SetTextI18n") + override fun bindView(item: DatabaseItem, view: View) = with(view) { + val attestation = item.attestation + // if (attestation.attributeValue != null) { + if (attestation.attributeName == ID_PICTURE.toUpperCase(Locale.getDefault())) { + pictureView.setImageBitmap(decodeImage(String(attestation.attributeValue))) + pictureView.visibility = View.VISIBLE + } + attributeNameAndValue.text = + attestation.attributeName + ": " + String(attestation.attributeValue) + // } else { + // attributeNameAndValue.text = "MALFORMED ATTESTATION" + // attributeNameAndValue.setTextColor(Color.RED) + // } + hash.text = attestation.attributeHash.toHex() + idformat.text = attestation.idFormat + blob.text = SimpleDateFormat( + "MM/dd/yyyy", + Locale.getDefault() + ).format(Date(attestation.signDate.toLong() * 1000)).toString() + + removeButton.setOnClickListener { + onRemoveButtonClick(item) + } + + setOnClickListener { + onItemClick(item) + } + } + + override fun getLayoutResourceId(): Int { + return R.layout.item_database + } +} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/dialogs/attestation/ActiveAttestationVerificationDialog.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/dialogs/attestation/ActiveAttestationVerificationDialog.kt new file mode 100644 index 000000000..d3bbcfd45 --- /dev/null +++ b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/dialogs/attestation/ActiveAttestationVerificationDialog.kt @@ -0,0 +1,90 @@ +package nl.tudelft.trustchain.ssi.ui.dialogs.attestation + +import SuccessDialog +import android.app.Dialog +import android.content.Context +import android.graphics.BlendMode +import android.graphics.BlendModeColorFilter +import android.graphics.Color +import android.graphics.PorterDuff +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.View +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import nl.tudelft.ipv8.Peer +import nl.tudelft.trustchain.ssi.R +import nl.tudelft.trustchain.ssi.ui.dialogs.status.DangerDialog + +class ActiveAttestationVerificationDialog : + DialogFragment() { + private lateinit var mView: View + private lateinit var mContext: Context + private lateinit var peer: Peer + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return activity?.let { + // Use the Builder class for convenient dialog construction + val builder = AlertDialog.Builder(it) + val inflater = requireActivity().layoutInflater + + val view = inflater.inflate(R.layout.active_verification_dialog, null) + this.mView = view + this.mContext = requireContext() + + builder.setView(view) + .setNegativeButton( + R.string.cancel + ) { _, _ -> } + .setTitle("Locating Client") + // Create the AlertDialog object and return it + builder.create() + } ?: throw IllegalStateException("Activity cannot be null") + } + + fun challengePeer(peer: Peer) { + Handler(Looper.getMainLooper()).post { + this.peer = peer + mView.findViewById(R.id.loadingInformation).text = + getString(R.string.challenging_client) + val progressBar = mView.findViewById(R.id.progressBar) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + progressBar.indeterminateDrawable.colorFilter = + BlendModeColorFilter(Color.BLUE, BlendMode.SRC_IN) + } else { + @Suppress("DEPRECATION") + progressBar.indeterminateDrawable + .setColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN) + } + + this.dialog!!.setTitle("Sending Challenges") + } + } + + fun setResult(result: Boolean) { + Handler(Looper.getMainLooper()).post { + this.dismiss() + if (result) { + SuccessDialog().show( + parentFragmentManager, + "ig-ssi" + ) + } else { + DangerDialog().show(parentFragmentManager, "ig-ssi") + } + } + } + + fun cancel(cancellationMessage: String) { + Handler(Looper.getMainLooper()).post { + Toast.makeText(requireContext(), cancellationMessage, Toast.LENGTH_LONG).show() + this.dismiss() + } + } +} diff --git a/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/dialogs/attestation/AttestationValueDialog.kt b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/dialogs/attestation/AttestationValueDialog.kt new file mode 100644 index 000000000..c1efb36ea --- /dev/null +++ b/ig-ssi/src/main/java/nl/tudelft/trustchain/ssi/ui/dialogs/attestation/AttestationValueDialog.kt @@ -0,0 +1,201 @@ +package nl.tudelft.trustchain.ssi.ui.dialogs.attestation + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Bundle +import android.provider.MediaStore +import android.text.InputType +import android.view.View +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.google.android.material.textfield.TextInputEditText +import com.jaredrummler.blockingdialog.BlockingDialogFragment +import nl.tudelft.ipv8.attestation.common.consts.SchemaConstants.ID_METADATA_RANGE_18PLUS +import nl.tudelft.ipv8.attestation.common.consts.SchemaConstants.ID_METADATA_RANGE_UNDERAGE +import nl.tudelft.trustchain.ssi.R +import nl.tudelft.trustchain.ssi.util.decodeImage +import nl.tudelft.trustchain.ssi.util.encodeImage +import nl.tudelft.trustchain.ssi.util.parseHtml +import java.io.BufferedInputStream +import java.io.InputStream +import java.util.Locale + +@SuppressLint("ValidFragment") +class DirectAttestationValueDialog(private val attributeName: String, private val idFormat: String) : + BlockingDialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog? { + @Suppress("DEPRECATION") + return activity?.let { + val builder = AlertDialog.Builder(it) + val inflater = it.layoutInflater + val view = inflater.inflate(R.layout.attestation_value_dialog, null) + val attrInput = view.findViewById(R.id.value_input) + when (idFormat) { + ID_METADATA_RANGE_18PLUS -> attrInput.inputType = InputType.TYPE_CLASS_NUMBER + ID_METADATA_RANGE_UNDERAGE -> attrInput.inputType = InputType.TYPE_CLASS_NUMBER + else -> attrInput.inputType = InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE + } + + builder.setView(view) + .setPositiveButton( + R.string.fire, + null + ) + .setNegativeButton( + R.string.cancel + ) { _, _ -> + setResult("", true) + } + .setTitle("Attestation Requested") + .setMessage(parseHtml("An attestation has been requested for $attributeName with format $idFormat.")) + // Create the AlertDialog object and return it + val dialog = builder.create() + dialog.setOnShowListener { + val posBtn = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + posBtn.setOnClickListener { + val inputValue = attrInput.text.toString() + if (inputValue != "") { + setResult(inputValue, false) + dialog.dismiss() + } else { + attrInput.error = "Enter a value." + } + } + } + dialog + } ?: throw IllegalStateException("Activity cannot be null") + } +} + +class AttestationValueDialog( + private val attributeName: String, + private val idFormat: String, + private val requestedValue: String? = null, + private val callback: (value: String) -> Unit +) : + DialogFragment() { + + private lateinit var mView: View + private var image: Bitmap? = null + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == PICK_IMAGE) { + val imageView = mView.findViewById(R.id.picture_view) + if (data != null) { + val inputStream: InputStream? = + requireContext().contentResolver.openInputStream(data.data!!) + val bufferedInputStream = BufferedInputStream(inputStream) + val imageBM = BitmapFactory.decodeStream(bufferedInputStream) + val resizedBM = Bitmap.createScaledBitmap(imageBM, 100, 100, true) + imageView.setImageBitmap(imageBM) + imageView.visibility = View.VISIBLE + this.image = resizedBM + } + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return activity?.let { + val builder = AlertDialog.Builder(it) + val inflater = it.layoutInflater + val view = inflater.inflate(R.layout.attestation_value_dialog, null) + this.mView = view + + val attrInput = view.findViewById(R.id.value_input) + when (idFormat) { + ID_METADATA_RANGE_18PLUS -> attrInput.inputType = InputType.TYPE_CLASS_NUMBER + ID_METADATA_RANGE_UNDERAGE -> attrInput.inputType = InputType.TYPE_CLASS_NUMBER + else -> attrInput.inputType = InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE + } + + val imageView = view.findViewById(R.id.picture_view) + val selectImageButton = view.findViewById