Skip to content

Commit

Permalink
Merge branch 'develop' into 'master'
Browse files Browse the repository at this point in the history
Develop

See merge request papers/airgap/airgap-vault!412
  • Loading branch information
godenzim committed Mar 14, 2023
2 parents 3dbc816 + 9dd916c commit dc745ca
Show file tree
Hide file tree
Showing 140 changed files with 8,084 additions and 1,305 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ $RECYCLE.BIN/
Thumbs.db
UserInterfaceState.xcuserstate

src/assets/libs/*.browserify.js
src/assets/protocol_modules/
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ COPY package.json /app
COPY yarn.lock /app
COPY apply-diagnostic-modules.js /app
COPY fix-qrscanner-gradle.js /app
COPY copy-builtin-modules.js /app

RUN yarn install-test-dependencies

Expand Down
4 changes: 4 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ dependencies {

def saplingVersion = "0.0.7"
implementation "com.github.airgap-it:airgap-sapling:$saplingVersion"

implementation "androidx.javascriptengine:javascriptengine:1.0.0-alpha03"
implementation 'com.google.guava:listenablefuture:1.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutinesVersion"
}


Expand Down
5 changes: 3 additions & 2 deletions android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

Expand All @@ -15,6 +15,7 @@ dependencies {
implementation project(':capacitor-filesystem')
implementation project(':capacitor-splash-screen')
implementation project(':capacitor-status-bar')
implementation project(':capawesome-capacitor-file-picker')
implementation "com.android.support:support-v4:26.+"
implementation "com.android.support:appcompat-v7:26.+"
}
Expand Down
8 changes: 7 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="it.airgap.vault">
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="it.airgap.vault">

<uses-sdk tools:overrideLibrary="androidx.javascriptengine" />

<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/assets/capacitor.plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,9 @@
{
"pkg": "@capacitor/status-bar",
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
},
{
"pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
}
]
8 changes: 6 additions & 2 deletions android/app/src/main/java/it/airgap/vault/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

import it.airgap.vault.plugin.appinfo.AppInfo;
import it.airgap.vault.plugin.camerapreview.CameraPreview;
import it.airgap.vault.plugin.isolatedmodules.IsolatedProtocol;
import it.airgap.vault.plugin.isolatedmodules.IsolatedModules;
import it.airgap.vault.plugin.saplingnative.SaplingNative;
import it.airgap.vault.plugin.securityutils.SecurityUtils;
import it.airgap.vault.plugin.zip.Zip;

public class MainActivity extends BridgeActivity {
@Override
Expand All @@ -17,7 +18,10 @@ public void onCreate(Bundle savedInstanceState) {
registerPlugin(AppInfo.class);
registerPlugin(SecurityUtils.class);
registerPlugin(SaplingNative.class);
registerPlugin(IsolatedProtocol.class);
registerPlugin(Zip.class);

// disable true isolation until it's production ready
// registerPlugin(IsolatedModules.class);

super.onCreate(savedInstanceState);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package it.airgap.vault.plugin.isolatedmodules

import android.content.Context
import com.getcapacitor.JSObject
import it.airgap.vault.plugin.isolatedmodules.js.JSModule
import it.airgap.vault.plugin.isolatedmodules.js.environment.JSEnvironment
import it.airgap.vault.util.Directory
import it.airgap.vault.util.getDirectory
import it.airgap.vault.util.readBytes
import java.io.File

interface StaticSourcesExplorer {
fun readJavaScriptEngineUtils(): ByteArray
fun readIsolatedModulesScript(): ByteArray
}

interface DynamicSourcesExplorer<in M : JSModule> {
fun listModules(): List<String>

fun readModuleSources(module: M): Sequence<ByteArray>
fun readModuleManifest(module: String): ByteArray
}

private const val MANIFEST_FILENAME = "manifest.json"

class FileExplorer private constructor(
private val context: Context,
private val assetsExplorer: AssetsExplorer,
private val filesExplorer: FilesExplorer,
) : StaticSourcesExplorer by assetsExplorer {
constructor(context: Context) : this(context, AssetsExplorer(context), FilesExplorer(context))

fun loadAssetModules(): List<JSModule> = loadModules(assetsExplorer, JSModule::Asset)

fun loadInstalledModules(): List<JSModule> = loadModules(filesExplorer, JSModule::Installed)

fun loadInstalledModule(identifier: String): JSModule {
val manifest = JSObject(filesExplorer.readModuleManifest(identifier).decodeToString())

return loadModule(identifier, manifest, JSModule::Installed)
}

fun loadPreviewModule(path: String, directory: Directory): JSModule {
val moduleDir = File(context.getDirectory(directory), path)
val manifest = JSObject(File(moduleDir, MANIFEST_FILENAME).readBytes().decodeToString())

return loadModule(moduleDir.name, manifest) { identifier, namespace, preferredEnvironment, paths ->
JSModule.Preview(
identifier,
namespace,
preferredEnvironment,
paths,
moduleDir.absolutePath,
)
}
}

fun readModuleSources(module: JSModule): Sequence<ByteArray> =
when (module) {
is JSModule.Asset -> assetsExplorer.readModuleSources(module)
is JSModule.Installed -> filesExplorer.readModuleSources(module)
is JSModule.Preview -> module.sources.asSequence().map { File(module.path, it).readBytes() }
}

fun readModuleManifest(module: JSModule): ByteArray =
when (module) {
is JSModule.Asset -> assetsExplorer.readModuleManifest(module.identifier)
is JSModule.Installed -> filesExplorer.readModuleManifest(module.identifier)
is JSModule.Preview -> File(module.path, MANIFEST_FILENAME).readBytes()
}

private fun <T : JSModule> loadModules(
explorer: DynamicSourcesExplorer<T>,
constructor: (identifier: String, namespace: String?, preferredEnvironment: JSEnvironment.Type, paths: List<String>) -> T,
): List<T> = explorer.listModules().map { module ->
val manifest = JSObject(explorer.readModuleManifest(module).decodeToString())
loadModule(module, manifest, constructor)
}

private fun <T : JSModule> loadModule(
identifier: String,
manifest: JSObject,
constructor: (identifier: String, namespace: String?, preferredEnvironment: JSEnvironment.Type, paths: List<String>) -> T,
): T {
val namespace = manifest.getJSObject("src")?.getString("namespace")
val preferredEnvironment = manifest.getJSObject("jsenv")?.getString("android")?.let { JSEnvironment.Type.fromString(it) } ?: JSEnvironment.Type.JavaScriptEngine
val sources = buildList {
val include = manifest.getJSONArray("include")
for (i in 0 until include.length()) {
val source = include.getString(i).takeIf { it.endsWith(".js") } ?: continue
add(source.trimStart('/'))
}
}

return constructor(identifier, namespace, preferredEnvironment, sources)
}
}

private class AssetsExplorer(private val context: Context) : StaticSourcesExplorer, DynamicSourcesExplorer<JSModule.Asset> {
override fun readJavaScriptEngineUtils(): ByteArray = context.assets.readBytes(JAVA_SCRIPT_ENGINE_UTILS)
override fun readIsolatedModulesScript(): ByteArray = context.assets.readBytes(SCRIPT)

override fun listModules(): List<String> = context.assets.list(MODULES_DIR)?.toList() ?: emptyList()

override fun readModuleSources(module: JSModule.Asset): Sequence<ByteArray> =
module.sources.asSequence().map { context.assets.readBytes(modulePath(module.identifier, it))}
override fun readModuleManifest(module: String): ByteArray = context.assets.readBytes(modulePath(module, MANIFEST_FILENAME))

private fun modulePath(module: String, path: String): String =
"${MODULES_DIR}/${module.trimStart('/')}/${path.trimStart('/')}"

companion object {
private const val SCRIPT = "public/assets/native/isolated_modules/isolated-modules.script.js"
private const val JAVA_SCRIPT_ENGINE_UTILS = "public/assets/native/isolated_modules/isolated-modules.js-engine-android.js"

private const val MODULES_DIR = "public/assets/protocol_modules"
}
}

private class FilesExplorer(private val context: Context) : DynamicSourcesExplorer<JSModule.Installed> {
private val modulesDir: File
get() = File(context.filesDir, MODULES_DIR)

override fun listModules(): List<String> = modulesDir.list()?.toList() ?: emptyList()

override fun readModuleSources(module: JSModule.Installed): Sequence<ByteArray> =
module.sources.asSequence().map { File(modulesDir, modulePath(module.identifier, it)).readBytes() }
override fun readModuleManifest(module: String): ByteArray = File(modulesDir, modulePath(module, MANIFEST_FILENAME)).readBytes()

private fun modulePath(module: String, path: String): String =
"${module.trimStart('/')}/${path.trimStart('/')}"

companion object {
private const val MODULES_DIR = "protocol_modules"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package it.airgap.vault.plugin.isolatedmodules

import androidx.lifecycle.lifecycleScope
import com.getcapacitor.JSArray
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import it.airgap.vault.plugin.isolatedmodules.js.*
import it.airgap.vault.util.*
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.launch

@CapacitorPlugin
class IsolatedModules : Plugin() {
private val jsEvaluator: Deferred<JSEvaluator> = ExecutableDeferred { JSEvaluator(context, fileExplorer) }
private val fileExplorer: FileExplorer by lazy { FileExplorer(context) }

@PluginMethod
fun previewModule(call: PluginCall) {
call.executeCatching {
assertReceived(Param.PATH, Param.DIRECTORY)

activity.lifecycleScope.launch {
executeCatching {
val module = fileExplorer.loadPreviewModule(path, directory)
val manifest = fileExplorer.readModuleManifest(module)
val moduleJson = jsEvaluator.await().evaluatePreviewModule(module)

resolve(
JSObject(
"""
{
"module": $moduleJson,
"manifest": ${JSObject(manifest.decodeToString())}
}
""".trimIndent()
)
)
}
}
}
}

@PluginMethod
fun registerModule(call: PluginCall) {
call.executeCatching {
assertReceived(Param.IDENTIFIER, Param.PROTOCOL_IDENTIFIERS)

activity.lifecycleScope.launch {
executeCatching {
val module = fileExplorer.loadInstalledModule(identifier)
jsEvaluator.await().registerModule(module, protocolIdentifiers)

resolve()
}
}
}
}

@PluginMethod
fun loadModules(call: PluginCall) {
activity.lifecycleScope.launch {
call.executeCatching {
val modules = fileExplorer.loadAssetModules() + fileExplorer.loadInstalledModules()

resolve(jsEvaluator.await().evaluateLoadModules(modules, protocolType))
}
}
}

@PluginMethod
fun callMethod(call: PluginCall) {
call.executeCatching {
assertReceived(Param.TARGET, Param.METHOD)

activity.lifecycleScope.launch {
executeCatching {
val value = when (target) {
JSCallMethodTarget.OfflineProtocol -> {
assertReceived(Param.PROTOCOL_IDENTIFIER)
jsEvaluator.await().evaluateCallOfflineProtocolMethod(method, args, protocolIdentifier)
}
JSCallMethodTarget.OnlineProtocol -> {
assertReceived(Param.PROTOCOL_IDENTIFIER)
jsEvaluator.await().evaluateCallOnlineProtocolMethod(method, args, protocolIdentifier, networkId)
}
JSCallMethodTarget.BlockExplorer -> {
assertReceived(Param.PROTOCOL_IDENTIFIER)
jsEvaluator.await().evaluateCallBlockExplorerMethod(method, args, protocolIdentifier, networkId)
}
JSCallMethodTarget.V3SerializerCompanion -> {
assertReceived(Param.MODULE_IDENTIFIER)
jsEvaluator.await().evaluateCallV3SerializerCompanionMethod(method, args, moduleIdentifier)
}
}
resolve(value)
}
}
}
}

override fun handleOnDestroy() {
super.handleOnDestroy()
activity.lifecycleScope.launch {
jsEvaluator.await().destroy()
}
}

private val PluginCall.path: String
get() = getString(Param.PATH)!!

private val PluginCall.directory: Directory
get() = getString(Param.DIRECTORY)?.let { Directory.fromString(it) }!!

private val PluginCall.identifier: String
get() = getString(Param.IDENTIFIER)!!

private val PluginCall.protocolIdentifiers: List<String>
get() = getArray(Param.PROTOCOL_IDENTIFIERS).toList()

private val PluginCall.protocolType: JSProtocolType?
get() = getString(Param.PROTOCOL_TYPE)?.let { JSProtocolType.fromString(it) }

private val PluginCall.target: JSCallMethodTarget
get() = getString(Param.TARGET)?.let { JSCallMethodTarget.fromString(it) }!!

private val PluginCall.method: String
get() = getString(Param.METHOD)!!

private val PluginCall.args: JSArray?
get() = getArray(Param.ARGS, null)

private val PluginCall.protocolIdentifier: String
get() = getString(Param.PROTOCOL_IDENTIFIER)!!

private val PluginCall.moduleIdentifier: String
get() = getString(Param.MODULE_IDENTIFIER)!!

private val PluginCall.networkId: String?
get() = getString(Param.NETWORK_ID)

private object Param {
const val PATH = "path"
const val DIRECTORY = "directory"
const val IDENTIFIER = "identifier"
const val PROTOCOL_IDENTIFIERS = "protocolIdentifiers"
const val PROTOCOL_TYPE = "protocolType"
const val TARGET = "target"
const val METHOD = "method"
const val ARGS = "args"
const val PROTOCOL_IDENTIFIER = "protocolIdentifier"
const val MODULE_IDENTIFIER = "moduleIdentifier"
const val NETWORK_ID = "networkId"
}
}

0 comments on commit dc745ca

Please sign in to comment.