lib: use isolated process

This commit is contained in:
Oscar Mira 2023-05-27 01:34:09 +02:00
parent 70231790ee
commit f0c425dca9
7 changed files with 65 additions and 52 deletions

View File

@ -8,7 +8,8 @@
<application android:usesCleartextTraffic="true"> <application android:usesCleartextTraffic="true">
<service <service
android:name=".WalletService" android:name=".WalletService"
android:exported="false" /> android:process=":wallet_service"
<!-- android:isolatedProcess="true" />--> android:isolatedProcess="true"
/>
</application> </application>
</manifest> </manifest>

View File

@ -1,13 +1,15 @@
package im.molly.monero; package im.molly.monero;
import im.molly.monero.IRemoteNodeClient;
import im.molly.monero.IStorageAdapter;
import im.molly.monero.IWalletServiceCallbacks; import im.molly.monero.IWalletServiceCallbacks;
import im.molly.monero.IWalletServiceListener; import im.molly.monero.IWalletServiceListener;
import im.molly.monero.SecretKey; import im.molly.monero.SecretKey;
import im.molly.monero.WalletConfig; import im.molly.monero.WalletConfig;
interface IWalletService { interface IWalletService {
oneway void createWallet(in WalletConfig config, in IWalletServiceCallbacks callback); oneway void createWallet(in WalletConfig config, in IStorageAdapter storage, in IRemoteNodeClient client, in IWalletServiceCallbacks callback);
oneway void restoreWallet(in WalletConfig config, in IWalletServiceCallbacks callback, in SecretKey spendSecretKey, long restorePoint); oneway void restoreWallet(in WalletConfig config, in IStorageAdapter storage, in IRemoteNodeClient client, in IWalletServiceCallbacks callback, in SecretKey spendSecretKey, long restorePoint);
oneway void openWallet(in WalletConfig config, in IWalletServiceCallbacks callback); oneway void openWallet(in WalletConfig config, in IStorageAdapter storage, in IRemoteNodeClient client, in IWalletServiceCallbacks callback);
void setListener(in IWalletServiceListener listener); void setListener(in IWalletServiceListener listener);
} }

View File

@ -10,22 +10,19 @@ import kotlinx.coroutines.sync.withLock
internal class StorageAdapter(var dataStore: WalletDataStore?) : IStorageAdapter.Stub() { internal class StorageAdapter(var dataStore: WalletDataStore?) : IStorageAdapter.Stub() {
private val logger = loggerFor<StorageAdapter>() private val ioStorageScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val storageScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val mutex = Mutex() private val mutex = Mutex()
override fun writeAsync(pfd: ParcelFileDescriptor?): Boolean { override fun writeAsync(pfd: ParcelFileDescriptor?): Boolean {
requireNotNull(pfd) requireNotNull(pfd)
val inputStream = ParcelFileDescriptor.AutoCloseInputStream(pfd)
val localDataStore = dataStore val localDataStore = dataStore
if (localDataStore == null) { if (localDataStore == null) {
logger.i("Unable to save wallet data because WalletDataStore is unset") pfd.close()
inputStream.close()
return false return false
} }
storageScope.launch { val inputStream = ParcelFileDescriptor.AutoCloseInputStream(pfd)
ioStorageScope.launch {
mutex.withLock { mutex.withLock {
localDataStore.write { output -> localDataStore.write { output ->
inputStream.copyTo(output) inputStream.copyTo(output)
@ -37,13 +34,13 @@ internal class StorageAdapter(var dataStore: WalletDataStore?) : IStorageAdapter
override fun readAsync(pfd: ParcelFileDescriptor?) { override fun readAsync(pfd: ParcelFileDescriptor?) {
requireNotNull(pfd) requireNotNull(pfd)
val outputStream = ParcelFileDescriptor.AutoCloseOutputStream(pfd)
val localDataStore = dataStore val localDataStore = dataStore
if (localDataStore == null) { if (localDataStore == null) {
outputStream.close() pfd.close()
throw IllegalArgumentException("WalletDataStore cannot be null") throw IllegalArgumentException("WalletDataStore cannot be null")
} }
storageScope.launch { val outputStream = ParcelFileDescriptor.AutoCloseOutputStream(pfd)
ioStorageScope.launch {
mutex.withLock { mutex.withLock {
localDataStore.read().use { input -> localDataStore.read().use { input ->
input.copyTo(outputStream) input.copyTo(outputStream)

View File

@ -6,6 +6,4 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
internal data class WalletConfig( internal data class WalletConfig(
val networkId: Int, val networkId: Int,
val storageAdapter: IStorageAdapter.Stub?,
val remoteNodeClient: IRemoteNodeClient.Stub?,
) : Parcelable ) : Parcelable

View File

@ -19,7 +19,7 @@ class WalletNative private constructor(
companion object { companion object {
// TODO: Find better name because this is a local synchronization wallet, not a full node wallet // TODO: Find better name because this is a local synchronization wallet, not a full node wallet
fun fullNode( suspend fun fullNode(
networkId: Int, networkId: Int,
storageAdapter: IStorageAdapter? = null, storageAdapter: IStorageAdapter? = null,
remoteNodeClient: IRemoteNodeClient? = null, remoteNodeClient: IRemoteNodeClient? = null,
@ -41,6 +41,7 @@ class WalletNative private constructor(
nativeRestoreAccount(handle, secretSpendKey.bytes, restorePointOrNow) nativeRestoreAccount(handle, secretSpendKey.bytes, restorePointOrNow)
tryWriteState() tryWriteState()
} }
else -> { else -> {
require(restorePoint == null) require(restorePoint == null)
readState() readState()
@ -57,10 +58,11 @@ class WalletNative private constructor(
private val handle: Long = nativeCreate(networkId) private val handle: Long = nativeCreate(networkId)
private fun tryWriteState(): Boolean { private suspend fun tryWriteState(): Boolean {
requireNotNull(storageAdapter) requireNotNull(storageAdapter)
return withContext(ioDispatcher) {
val pipe = ParcelFileDescriptor.createReliablePipe() val pipe = ParcelFileDescriptor.createReliablePipe()
return pipe[1].use { writeSide -> pipe[1].use { writeSide ->
val storageIsReady = storageAdapter.writeAsync(pipe[0]) val storageIsReady = storageAdapter.writeAsync(pipe[0])
if (storageIsReady) { if (storageIsReady) {
val result = nativeSave(handle, writeSide.fd) val result = nativeSave(handle, writeSide.fd)
@ -68,19 +70,27 @@ class WalletNative private constructor(
logger.e("Wallet data serialization failed") logger.e("Wallet data serialization failed")
} }
result result
} else false } else {
logger.i("Unable to save wallet data because WalletDataStore is unset")
false
}
}
} }
} }
private fun readState() { private suspend fun readState() {
requireNotNull(storageAdapter) requireNotNull(storageAdapter)
return withContext(ioDispatcher) {
val pipe = ParcelFileDescriptor.createReliablePipe() val pipe = ParcelFileDescriptor.createReliablePipe()
return pipe[0].use { readSide -> pipe[0].use { readSide ->
storageAdapter.readAsync(pipe[1]) pipe[1].use { writeSide ->
storageAdapter.readAsync(writeSide)
}
val result = nativeLoad(handle, readSide.fd) val result = nativeLoad(handle, readSide.fd)
check(result) { "Wallet data deserialization failed" } check(result) { "Wallet data deserialization failed" }
} }
} }
}
override fun getPrimaryAccountAddress() = nativeGetPrimaryAccountAddress(handle) override fun getPrimaryAccountAddress() = nativeGetPrimaryAccountAddress(handle)

View File

@ -6,7 +6,6 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.time.Instant
// TODO: Rename to SandboxedWalletProvider and extract interface, add InProcessWalletProvider // TODO: Rename to SandboxedWalletProvider and extract interface, add InProcessWalletProvider
class WalletProvider private constructor( class WalletProvider private constructor(
@ -54,10 +53,11 @@ class WalletProvider private constructor(
dataStore: WalletDataStore? = null, dataStore: WalletDataStore? = null,
client: RemoteNodeClient? = null, client: RemoteNodeClient? = null,
): MoneroWallet { ): MoneroWallet {
require(client == null || client.network == network)
val storageAdapter = StorageAdapter(dataStore) val storageAdapter = StorageAdapter(dataStore)
val wallet = suspendCancellableCoroutine { continuation -> val wallet = suspendCancellableCoroutine { continuation ->
service.createWallet( service.createWallet(
buildConfig(network, StorageAdapter(dataStore), client), buildConfig(network), storageAdapter, client,
WalletResultCallback(continuation), WalletResultCallback(continuation),
) )
} }
@ -71,10 +71,11 @@ class WalletProvider private constructor(
secretSpendKey: SecretKey, secretSpendKey: SecretKey,
restorePoint: RestorePoint, restorePoint: RestorePoint,
): MoneroWallet { ): MoneroWallet {
require(client == null || client.network == network)
val storageAdapter = StorageAdapter(dataStore) val storageAdapter = StorageAdapter(dataStore)
val wallet = suspendCancellableCoroutine { continuation -> val wallet = suspendCancellableCoroutine { continuation ->
service.restoreWallet( service.restoreWallet(
buildConfig(network, StorageAdapter(dataStore), client), buildConfig(network), storageAdapter, client,
WalletResultCallback(continuation), WalletResultCallback(continuation),
secretSpendKey, secretSpendKey,
restorePoint.heightOrTimestamp, restorePoint.heightOrTimestamp,
@ -88,23 +89,19 @@ class WalletProvider private constructor(
dataStore: WalletDataStore, dataStore: WalletDataStore,
client: RemoteNodeClient? = null, client: RemoteNodeClient? = null,
): MoneroWallet { ): MoneroWallet {
require(client == null || client.network == network)
val storageAdapter = StorageAdapter(dataStore) val storageAdapter = StorageAdapter(dataStore)
val wallet = suspendCancellableCoroutine { continuation -> val wallet = suspendCancellableCoroutine { continuation ->
service.openWallet( service.openWallet(
buildConfig(network, storageAdapter, client), buildConfig(network), storageAdapter, client,
WalletResultCallback(continuation), WalletResultCallback(continuation),
) )
} }
return MoneroWallet(wallet, storageAdapter, client) return MoneroWallet(wallet, storageAdapter, client)
} }
private fun buildConfig( private fun buildConfig(network: MoneroNetwork): WalletConfig {
network: MoneroNetwork, return WalletConfig(network.id)
storageAdapter: StorageAdapter,
remoteNodeClient: RemoteNodeClient?,
): WalletConfig {
require(remoteNodeClient == null || remoteNodeClient.network == network)
return WalletConfig(network.id, storageAdapter, remoteNodeClient)
} }
fun disconnect() { fun disconnect() {

View File

@ -42,12 +42,14 @@ internal class WalletServiceImpl(
override fun createWallet( override fun createWallet(
config: WalletConfig?, config: WalletConfig?,
storage: IStorageAdapter?,
client: IRemoteNodeClient?,
callback: IWalletServiceCallbacks?, callback: IWalletServiceCallbacks?,
) { ) {
serviceScope.launch { serviceScope.launch {
val secretSpendKey = randomSecretKey() val secretSpendKey = randomSecretKey()
val wallet = secretSpendKey.use { secret -> val wallet = secretSpendKey.use { secret ->
createOrRestoreWallet(config, secret) createOrRestoreWallet(config, storage, client, secret)
} }
callback?.onWalletResult(wallet) callback?.onWalletResult(wallet)
} }
@ -55,13 +57,15 @@ internal class WalletServiceImpl(
override fun restoreWallet( override fun restoreWallet(
config: WalletConfig?, config: WalletConfig?,
storage: IStorageAdapter?,
client: IRemoteNodeClient?,
callback: IWalletServiceCallbacks?, callback: IWalletServiceCallbacks?,
secretSpendKey: SecretKey?, secretSpendKey: SecretKey?,
restorePoint: Long, restorePoint: Long,
) { ) {
serviceScope.launch { serviceScope.launch {
val wallet = secretSpendKey.use { secret -> val wallet = secretSpendKey.use { secret ->
createOrRestoreWallet(config, secret, restorePoint) createOrRestoreWallet(config, storage, client, secret, restorePoint)
} }
callback?.onWalletResult(wallet) callback?.onWalletResult(wallet)
} }
@ -69,22 +73,26 @@ internal class WalletServiceImpl(
override fun openWallet( override fun openWallet(
config: WalletConfig?, config: WalletConfig?,
storage: IStorageAdapter?,
client: IRemoteNodeClient?,
callback: IWalletServiceCallbacks?, callback: IWalletServiceCallbacks?,
) { ) {
requireNotNull(config) requireNotNull(config)
serviceScope.launch { serviceScope.launch {
val wallet = WalletNative.fullNode( val wallet = WalletNative.fullNode(
networkId = config.networkId, networkId = config.networkId,
storageAdapter = config.storageAdapter, storageAdapter = storage,
remoteNodeClient = config.remoteNodeClient, remoteNodeClient = client,
coroutineContext = serviceScope.coroutineContext, coroutineContext = serviceScope.coroutineContext,
) )
callback?.onWalletResult(wallet) callback?.onWalletResult(wallet)
} }
} }
private fun createOrRestoreWallet( private suspend fun createOrRestoreWallet(
config: WalletConfig?, config: WalletConfig?,
storage: IStorageAdapter?,
client: IRemoteNodeClient?,
secretSpendKey: SecretKey?, secretSpendKey: SecretKey?,
restorePoint: Long? = null, restorePoint: Long? = null,
): IWallet { ): IWallet {
@ -92,8 +100,8 @@ internal class WalletServiceImpl(
requireNotNull(secretSpendKey) requireNotNull(secretSpendKey)
return WalletNative.fullNode( return WalletNative.fullNode(
networkId = config.networkId, networkId = config.networkId,
storageAdapter = config.storageAdapter, storageAdapter = storage,
remoteNodeClient = config.remoteNodeClient, remoteNodeClient = client,
secretSpendKey = secretSpendKey, secretSpendKey = secretSpendKey,
restorePoint = restorePoint, restorePoint = restorePoint,
coroutineContext = serviceScope.coroutineContext, coroutineContext = serviceScope.coroutineContext,