Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tx execution with non-owners; adjust ledger signing flow for execution #2013

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/java/io/gnosis/safe/HeimdallApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ class HeimdallApplication : MultiDexApplication(), ComponentProvider {
}

companion object Companion {

const val LEDGER_EXECUTION = true

operator fun get(context: Context): ApplicationComponent {
return (context.applicationContext as ComponentProvider).get()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import android.os.Build
import android.os.ParcelUuid
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import io.gnosis.safe.ui.settings.owner.ledger.LedgerWrapper.chunkDataAPDU
import io.gnosis.safe.ui.settings.owner.ledger.LedgerWrapper.commandGetAddress
import io.gnosis.safe.ui.settings.owner.ledger.LedgerWrapper.commandSignMessage
import io.gnosis.safe.ui.settings.owner.ledger.LedgerWrapper.commandSignTx
import io.gnosis.safe.ui.settings.owner.ledger.LedgerWrapper.parseGetAddress
import io.gnosis.safe.ui.settings.owner.ledger.LedgerWrapper.parseSignMessage
import io.gnosis.safe.ui.settings.owner.ledger.LedgerWrapper.splitPath
import io.gnosis.safe.ui.settings.owner.ledger.LedgerWrapper.unwrapAPDU
import io.gnosis.safe.ui.settings.owner.ledger.LedgerWrapper.wrapAPDU
import io.gnosis.safe.ui.settings.owner.ledger.ble.ConnectionEventListener
import io.gnosis.safe.ui.settings.owner.ledger.ble.ConnectionManager
import io.gnosis.safe.ui.settings.owner.ledger.transport.LedgerException
import io.gnosis.safe.ui.settings.owner.ledger.transport.SerializeHelper
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
Expand All @@ -34,12 +35,11 @@ import kotlinx.coroutines.withTimeout
import pm.gnosis.crypto.utils.asEthereumAddressChecksumString
import pm.gnosis.model.Solidity
import pm.gnosis.utils.asEthereumAddress
import pm.gnosis.utils.hexToByteArray
import pm.gnosis.utils.nullOnThrow
import pm.gnosis.utils.toHexString
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.util.*
import java.util.LinkedList
import java.util.Queue
import java.util.UUID
import kotlin.coroutines.Continuation
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
Expand All @@ -59,12 +59,14 @@ class LedgerController(val context: Context) {
private set

private var deviceConnectedCallback: DeviceConnectedCallback? = null
private var addressContinuations: Queue<Continuation<Solidity.Address>> = LinkedList<Continuation<Solidity.Address>>()
private var signContinuations: Queue<Continuation<String>> = LinkedList<Continuation<String>>()
private var addressContinuations: Queue<Continuation<Solidity.Address>> = LinkedList()
private var signContinuations: Queue<Continuation<String>> = LinkedList()

var writeCharacteristic: BluetoothGattCharacteristic? = null
var notifyCharacteristic: BluetoothGattCharacteristic? = null

private var mtu: Int = 20

private fun loadDeviceCharacteristics() {
val characteristic = connectedDevice?.let {
ConnectionManager.servicesOnDevice(it)?.flatMap { service ->
Expand All @@ -88,11 +90,16 @@ class LedgerController(val context: Context) {
}

onDisconnect = {
Timber.d("onDisconnect()")
}

onCharacteristicRead = { _, characteristic -> }
onCharacteristicRead = { _, characteristic ->
Timber.d("onCharacteristicRead()")
}

onCharacteristicWrite = { _, characteristic -> }
onCharacteristicWrite = { _, characteristic ->
Timber.d("onCharacteristicWrite()")
}

onCharacteristicWriteError = { _, _, error ->
val addressContinuation = nullOnThrow {
Expand All @@ -103,7 +110,9 @@ class LedgerController(val context: Context) {
}
}

onMtuChanged = { _, mtu -> }
onMtuChanged = { _, mtu ->
this@LedgerController.mtu = mtu
}

onCharacteristicChanged = { _, characteristic ->

Expand Down Expand Up @@ -190,8 +199,8 @@ class LedgerController(val context: Context) {
}

private fun locationPermissionMissing() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.S
&& (!context.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION))
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.S
&& (!context.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION))

private fun blePermissionMissing() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& (!context.hasPermission(Manifest.permission.BLUETOOTH_SCAN) || !context.hasPermission(Manifest.permission.BLUETOOTH_CONNECT))
Expand Down Expand Up @@ -221,74 +230,22 @@ class LedgerController(val context: Context) {
ConnectionManager.unregisterListener(connectionEventListener)
}

fun getSignCommand(path: String, message: String): ByteArray {

val paths = splitPath(path)
val messageBytes = message.hexToByteArray()

val pathsData = ByteArray(paths.size)
paths.forEachIndexed { index, element ->
pathsData[index] = element
suspend fun getTxSignature(path: String, encodedTx: String): String = suspendCoroutine { continuation ->
val payload = commandSignTx(path, encodedTx)
val chunks = chunkDataAPDU(payload, 150)
chunks.forEach {
ConnectionManager.writeCharacteristic(connectedDevice!!, writeCharacteristic!!, it)
}

val commandData = mutableListOf<Byte>()
commandData.add(0xe0.toByte())
commandData.add(0x08.toByte())
commandData.add(0x00.toByte())
commandData.add(0x00.toByte())

val messageData = ByteArrayOutputStream()
SerializeHelper.writeUint32BE(messageData, messageBytes.size.toLong())
messageBytes.forEachIndexed { index, element ->
messageData.write(element.toInt())
}

commandData.add((paths.size + messageBytes.size + 4).toByte())
commandData.addAll(pathsData.toList())
commandData.addAll(messageData.toByteArray().toList())

// Command length should be 150 bytes length otherwise we should split
// it into chuncks. As we sign hashes we should be fine for now.
val command = commandData.toByteArray()
Timber.d("Sign command: ${command.toHexString()}")

if (command.size > 150) throw LedgerException(LedgerException.ExceptionReason.IO_ERROR, "invalid data format")

return command
signContinuations.add(continuation)
}

suspend fun getSignature(path: String, message: String): String = suspendCoroutine { continuation ->
ConnectionManager.writeCharacteristic(connectedDevice!!, writeCharacteristic!!, wrapAPDU(getSignCommand(path, message)))
ConnectionManager.writeCharacteristic(connectedDevice!!, writeCharacteristic!!, wrapAPDU(commandSignMessage(path, message)))
signContinuations.add(continuation)
}

fun getAddressCommand(path: String, displayVerificationDialog: Boolean = false, chainCode: Boolean = false): ByteArray {

val paths = splitPath(path)

val commandData = mutableListOf<Byte>()

val pathsData = ByteArray(1 + paths.size)
pathsData[0] = paths.size.toByte()

paths.forEachIndexed { index, element ->
pathsData[1 + index] = element
}

commandData.add(0xe0.toByte())
commandData.add(0x02.toByte())
commandData.add((if (displayVerificationDialog) 0x01.toByte() else 0x00.toByte()))
commandData.add((if (chainCode) 0x01.toByte() else 0x00.toByte()))
commandData.addAll(pathsData.toList())

val command = commandData.toByteArray()
Timber.d("Get address command: ${command.toHexString()}")

return command
}

private suspend fun getAddress(device: BluetoothDevice, path: String): Solidity.Address = suspendCancellableCoroutine { continuation ->
ConnectionManager.writeCharacteristic(device, writeCharacteristic!!, wrapAPDU(getAddressCommand(path)))
ConnectionManager.writeCharacteristic(device, writeCharacteristic!!, wrapAPDU(commandGetAddress(path)))
addressContinuations.add(continuation)
}

Expand Down Expand Up @@ -328,7 +285,6 @@ class LedgerController(val context: Context) {
fragment.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_CODE_BLE_PERMISSION)
} else {
fragment.requestPermissions(arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT), REQUEST_CODE_BLE_PERMISSION)

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ class LedgerDeviceListFragment : BaseViewBindingFragment<FragmentLedgerDeviceLis
enum class Mode {
ADDRESS_SELECTION,
CONFIRMATION,
REJECTION
REJECTION,
EXECUTION
}

private val navArgs by navArgs<LedgerDeviceListFragmentArgs>()
private val mode by lazy { Mode.valueOf(navArgs.mode) }
private val owner by lazy { navArgs.owner }
private val safeTxHash by lazy { navArgs.safeTxHash }
private val txHash by lazy { navArgs.txHash }

override fun screenId() = ScreenId.LEDGER_DEVICE_LIST

Expand Down Expand Up @@ -109,15 +110,24 @@ class LedgerDeviceListFragment : BaseViewBindingFragment<FragmentLedgerDeviceLis
findNavController().navigate(
LedgerDeviceListFragmentDirections.actionLedgerDeviceListFragmentToLedgerSignDialog(
owner!!,
safeTxHash!!
txHash!!,
Mode.CONFIRMATION.name
)
)
Mode.REJECTION ->
findNavController().navigate(
LedgerDeviceListFragmentDirections.actionLedgerDeviceListFragmentToLedgerSignDialog(
owner!!,
safeTxHash!!,
false
txHash!!,
Mode.REJECTION.name
)
)
Mode.EXECUTION ->
findNavController().navigate(
LedgerDeviceListFragmentDirections.actionLedgerDeviceListFragmentToLedgerSignDialog(
owner!!,
txHash!!,
Mode.EXECUTION.name
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ import javax.inject.Inject

class LedgerSignDialog : BaseBottomSheetDialogFragment<DialogLedgerSignBinding>() {

enum class Mode {
CONFIRMATION,
REJECTION,
EXECUTION
}

private val navArgs by navArgs<LedgerSignDialogArgs>()
private val confirmation by lazy { navArgs.confirmation }
private val mode by lazy { Mode.valueOf(navArgs.mode) }
private val owner by lazy { navArgs.owner.asEthereumAddress()!! }
private val safeTxHash by lazy { navArgs.safeTxHash }

Expand All @@ -43,7 +49,11 @@ class LedgerSignDialog : BaseBottomSheetDialogFragment<DialogLedgerSignBinding>(
super.onViewCreated(view, savedInstanceState)

with(binding) {
actionLabel.text = getString(if (confirmation) R.string.ledger_sign_confirm else R.string.ledger_sign_reject)
actionLabel.text = getString(when(mode) {
Mode.CONFIRMATION -> R.string.ledger_sign_confirm
Mode.REJECTION -> R.string.ledger_sign_reject
Mode.EXECUTION -> R.string.ledger_sign_execute
})
hash.text = viewModel.getPreviewHash(safeTxHash)
cancel.setOnClickListener {
navigateBack()
Expand All @@ -65,7 +75,7 @@ class LedgerSignDialog : BaseBottomSheetDialogFragment<DialogLedgerSignBinding>(
}
})

viewModel.getSignature(owner, safeTxHash)
viewModel.getSignature(mode, owner, safeTxHash)
}

override fun onStop() {
Expand All @@ -74,29 +84,45 @@ class LedgerSignDialog : BaseBottomSheetDialogFragment<DialogLedgerSignBinding>(
}

private fun navigateBack(signedSafeTxHash: String? = null) {
if (confirmation) {
findNavController().popBackStack(R.id.signingOwnerSelectionFragment, true)
signedSafeTxHash?.let {
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SELECTED_RESULT,
owner.asEthereumAddressString()
)
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SIGNED_RESULT,
it
)
when(mode) {
Mode.CONFIRMATION -> {
findNavController().popBackStack(R.id.signingOwnerSelectionFragment, true)
signedSafeTxHash?.let {
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SELECTED_RESULT,
owner.asEthereumAddressString()
)
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SIGNED_RESULT,
it
)
}
}
} else {
findNavController().popBackStack(R.id.signingOwnerSelectionFragment, true)
signedSafeTxHash?.let {
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SELECTED_RESULT,
owner.asEthereumAddressString()
)
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SIGNED_RESULT,
it
)
Mode.REJECTION -> {
findNavController().popBackStack(R.id.signingOwnerSelectionFragment, true)
signedSafeTxHash?.let {
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SELECTED_RESULT,
owner.asEthereumAddressString()
)
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SIGNED_RESULT,
it
)
}
}
Mode.EXECUTION -> {
findNavController().popBackStack(R.id.ledgerDeviceListFragment, true)
signedSafeTxHash?.let {
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SELECTED_RESULT,
owner.asEthereumAddressString()
)
findNavController().currentBackStackEntry?.savedStateHandle?.set(
SafeOverviewBaseFragment.OWNER_SIGNED_RESULT,
it
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ class LedgerSignViewModel

override fun initialState() = LedgerSignState(ViewAction.Loading(true))

fun getSignature(ownerAddress: Solidity.Address, safeTxHash: String) {
fun getSignature(mode: LedgerSignDialog.Mode, ownerAddress: Solidity.Address, txHash: String) {
safeLaunch {
val owner = credentialsRepository.owner(ownerAddress)!!
val signature = ledgerController.getSignature(
owner.keyDerivationPath!!,
safeTxHash
)
val signature = when (mode) {
LedgerSignDialog.Mode.EXECUTION -> {
ledgerController.getTxSignature(
owner.keyDerivationPath!!,
txHash
)
}
else -> {
ledgerController.getSignature(
owner.keyDerivationPath!!,
txHash
)
}
}
updateState {
LedgerSignState(Signature(signature))
}
Expand All @@ -37,7 +47,7 @@ class LedgerSignViewModel
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(safeTxHash.hexToByteArray())
val sha256hash = digest.fold("", { str, it -> str + "%02x".format(it) })
return sha256hash.toUpperCase()
return sha256hash.uppercase()
}

fun disconnectFromDevice() {
Expand Down