openrtc-tauri-plugin 2.8.6

First-party Tauri v2 plugin for the OpenRTC native runtime.
Documentation
//! Adapt the existing host key/record boundary to Rust control-plane inputs.
//! Record names and certificate JSON remain compatible with the TS control plane.
use std::sync::Arc;

use anyhow::{anyhow, Result};
use async_trait::async_trait;
use openrtc::native::{CertificateStore, DeviceSigner, StoredCertificate};
use serde::Deserialize;

use super::{required_app_tag, required_secure_record_key, DeviceKeySigner};

pub(crate) struct HostIdentityStorage(pub Arc<dyn DeviceKeySigner>);

impl DeviceSigner for HostIdentityStorage {
    fn public_jwk(&self, app_tag: &str) -> Result<serde_json::Value> {
        self.0.public_jwk(app_tag).map_err(|error| anyhow!(error))
    }

    fn sign(&self, app_tag: &str, challenge: &[u8]) -> Result<Vec<u8>> {
        self.0
            .sign(app_tag, challenge)
            .map_err(|error| anyhow!(error))
    }
}

fn certificate_key(app_tag: &str, principal: &str, device: &str) -> String {
    format!("openrtc:v2:device-certificate:{app_tag}:{principal}:{device}")
}

fn session_key(hash: &str, device: &str) -> String {
    format!("openrtc:v2:device-session:{hash}:{device}")
}

// Older TS records omit appTag/deviceId/sessionKeyHash. The signed certificate,
// not storage metadata, is validated by the Rust control plane before use.
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CachedCertificate {
    principal_id: String,
    token: String,
    expires_at_ms: u64,
    session_key_hash: Option<String>,
    signing_public_jwk: Option<serde_json::Value>,
}

impl HostIdentityStorage {
    fn read(&self, app_tag: &str, key: &str, device_id: &str) -> Result<Option<StoredCertificate>> {
        required_app_tag(app_tag).map_err(|error| anyhow!(error))?;
        required_secure_record_key(key).map_err(|error| anyhow!(error))?;
        let Some(raw) = self
            .0
            .read_secure_record(app_tag, key)
            .map_err(|error| anyhow!(error))?
        else {
            return Ok(None);
        };
        let cached: CachedCertificate = serde_json::from_str(&raw)?;
        Ok(Some(StoredCertificate {
            app_tag: app_tag.into(),
            device_id: device_id.into(),
            principal_id: cached.principal_id,
            token: cached.token,
            expires_at_ms: cached.expires_at_ms,
            session_key_hash: cached.session_key_hash,
            signing_public_jwk: cached.signing_public_jwk,
        }))
    }

    fn write(&self, app_tag: &str, key: &str, value: &str) -> Result<()> {
        required_app_tag(app_tag).map_err(|error| anyhow!(error))?;
        required_secure_record_key(key).map_err(|error| anyhow!(error))?;
        self.0
            .write_secure_record(app_tag, key, value)
            .map_err(|error| anyhow!(error))
    }
}

#[async_trait]
impl CertificateStore for HostIdentityStorage {
    async fn load(
        &self,
        app_tag: &str,
        principal: &str,
        device: &str,
    ) -> Result<Option<StoredCertificate>> {
        Ok(self
            .read(
                app_tag,
                &certificate_key(app_tag, principal, device),
                device,
            )?
            .filter(|stored| stored.principal_id == principal))
    }

    async fn save(&self, stored: &StoredCertificate) -> Result<()> {
        let value = serde_json::to_string(stored)?;
        self.write(
            &stored.app_tag,
            &certificate_key(&stored.app_tag, &stored.principal_id, &stored.device_id),
            &value,
        )?;
        if let Some(hash) = &stored.session_key_hash {
            self.write(
                &stored.app_tag,
                &session_key(hash, &stored.device_id),
                &value,
            )?;
        }
        Ok(())
    }

    async fn load_for_session(
        &self,
        app_tag: &str,
        hash: &str,
        device: &str,
    ) -> Result<Option<StoredCertificate>> {
        let Some(mut stored) = self.read(app_tag, &session_key(hash, device), device)? else {
            return Ok(None);
        };
        if stored.session_key_hash.as_deref() != Some(hash) {
            // Backfill the hashed index once when adopting an older TS record,
            // so explicit removal can retire both records without a store scan.
            stored.session_key_hash = Some(hash.into());
            self.save(&stored).await?;
        }
        Ok(Some(stored))
    }

    async fn remove(&self, app_tag: &str, principal: &str, device: &str) -> Result<()> {
        if let Some(stored) = self.load(app_tag, principal, device).await? {
            if let Some(hash) = stored.session_key_hash {
                let key = session_key(&hash, device);
                required_secure_record_key(&key).map_err(|error| anyhow!(error))?;
                self.0
                    .delete_secure_record(app_tag, &key)
                    .map_err(|error| anyhow!(error))?;
            }
        }
        self.0
            .delete_secure_record(app_tag, &certificate_key(app_tag, principal, device))
            .map_err(|error| anyhow!(error))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;
    use std::sync::Mutex;

    #[derive(Default)]
    struct HostStore(Mutex<HashMap<(String, String), String>>);

    impl DeviceKeySigner for HostStore {
        fn public_jwk(&self, _: &str) -> Result<serde_json::Value, String> {
            Ok(serde_json::json!({"kty":"OKP", "crv":"Ed25519", "x":"host-public-key"}))
        }
        fn sign(&self, app: &str, bytes: &[u8]) -> Result<Vec<u8>, String> {
            assert_eq!(app, "app_test");
            assert_eq!(bytes, b"challenge");
            Ok(vec![7; 64])
        }
        fn delete(&self, _: &str) -> Result<(), String> {
            panic!("certificate removal must not delete the installation key")
        }
        fn read_secure_record(&self, app: &str, key: &str) -> Result<Option<String>, String> {
            Ok(self
                .0
                .lock()
                .unwrap()
                .get(&(app.into(), key.into()))
                .cloned())
        }
        fn write_secure_record(&self, app: &str, key: &str, value: &str) -> Result<(), String> {
            self.0
                .lock()
                .unwrap()
                .insert((app.into(), key.into()), value.into());
            Ok(())
        }
        fn delete_secure_record(&self, app: &str, key: &str) -> Result<(), String> {
            self.0.lock().unwrap().remove(&(app.into(), key.into()));
            Ok(())
        }
    }

    #[tokio::test]
    async fn adopts_ts_certificate_and_removes_both_indexes() {
        let host = Arc::new(HostStore::default());
        let store = HostIdentityStorage(host.clone());
        // Exact existing TS cache envelope: no native-only identity fields.
        let value = serde_json::json!({
            "principalId":"principal", "token":"fixture-certificate", "expiresAtMs":123456,
            "signingPublicJwk":{"kty":"OKP", "crv":"Ed25519", "x":"issuer-key"}
        })
        .to_string();
        host.write_secure_record("app_test", &session_key("session-hash", "device"), &value)
            .unwrap();
        host.write_secure_record("app_other", &session_key("session-hash", "device"), &value)
            .unwrap();
        let stored = store
            .load_for_session("app_test", "session-hash", "device")
            .await
            .unwrap()
            .unwrap();
        assert_eq!(stored.app_tag, "app_test");
        assert_eq!(stored.device_id, "device");
        assert_eq!(stored.session_key_hash.as_deref(), Some("session-hash"));
        assert_eq!(stored.token, "fixture-certificate");
        assert_eq!(
            store.load("app_test", "principal", "device").await.unwrap(),
            Some(stored)
        );
        assert!(store
            .load_for_session("app_test", "another-session", "device")
            .await
            .unwrap()
            .is_none());
        assert!(store
            .load_for_session("app_test", "session-hash", "another-device")
            .await
            .unwrap()
            .is_none());
        store
            .remove("app_test", "principal", "device")
            .await
            .unwrap();
        assert!(store
            .load_for_session("app_test", "session-hash", "device")
            .await
            .unwrap()
            .is_none());
        assert!(store
            .load("app_test", "principal", "device")
            .await
            .unwrap()
            .is_none());
        assert!(host
            .read_secure_record("app_other", &session_key("session-hash", "device"))
            .unwrap()
            .is_some());
    }

    #[tokio::test]
    async fn preserves_native_fields_and_rejects_wrong_principal_or_corrupt_record() {
        let host = Arc::new(HostStore::default());
        let store = HostIdentityStorage(host.clone());
        let stored = StoredCertificate {
            app_tag: "app_test".into(),
            principal_id: "principal".into(),
            device_id: "device".into(),
            token: "fixture".into(),
            expires_at_ms: 123456,
            session_key_hash: Some("hash".into()),
            signing_public_jwk: Some(serde_json::json!({"x":"issuer-key"})),
        };
        store.save(&stored).await.unwrap();
        assert_eq!(
            store
                .load_for_session("app_test", "hash", "device")
                .await
                .unwrap(),
            Some(stored.clone())
        );
        host.write_secure_record(
            "app_test",
            &certificate_key("app_test", "other", "device"),
            &serde_json::to_string(&stored).unwrap(),
        )
        .unwrap();
        assert!(store
            .load("app_test", "other", "device")
            .await
            .unwrap()
            .is_none());
        host.write_secure_record("app_test", &session_key("bad", "device"), "not-json")
            .unwrap();
        assert!(store
            .load_for_session("app_test", "bad", "device")
            .await
            .is_err());
        assert!(store
            .load_for_session("app_test", "../bad", "device")
            .await
            .is_err());
        let mut corrupt = stored;
        corrupt.session_key_hash = Some("../bad".into());
        host.write_secure_record(
            "app_test",
            &certificate_key("app_test", "principal", "device"),
            &serde_json::to_string(&corrupt).unwrap(),
        )
        .unwrap();
        assert!(store
            .remove("app_test", "principal", "device")
            .await
            .is_err());
    }

    #[test]
    fn delegates_proof_to_the_existing_host_key() {
        let store = HostIdentityStorage(Arc::new(HostStore::default()));
        assert_eq!(
            DeviceSigner::public_jwk(&store, "app_test").unwrap()["x"],
            "host-public-key"
        );
        assert_eq!(
            DeviceSigner::sign(&store, "app_test", b"challenge").unwrap(),
            vec![7; 64]
        );
    }
}