diff --git a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/AccountListAdapter.kt b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/AccountListAdapter.kt index dbb29d6..843ae22 100644 --- a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/AccountListAdapter.kt +++ b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/AccountListAdapter.kt @@ -15,12 +15,11 @@ import com.solanamobile.fakewallet.ui.setaccountname.SetAccountNameDialogFragmen class AccountListAdapter( private val onSignTransaction: (Account) -> Unit, + private val onSignMessage: (Account) -> Unit, private val onAccountNameUpdated: (Account, String) -> Unit ) : ListAdapter(AccountDiffCallback) { - class AccountViewHolder( + inner class AccountViewHolder( private val binding: ItemAccountBinding, - private val onSignTransaction: (Account) -> Unit, - private val onAccountNameUpdated: (Account, String) -> Unit ) : ViewHolder(binding.root) { private var account: Account? = null @@ -30,6 +29,11 @@ class AccountListAdapter( onSignTransaction(a) } } + binding.buttonSignMessage.setOnClickListener { + account?.let { a -> + onSignMessage(a) + } + } val activity = binding.root.context as FragmentActivity binding.buttonEditName.setOnClickListener { SetAccountNameDialogFragment( @@ -58,7 +62,7 @@ class AccountListAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { val binding = ItemAccountBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return AccountViewHolder(binding, onSignTransaction, onAccountNameUpdated) + return AccountViewHolder(binding) } override fun onBindViewHolder(holder: AccountViewHolder, position: Int) { diff --git a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/HasUnauthorizedSeedsAdapter.kt b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/HasUnauthorizedSeedsAdapter.kt index d24f35e..1c0697b 100644 --- a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/HasUnauthorizedSeedsAdapter.kt +++ b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/HasUnauthorizedSeedsAdapter.kt @@ -16,9 +16,8 @@ import com.solanamobile.fakewallet.databinding.ItemHasUnauthorizedSeedsBinding class HasUnauthorizedSeedsAdapter( private val onAuthorizeNewSeed: () -> Unit ) : ListAdapter(HasUnauthorizedSeedsDiffCallback) { - class HasUnauthorizedSeedsViewHolder( + inner class HasUnauthorizedSeedsViewHolder( private val binding: ItemHasUnauthorizedSeedsBinding, - private val onAuthorizeNewSeed: () -> Unit ) : RecyclerView.ViewHolder(binding.root) { init { binding.buttonAuthorizeSeed.setOnClickListener { @@ -39,7 +38,7 @@ class HasUnauthorizedSeedsAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HasUnauthorizedSeedsViewHolder { val binding = ItemHasUnauthorizedSeedsBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return HasUnauthorizedSeedsViewHolder(binding, onAuthorizeNewSeed) + return HasUnauthorizedSeedsViewHolder(binding) } override fun onBindViewHolder(holder: HasUnauthorizedSeedsViewHolder, position: Int) { diff --git a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainActivity.kt b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainActivity.kt index 7ab6110..ab4d920 100644 --- a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainActivity.kt +++ b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainActivity.kt @@ -47,6 +47,9 @@ class MainActivity : AppCompatActivity() { onSignTransaction = { seed, account -> viewModel.signFakeTransaction(seed.authToken, account) }, + onSignMessage = { seed, account -> + viewModel.signFakeMessage(seed.authToken, account) + }, onAccountNameUpdated = { seed, account, name -> viewModel.updateAccountName( seed.authToken, @@ -62,6 +65,9 @@ class MainActivity : AppCompatActivity() { }, onSignMaxTransactionsWithMaxSignatures = { seed -> viewModel.signMaxTransactionsWithMaxSignatures(seed.authToken) + }, + onSignMaxMessagesWithMaxSignatures = { seed -> + viewModel.signMaxMessagesWithMaxSignatures(seed.authToken) } ) val remainingSeedsAdapter = HasUnauthorizedSeedsAdapter( @@ -152,6 +158,12 @@ class MainActivity : AppCompatActivity() { @Suppress("deprecation") startActivityForResult(i, REQUEST_SIGN_TRANSACTIONS) } + is ViewModelEvent.SignMessages -> { + val i = Wallet.signMessages( + event.authToken, event.messages) + @Suppress("deprecation") + startActivityForResult(i, REQUEST_SIGN_MESSAGES) + } is ViewModelEvent.RequestPublicKeys -> { val i = Wallet.requestPublicKeys( event.authToken, event.derivationPaths) @@ -189,6 +201,16 @@ class MainActivity : AppCompatActivity() { viewModel.onSignTransactionsFailure(resultCode) } } + REQUEST_SIGN_MESSAGES -> { + try { + val result = Wallet.onSignMessagesResult(resultCode, data) + Log.d(TAG, "Message signed: signatures=$result") + viewModel.onSignMessagesSuccess(result) + } catch (e: Wallet.ActionFailedException) { + Log.e(TAG, "Message signing failed", e) + viewModel.onSignMessagesFailure(resultCode) + } + } REQUEST_GET_PUBLIC_KEYS -> { try { val result = Wallet.onRequestPublicKeysResult(resultCode, data) @@ -206,6 +228,7 @@ class MainActivity : AppCompatActivity() { private val TAG = MainActivity::class.simpleName private const val REQUEST_AUTHORIZE_SEED_ACCESS = 0 private const val REQUEST_SIGN_TRANSACTIONS = 1 - private const val REQUEST_GET_PUBLIC_KEYS = 2 + private const val REQUEST_SIGN_MESSAGES = 2 + private const val REQUEST_GET_PUBLIC_KEYS = 3 } } \ No newline at end of file diff --git a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainViewModel.kt b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainViewModel.kt index 41d0fcd..a7fba35 100644 --- a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainViewModel.kt +++ b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainViewModel.kt @@ -265,6 +265,48 @@ class MainViewModel( showErrorMessage(resultCode) } + fun signFakeMessage(@WalletContractV1.AuthToken authToken: Long, account: Account) { + val fakeMessage = byteArrayOf(1.toByte()) + viewModelScope.launch { + val message = SigningRequest(fakeMessage, listOf(account.derivationPath)) + _viewModelEvents.emit( + ViewModelEvent.SignMessages(authToken, arrayListOf(message)) + ) + } + } + + fun signMaxMessagesWithMaxSignatures(@WalletContractV1.AuthToken authToken: Long) { + signMMessagesWithNSignatures(authToken, maxSigningRequests, maxRequestedSignatures) + } + + private fun signMMessagesWithNSignatures( + @WalletContractV1.AuthToken authToken: Long, + m: Int, + n: Int + ) { + val signingRequests = (0 until m).map { i -> + val derivationPaths = (0 until n).map { j -> + Bip44DerivationPath.newBuilder() + .setAccount(BipLevel(i * maxRequestedSignatures + j, true)).build().toUri() + } + SigningRequest(byteArrayOf(i.toByte()), derivationPaths) + } + + viewModelScope.launch { + _viewModelEvents.emit( + ViewModelEvent.SignMessages(authToken, ArrayList(signingRequests)) + ) + } + } + + fun onSignMessagesSuccess(signatures: List) { + showMessage("Messages signed successfully") + } + + fun onSignMessagesFailure(resultCode: Int) { + showErrorMessage(resultCode) + } + fun requestPublicKeys(@WalletContractV1.AuthToken authToken: Long) { requestMPublicKeys(authToken, maxRequestedPublicKeys) } @@ -397,6 +439,11 @@ sealed interface ViewModelEvent { val transactions: ArrayList, ) : ViewModelEvent + data class SignMessages( + @WalletContractV1.AuthToken val authToken: Long, + val messages: ArrayList, + ) : ViewModelEvent + data class RequestPublicKeys( @WalletContractV1.AuthToken val authToken: Long, val derivationPaths: ArrayList, diff --git a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/SeedListAdapter.kt b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/SeedListAdapter.kt index 0af6197..7eac046 100644 --- a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/SeedListAdapter.kt +++ b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/SeedListAdapter.kt @@ -20,10 +20,12 @@ class SeedListAdapter( private val lifecycleScope: CoroutineScope, private val implementationLimits: Flow, private val onSignTransaction: (Seed, Account) -> Unit, + private val onSignMessage: (Seed, Account) -> Unit, private val onAccountNameUpdated: (Seed, Account, String) -> Unit, private val onDeauthorizeSeed: (Seed) -> Unit, private val onRequestPublicKeys: (Seed) -> Unit, - private val onSignMaxTransactionsWithMaxSignatures: (Seed) -> Unit + private val onSignMaxTransactionsWithMaxSignatures: (Seed) -> Unit, + private val onSignMaxMessagesWithMaxSignatures: (Seed) -> Unit ) : ListAdapter(SeedDiffCallback) { data class ImplementationLimits( val maxSigningRequests: Int, @@ -42,6 +44,11 @@ class SeedListAdapter( onSignTransaction(s, account) } }, + onSignMessage = { account -> + seed?.let { s -> + onSignMessage(s, account) + } + }, onAccountNameUpdated = { account, name -> seed?.let { s -> onAccountNameUpdated(s, account, name) @@ -58,6 +65,12 @@ class SeedListAdapter( it.maxSigningRequests, it.maxRequestedSignatures ) + binding.buttonSignMaxMessagesWithMaxSignatures.text = + binding.root.context.getString( + R.string.action_sign_max_messages_with_max_signatures, + it.maxSigningRequests, + it.maxRequestedSignatures + ) binding.buttonRequestPublicKeys.text = binding.root.context.getString( R.string.action_request_public_keys, it.firstRequestedPublicKey, @@ -81,6 +94,11 @@ class SeedListAdapter( onSignMaxTransactionsWithMaxSignatures(s) } } + binding.buttonSignMaxMessagesWithMaxSignatures.setOnClickListener { + seed?.let { s -> + onSignMaxMessagesWithMaxSignatures(s) + } + } binding.recyclerviewAccounts.adapter = adapter } diff --git a/fakewallet/src/main/res/layout/item_account.xml b/fakewallet/src/main/res/layout/item_account.xml index aaf3545..9b095b7 100644 --- a/fakewallet/src/main/res/layout/item_account.xml +++ b/fakewallet/src/main/res/layout/item_account.xml @@ -96,14 +96,38 @@ app:layout_constraintEnd_toEndOf="parent" android:textSize="10pt" /> + + + + \ No newline at end of file diff --git a/fakewallet/src/main/res/layout/item_seed.xml b/fakewallet/src/main/res/layout/item_seed.xml index 765df63..685d871 100644 --- a/fakewallet/src/main/res/layout/item_seed.xml +++ b/fakewallet/src/main/res/layout/item_seed.xml @@ -78,13 +78,21 @@ app:layout_constraintTop_toBottomOf="@id/button_request_public_keys" android:textSize="8pt" /> + + diff --git a/fakewallet/src/main/res/values/strings.xml b/fakewallet/src/main/res/values/strings.xml index 8df361a..aab25fa 100644 --- a/fakewallet/src/main/res/values/strings.xml +++ b/fakewallet/src/main/res/values/strings.xml @@ -8,9 +8,12 @@ Account: Public Key: Path: - Sign a fake transaction + Sign a: + transaction + message Get public keys for %1$s - %2$s Sign %1$d transactions x %2$d keys + Sign %1$d messages x %2$d keys (No seeds remaining to be authorized for PURPOSE_SIGN_SOLANA_TRANSACTION) Authorize another seed for PURPOSE_SIGN_SOLANA_TRANSACTION diff --git a/impl/src/main/AndroidManifest.xml b/impl/src/main/AndroidManifest.xml index b803c99..8966aed 100644 --- a/impl/src/main/AndroidManifest.xml +++ b/impl/src/main/AndroidManifest.xml @@ -61,6 +61,10 @@ + + + + diff --git a/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeViewModel.kt b/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeViewModel.kt index e3f0e8a..d4152ca 100644 --- a/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeViewModel.kt +++ b/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeViewModel.kt @@ -56,7 +56,8 @@ class AuthorizeViewModel : ViewModel() { _requests.emit(request) } } - WalletContractV1.ACTION_SIGN_TRANSACTION -> { + WalletContractV1.ACTION_SIGN_TRANSACTION, + WalletContractV1.ACTION_SIGN_MESSAGE -> { val authToken = getAuthTokenFromIntent(callerIntent) if (authToken == -1L) { Log.e(TAG, "No or invalid auth token provided; aborting...") @@ -66,11 +67,15 @@ class AuthorizeViewModel : ViewModel() { val signingRequests = callerIntent.getParcelableArrayListExtra(WalletContractV1.EXTRA_SIGNING_REQUEST) if (signingRequests == null || signingRequests.isEmpty()) { Log.e(TAG, "No or empty signing requests provided; aborting...") - completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_TRANSACTION) + completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_PAYLOAD) return } + val type = if (callerIntent.action == WalletContractV1.ACTION_SIGN_TRANSACTION) + AuthorizeRequestType.Signature.Type.Transaction + else + AuthorizeRequestType.Signature.Type.Message startAuthorization() - val request = AuthorizeRequest(AuthorizeRequestType.Transaction(authToken, signingRequests), callerActivity, callerUid) + val request = AuthorizeRequest(AuthorizeRequestType.Signature(type, authToken, signingRequests), callerActivity, callerUid) cachedRequest = request viewModelScope.launch { _requests.emit(request) @@ -173,10 +178,13 @@ sealed interface AuthorizeRequestType { val seedId: Long? = null ) : AuthorizeRequestType - data class Transaction( + data class Signature( + val type: Type, @WalletContractV1.AuthToken val authToken: Long, val transactions: List, - ) : AuthorizeRequestType + ) : AuthorizeRequestType { + enum class Type { Transaction, Message } + } data class PublicKey( @WalletContractV1.AuthToken val authToken: Long, diff --git a/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeFragment.kt b/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeFragment.kt index a53da2f..1a6adca 100644 --- a/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeFragment.kt +++ b/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeFragment.kt @@ -66,6 +66,7 @@ class AuthorizeFragment : Fragment() { binding.labelAuthorizationType.setText(when (uiState.authorizationType) { AuthorizeUiState.AuthorizationType.SEED -> R.string.label_authorize_seed AuthorizeUiState.AuthorizationType.TRANSACTION -> R.string.label_authorize_transaction + AuthorizeUiState.AuthorizationType.MESSAGE -> R.string.label_authorize_message AuthorizeUiState.AuthorizationType.PUBLIC_KEY -> R.string.label_authorize_public_key null -> android.R.string.unknownName }) diff --git a/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeViewModel.kt b/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeViewModel.kt index 9573008..d2fde3c 100644 --- a/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeViewModel.kt +++ b/impl/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeViewModel.kt @@ -50,7 +50,7 @@ class AuthorizeViewModel private constructor( viewModelScope.launch { activityViewModel.requests.collect { request -> if (request.type !is AuthorizeRequestType.Seed && - request.type !is AuthorizeRequestType.Transaction && + request.type !is AuthorizeRequestType.Signature && request.type !is AuthorizeRequestType.PublicKey) { // Any other request types should only be observed transiently, whilst the // activity state is being updated. @@ -86,8 +86,12 @@ class AuthorizeViewModel private constructor( normalizedDerivationPaths = null } - is AuthorizeRequestType.Transaction -> { - authorizationType = AuthorizeUiState.AuthorizationType.TRANSACTION + is AuthorizeRequestType.Signature -> { + authorizationType = + if (request.type.type == AuthorizeRequestType.Signature.Type.Transaction) + AuthorizeUiState.AuthorizationType.TRANSACTION + else + AuthorizeUiState.AuthorizationType.MESSAGE val authKey = SeedRepository.AuthorizationKey( request.requestorUid, request.type.authToken @@ -105,7 +109,7 @@ class AuthorizeViewModel private constructor( this@AuthorizeViewModel.purpose = purpose if (request.type.transactions.any { t -> t.payload.isEmpty() }) { Log.e(TAG, "Only non-empty transaction payloads can be signed") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_TRANSACTION) + activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_PAYLOAD) return@collect } val numTransactions = request.type.transactions.size @@ -269,7 +273,7 @@ class AuthorizeViewModel private constructor( } } - is AuthorizeRequestType.Transaction -> { + is AuthorizeRequestType.Signature -> { val normalizedDerivationPaths = normalizedDerivationPaths!! viewModelScope.launch { @@ -281,7 +285,18 @@ class AuthorizeViewModel private constructor( try { withContext(Dispatchers.Default) { val privateKey = bipDerivationUseCase.derivePrivateKey(purpose, seed, path) - SignTransactionUseCase(purpose, privateKey, sr.payload) + if (request.type.type == AuthorizeRequestType.Signature.Type.Transaction) + SignPayloadUseCase.signTransaction( + purpose, + privateKey, + sr.payload + ) + else + SignPayloadUseCase.signMessage( + purpose, + privateKey, + sr.payload + ) } } catch (_: BipDerivationUseCase.KeyDoesNotExistException) { Log.e(TAG, "Key does not exist for $purpose:$path") @@ -356,6 +371,6 @@ data class AuthorizeUiState( val message: CharSequence? = null ) { enum class AuthorizationType { - SEED, TRANSACTION, PUBLIC_KEY + SEED, TRANSACTION, MESSAGE, PUBLIC_KEY } } \ No newline at end of file diff --git a/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Bip32UseCase.kt b/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Bip32UseCase.kt index 26c6186..30a02c6 100644 --- a/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Bip32UseCase.kt +++ b/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Bip32UseCase.kt @@ -63,7 +63,7 @@ object Ed25519Bip32UseCase { 0x5C.toByte(), 0xF5.toByte(), 0xD3.toByte(), 0xED.toByte())) } - @Size(SignTransactionUseCase.ED25519_SECRET_KEY_SIZE) + @Size(SignPayloadUseCase.ED25519_SECRET_KEY_SIZE) fun derivePrivateKey( seed: SeedDetails, bip32DerivationPath: Bip32DerivationPath @@ -82,7 +82,7 @@ object Ed25519Bip32UseCase { return privateKey } - @Size(SignTransactionUseCase.ED25519_PUBLIC_KEY_SIZE) + @Size(SignPayloadUseCase.ED25519_PUBLIC_KEY_SIZE) fun derivePublicKey( seed: SeedDetails, bip32DerivationPath: Bip32DerivationPath @@ -211,7 +211,7 @@ object Ed25519Bip32UseCase { // that it assumes the point is fully normalized, as the output of // scalarMultiplyByEd25519BasePoint is guaranteed to be. private fun isEd25519IdentityPointEncoded( - @Size(SignTransactionUseCase.ED25519_SECRET_KEY_SIZE) encodedPoint: ByteArray + @Size(SignPayloadUseCase.ED25519_SECRET_KEY_SIZE) encodedPoint: ByteArray ): Boolean { if (encodedPoint[0] != 1.toByte()) return false for (i in 1..31) { diff --git a/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Slip10UseCase.kt b/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Slip10UseCase.kt index 3bd16a5..c9f0ae6 100644 --- a/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Slip10UseCase.kt +++ b/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Slip10UseCase.kt @@ -16,8 +16,8 @@ import javax.crypto.spec.SecretKeySpec object Ed25519Slip10UseCase { private data class KeyDerivationMaterial( - @Size(SignTransactionUseCase.ED25519_SECRET_KEY_SIZE) val k: ByteArray, - @Size(SignTransactionUseCase.ED25519_SECRET_KEY_SIZE) val c: ByteArray, + @Size(SignPayloadUseCase.ED25519_SECRET_KEY_SIZE) val k: ByteArray, + @Size(SignPayloadUseCase.ED25519_SECRET_KEY_SIZE) val c: ByteArray, ) : BipDerivationUseCase.PartialPublicDerivation { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -43,7 +43,7 @@ object Ed25519Slip10UseCase { private const val MASTER_SECRET_MAC_KEY = "ed25519 seed" private const val MAC = "HmacSHA512" - @Size(SignTransactionUseCase.ED25519_SECRET_KEY_SIZE) + @Size(SignPayloadUseCase.ED25519_SECRET_KEY_SIZE) fun derivePrivateKey( seed: SeedDetails, bip32DerivationPath: Bip32DerivationPath @@ -58,7 +58,7 @@ object Ed25519Slip10UseCase { return keyPair.secretKey.asBytes } - @Size(SignTransactionUseCase.ED25519_PUBLIC_KEY_SIZE) + @Size(SignPayloadUseCase.ED25519_PUBLIC_KEY_SIZE) fun derivePublicKey( seed: SeedDetails, bip32DerivationPath: Bip32DerivationPath, @@ -74,7 +74,7 @@ object Ed25519Slip10UseCase { return keyPair.publicKey.asBytes } - @Size(SignTransactionUseCase.ED25519_PUBLIC_KEY_SIZE) + @Size(SignPayloadUseCase.ED25519_PUBLIC_KEY_SIZE) fun derivePublicKeyPartialDerivation( seed: SeedDetails, bip32DerivationPath: Bip32DerivationPath diff --git a/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignTransactionUseCase.kt b/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignPayloadUseCase.kt similarity index 70% rename from impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignTransactionUseCase.kt rename to impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignPayloadUseCase.kt index 1e3366c..6ac7205 100644 --- a/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignTransactionUseCase.kt +++ b/impl/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignPayloadUseCase.kt @@ -9,12 +9,12 @@ import com.solanamobile.seedvaultimpl.ApplicationDependencyContainer import com.solanamobile.seedvaultimpl.model.Authorization import com.goterl.lazysodium.interfaces.Sign -object SignTransactionUseCase { +object SignPayloadUseCase { const val ED25519_SECRET_KEY_SIZE = Sign.ED25519_SECRETKEYBYTES.toLong() const val ED25519_PUBLIC_KEY_SIZE = Sign.ED25519_PUBLICKEYBYTES.toLong() const val ED25519_SIGNATURE_SIZE = Sign.ED25519_BYTES.toLong() - operator fun invoke( + fun signTransaction( purpose: Authorization.Purpose, key: ByteArray, @Size(min=1) transaction: ByteArray @@ -30,6 +30,22 @@ object SignTransactionUseCase { } } + fun signMessage( + purpose: Authorization.Purpose, + key: ByteArray, + @Size(min=1) message: ByteArray + ): ByteArray { + require(message.isNotEmpty()) { "Message cannot be empty" } + + return when (purpose) { + Authorization.Purpose.SIGN_SOLANA_TRANSACTIONS -> { + // TODO: validate message is a Solana-compatible message before signing + require(key.size == ED25519_SECRET_KEY_SIZE.toInt()) { "Invalid private key for signing Solana messages" } + signEd25519(key, message) + } + } + } + @Size(ED25519_SIGNATURE_SIZE) private fun signEd25519( @Size(ED25519_SECRET_KEY_SIZE) key: ByteArray, diff --git a/impl/src/main/res/values/strings.xml b/impl/src/main/res/values/strings.xml index 8eb7379..9b8b278 100644 --- a/impl/src/main/res/values/strings.xml +++ b/impl/src/main/res/values/strings.xml @@ -47,6 +47,7 @@ Use password Don\'t allow Approve transaction + Apply signature Create new wallet Password not correct, please try again. You have %1$d attempt(s) remaining. diff --git a/seedvault/src/main/java/com/solanamobile/seedvault/PublicKeyResponse.java b/seedvault/src/main/java/com/solanamobile/seedvault/PublicKeyResponse.java index 2c8428f..3a71344 100644 --- a/seedvault/src/main/java/com/solanamobile/seedvault/PublicKeyResponse.java +++ b/seedvault/src/main/java/com/solanamobile/seedvault/PublicKeyResponse.java @@ -20,7 +20,7 @@ * The account public key generated in response to an {@link WalletContractV1#ACTION_GET_PUBLIC_KEY} * request. * - * @version 0.2.4 + * @version 0.2.5 */ @RequiresApi(api = Build.VERSION_CODES.M) // library minSdk is 17 public class PublicKeyResponse implements Parcelable { diff --git a/seedvault/src/main/java/com/solanamobile/seedvault/SeedVault.java b/seedvault/src/main/java/com/solanamobile/seedvault/SeedVault.java index 3fe8657..1b30a89 100644 --- a/seedvault/src/main/java/com/solanamobile/seedvault/SeedVault.java +++ b/seedvault/src/main/java/com/solanamobile/seedvault/SeedVault.java @@ -12,7 +12,7 @@ /** * Programming interfaces for interacting with the Seed Vault (non-Wallet interfaces) * - * @version 0.2.4 + * @version 0.2.5 */ @RequiresApi(api = Build.VERSION_CODES.M) // library minSdk is 17 public class SeedVault { diff --git a/seedvault/src/main/java/com/solanamobile/seedvault/SigningRequest.java b/seedvault/src/main/java/com/solanamobile/seedvault/SigningRequest.java index f4abd81..05f062b 100644 --- a/seedvault/src/main/java/com/solanamobile/seedvault/SigningRequest.java +++ b/seedvault/src/main/java/com/solanamobile/seedvault/SigningRequest.java @@ -21,7 +21,7 @@ /** * A request to sign a payload with the specified BIP derivation paths * - * @version 0.2.4 + * @version 0.2.5 */ @RequiresApi(api = Build.VERSION_CODES.M) // library minSdk is 17 public class SigningRequest implements Parcelable { diff --git a/seedvault/src/main/java/com/solanamobile/seedvault/SigningResponse.java b/seedvault/src/main/java/com/solanamobile/seedvault/SigningResponse.java index 9ba0817..7f22977 100644 --- a/seedvault/src/main/java/com/solanamobile/seedvault/SigningResponse.java +++ b/seedvault/src/main/java/com/solanamobile/seedvault/SigningResponse.java @@ -21,7 +21,7 @@ /** * The signatures generated in response to a {@link SigningRequest} * - * @version 0.2.4 + * @version 0.2.5 */ @RequiresApi(api = Build.VERSION_CODES.M) // library minSdk is 17 public class SigningResponse implements Parcelable { diff --git a/seedvault/src/main/java/com/solanamobile/seedvault/Wallet.java b/seedvault/src/main/java/com/solanamobile/seedvault/Wallet.java index d79275b..1411215 100644 --- a/seedvault/src/main/java/com/solanamobile/seedvault/Wallet.java +++ b/seedvault/src/main/java/com/solanamobile/seedvault/Wallet.java @@ -26,7 +26,7 @@ /** * Programming interfaces for {@link WalletContractV1} * - * @version 0.2.4 + * @version 0.2.5 */ @RequiresApi(api = Build.VERSION_CODES.R) // library minSdk is 17 public final class Wallet { @@ -176,6 +176,86 @@ public static ArrayList onSignTransactionsResult( return signingResponses; } + /** + * Request that the provided message be signed (with whatever method is appropriate for the + * purpose originally specified for this auth token). The returned {@link Intent} should be used + * with {@link Activity#startActivityForResult(Intent, int)}, and the result (as returned to + * {@link Activity#onActivityResult(int, int, Intent)}) should be used as parameters to + * {@link #onSignMessagesResult(int, Intent)}. + * @param authToken the auth token for the seed with which to perform message signing + * @param derivationPath a {@link BipDerivationPath} representing the account with which to + * sign this message + * @param message a {@code byte[]} containing the message to be signed + * @return an {@link Intent} suitable for usage with + * {@link Activity#startActivityForResult(Intent, int)} + */ + @NonNull + public static Intent signMessage( + @WalletContractV1.AuthToken long authToken, + @NonNull Uri derivationPath, + @NonNull byte[] message) { + final ArrayList paths = new ArrayList<>(1); + paths.add(derivationPath); + final ArrayList req = new ArrayList<>(1); + req.add(new SigningRequest(message, paths)); + return signMessages(authToken, req); + } + + /** + * Request that the provided messages be signed (with whatever method is appropriate for the + * purpose originally specified for this auth token). The returned {@link Intent} should be used + * with {@link Activity#startActivityForResult(Intent, int)}, and the result (as returned to + * {@link Activity#onActivityResult(int, int, Intent)}) should be used as parameters to + * {@link #onSignMessagesResult(int, Intent)}. + * @param authToken the auth token for the seed with which to perform message signing + * @param signingRequests the set of messages to be signed + * @return an {@link Intent} suitable for usage with + * {@link Activity#startActivityForResult(Intent, int)} + * @throws IllegalArgumentException if signingRequests is empty + */ + @NonNull + public static Intent signMessages( + @WalletContractV1.AuthToken long authToken, + @NonNull ArrayList signingRequests) { + if (signingRequests.isEmpty()) { + throw new IllegalArgumentException("signingRequests must not be empty"); + } + return new Intent() + .setPackage(WalletContractV1.PACKAGE_SEED_VAULT) + .setAction(WalletContractV1.ACTION_SIGN_MESSAGE) + .putExtra(WalletContractV1.EXTRA_AUTH_TOKEN, authToken) + .putParcelableArrayListExtra(WalletContractV1.EXTRA_SIGNING_REQUEST, + signingRequests); + } + + /** + * Process the results of {@link Activity#onActivityResult(int, int, Intent)} (in response to an + * invocation of {@link #signMessage(long, Uri, byte[])} or + * {@link #signMessages(long, ArrayList)}) + * @param resultCode resultCode from {@code onActivityResult} + * @param result intent from {@code onActivityResult} + * @return a {@link List} of {@link SigningResponse}s with the message signatures + * @throws ActionFailedException if message signing failed + */ + @NonNull + public static ArrayList onSignMessagesResult( + int resultCode, + @Nullable Intent result) throws ActionFailedException { + if (resultCode != Activity.RESULT_OK) { + throw new ActionFailedException("signMessages failed with result=" + resultCode); + } else if (result == null) { + throw new ActionFailedException("signMessages failed to return a result"); + } + + final ArrayList signingResponses = result.getParcelableArrayListExtra( + WalletContractV1.EXTRA_SIGNING_RESPONSE); + if (signingResponses == null) { + throw new ActionFailedException("signMessages returned no results"); + } + + return signingResponses; + } + /** * Request the public key for a given {@link BipDerivationPath} of a seed. The returned * {@link Intent} should be used with {@link Activity#startActivityForResult(Intent, int)}, and diff --git a/seedvault/src/main/java/com/solanamobile/seedvault/WalletContractV1.java b/seedvault/src/main/java/com/solanamobile/seedvault/WalletContractV1.java index c0c6d52..ed24ff0 100644 --- a/seedvault/src/main/java/com/solanamobile/seedvault/WalletContractV1.java +++ b/seedvault/src/main/java/com/solanamobile/seedvault/WalletContractV1.java @@ -20,7 +20,7 @@ /** * The programming contract for the Seed Vault Wallet API * - * @version 0.2.4 + * @version 0.2.5 */ @RequiresApi(api = Build.VERSION_CODES.M) // library minSdk is 17 public final class WalletContractV1 { @@ -78,7 +78,7 @@ public final class WalletContractV1 { *

If the Activity is cancelled for any reason, {@link android.app.Activity#RESULT_CANCELED} * will be returned. If the specified auth token is not valid, * {@link #RESULT_INVALID_AUTH_TOKEN} will be returned. If any transaction is not valid for - * signing with this auth token, {@link #RESULT_INVALID_TRANSACTION} will be returned. If any + * signing with this auth token, {@link #RESULT_INVALID_PAYLOAD} will be returned. If any * requested signature derivation path is not a valid BIP32 or BIP44 derivation path Uri, * {@link #RESULT_INVALID_DERIVATION_PATH} will be returned. If the user failed to authorize * signing the set of transactions, {@link #RESULT_AUTHENTICATION_FAILED} will be returned. If @@ -91,6 +91,32 @@ public final class WalletContractV1 { */ public static final String ACTION_SIGN_TRANSACTION = AUTHORITY_WALLET + ".ACTION_SIGN_TRANSACTION"; + /** + * Intent action to request that a set of messages be signed. The Intent should contain an + * {@link #EXTRA_SIGNING_REQUEST} extra {@link SigningRequest}s, one per message to be + * signed. Each {@link SigningRequest} may contain multiple requested signature BIP derivation + * paths. These derivation paths should be {@link Uri}s with a scheme of either + * {@link #BIP32_URI_SCHEME} or {@link #BIP44_URI_SCHEME}. The Intent should also contain an + * {@link #EXTRA_AUTH_TOKEN} extra specifying the authorized seed with which to sign. + *

On {@link android.app.Activity#RESULT_OK}, the resulting Intent will contain an + * {@link #EXTRA_SIGNING_RESPONSE} extra with {@link SigningResponse}s, one per + * {@link SigningRequest}. Each {@link SigningResponse} contains the requested signatures.

+ *

If the Activity is cancelled for any reason, {@link android.app.Activity#RESULT_CANCELED} + * will be returned. If the specified auth token is not valid, + * {@link #RESULT_INVALID_AUTH_TOKEN} will be returned. If any message is not valid for + * signing with this auth token, {@link #RESULT_INVALID_PAYLOAD} will be returned. If any + * requested signature derivation path is not a valid BIP32 or BIP44 derivation path Uri, + * {@link #RESULT_INVALID_DERIVATION_PATH} will be returned. If the user failed to authorize + * signing the set of messages, {@link #RESULT_AUTHENTICATION_FAILED} will be returned. If + * the number of {@link SigningRequest}s or the number of BIP derivation paths in a + * {@link SigningRequest} is more than the quantity supported by the Seed Vault implementation, + * {@link #RESULT_IMPLEMENTATION_LIMIT_EXCEEDED} will be returned.

+ * + * @see Bip32DerivationPath + * @see Bip44DerivationPath + */ + public static final String ACTION_SIGN_MESSAGE = AUTHORITY_WALLET + ".ACTION_SIGN_MESSAGE"; + /** * Intent action to request the public key for a set of accounts. The Intent should contain an * {@link #EXTRA_DERIVATION_PATH} extra with account BIP derivation path URIs, each with a @@ -130,7 +156,8 @@ public final class WalletContractV1 { * A transaction payload provided to {@link #ACTION_SIGN_TRANSACTION} was not valid for the * signing purpose associated with the corresponding {@link #EXTRA_AUTH_TOKEN} */ - public static final int RESULT_INVALID_TRANSACTION = RESULT_FIRST_USER + 1002; + public static final int RESULT_INVALID_PAYLOAD = RESULT_FIRST_USER + 1002; + public static final int RESULT_INVALID_TRANSACTION = RESULT_INVALID_PAYLOAD; // Legacy alias /** * The user declined to authenticate, or failed authentication, in response to an