add android-keyring

This commit is contained in:
FabianLars
2025-08-07 14:41:43 +02:00
parent af5500caed
commit 88da3d26ad
18 changed files with 91 additions and 399 deletions
+10 -12
View File
@@ -15,26 +15,24 @@ rustdoc-args = ["--cfg", "docsrs"]
# Support levels are "full", "partial", "none", "unknown"
# Details of the support level are left to plugin maintainer
[package.metadata.platforms]
windows = { level = "unknown", notes = "" }
linux = { level = "unknown", notes = "" }
macos = { level = "unknown", notes = "" }
android = { level = "unknown", notes = "" }
ios = { level = "unknown", notes = "" }
windows = { level = "full", notes = "Windows' credentials storage only protects from other accounts on that system. The main user and their apps can access your app's data." }
linux = { level = "partial", notes = "For this PoC on Linux the storage is in-memory only and will be reset on reboot." }
macos = { level = "full", notes = "May prompt for passwords twice." }
android = { level = "full", notes = "The implementation is very new and therefore not battle-tested nor audited." }
ios = { level = "full", notes = "" }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
tauri = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
# TODO: Non-android only:
keyring = { git = "https://github.com/open-source-cooperative/keyring-rs", rev = "9635a2f53a19eb7f188cdc4e38982dcb19caee00" }
# When updating to v4 we likely won't use linux-native aka keyutils but we need to look into which backend to use.
# Also, `linux-native` is non persistent.
keyring = { version = "3.6", features = ["apple-native", "windows-native", "linux-native"]}
[features]
vendored = ["keyring/vendored"]
[target."cfg(target_os = \"android\")".dependencies]
android-keyring = "0.2.0"
@@ -1,2 +0,0 @@
/build
/.tauri
@@ -1,44 +0,0 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "{{android_package_id}}"
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation(project(":tauri-android"))
}
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -1,2 +0,0 @@
include ':tauri-android'
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
@@ -1,28 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{android_package_id}}
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("{{android_package_id}}", appContext.packageName)
}
}
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -1,32 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{android_package_id}}
import android.app.Activity
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
@InvokeArg
class PingArgs {
var value: String? = null
}
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
private val implementation = Example()
@Command
fun ping(invoke: Invoke) {
val args = invoke.parseArgs(PingArgs::class.java)
val ret = JSObject()
ret.put("value", implementation.pong(args.value ?: "default value :("))
invoke.resolve(ret)
}
}
@@ -1,21 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{android_package_id}}
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ const COMMANDS: &[&str] = &["set_string", "get_string", "set_binary", "get_binar
fn main() {
let result = tauri_plugin::Builder::new(COMMANDS)
.global_api_script_path("./api-iife.js")
.android_path("android")
//.android_path("android")
//.ios_path("ios")
.try_build();
-37
View File
@@ -1,37 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use keyring::Entry;
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::Result;
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<SecureStorage<R>> {
Ok(SecureStorage(app.clone()))
}
/// Access to the secure-storage APIs.
pub struct SecureStorage<R: Runtime>(AppHandle<R>);
impl<R: Runtime> SecureStorage<R> {
pub fn set_string(&self, key: &str, value: &str) -> Result<()> {
Ok(Entry::new(&self.0.config().identifier, key)?.set_password(value)?)
}
pub fn get_string(&self, key: &str) -> Result<String> {
Ok(Entry::new(&self.0.config().identifier, key)?.get_password()?)
}
pub fn set_binary(&self, key: &str, value: &[u8]) -> Result<()> {
Ok(Entry::new(&self.0.config().identifier, key)?.set_secret(value)?)
}
pub fn get_binary(&self, key: &str) -> Result<Vec<u8>> {
Ok(Entry::new(&self.0.config().identifier, key)?.get_secret()?)
}
}
-4
View File
@@ -8,12 +8,8 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[cfg(not(target_os = "android"))]
#[error(transparent)]
Keyring(#[from] keyring::Error),
#[cfg(target_os = "android")]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
}
impl Serialize for Error {
+27 -19
View File
@@ -2,29 +2,17 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use keyring::Entry;
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
AppHandle, Manager, Runtime,
};
pub use models::*;
#[cfg(not(target_os = "android"))]
mod desktop;
#[cfg(target_os = "android")]
mod mobile;
mod commands;
mod error;
mod models;
pub use error::{Error, Result};
#[cfg(not(target_os = "android"))]
pub use desktop::SecureStorage;
#[cfg(target_os = "android")]
pub use mobile::SecureStorage;
// TODO: Consider using a worker thread to handle caveats mentioned by keyring-rs
/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the secure-storage APIs.
@@ -47,13 +35,33 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::set_binary,
commands::get_binary
])
.setup(|app, api| {
.setup(|app, _api| {
#[cfg(target_os = "android")]
let secure_storage = mobile::init(app, api)?;
#[cfg(not(target_os = "android"))]
let secure_storage = desktop::init(app, api)?;
app.manage(secure_storage);
android_keyring::set_android_keyring_credential_builder()?;
app.manage(SecureStorage(app.clone()));
Ok(())
})
.build()
}
/// Access to the secure-storage APIs.
pub struct SecureStorage<R: Runtime>(AppHandle<R>);
impl<R: Runtime> SecureStorage<R> {
pub fn set_string(&self, key: &str, value: &str) -> Result<()> {
Ok(Entry::new(&self.0.config().identifier, key)?.set_password(value)?)
}
pub fn get_string(&self, key: &str) -> Result<String> {
Ok(Entry::new(&self.0.config().identifier, key)?.get_password()?)
}
pub fn set_binary(&self, key: &str, value: &[u8]) -> Result<()> {
Ok(Entry::new(&self.0.config().identifier, key)?.set_secret(value)?)
}
pub fn get_binary(&self, key: &str) -> Result<Vec<u8>> {
Ok(Entry::new(&self.0.config().identifier, key)?.get_secret()?)
}
}
-40
View File
@@ -1,40 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{
plugin::{PluginApi, PluginHandle},
AppHandle, Runtime,
};
use crate::models::*;
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.secure_storage";
//#[cfg(target_os = "ios")]
//tauri::ios_plugin_binding!(init_plugin_secure_storage);
// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<SecureStorage<R>> {
#[cfg(target_os = "android")]
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ExamplePlugin")?;
//#[cfg(target_os = "ios")]
//let handle = api.register_ios_plugin(init_plugin_secure_storage)?;
Ok(SecureStorage(handle))
}
/// Access to the secure_storage APIs.
pub struct SecureStorage<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> SecureStorage<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
self.0
.run_mobile_plugin("ping", payload)
.map_err(Into::into)
}
}
-17
View File
@@ -1,17 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PingRequest {
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PingResponse {
pub value: Option<String>,
}