android_native_keyring_store/
credential.rs

1use crate::{
2    cipher::{Cipher, GCMParameterSpec},
3    keystore::{Key, KeyGenParameterSpecBuilder, KeyGenerator, KeyStore},
4    shared_preferences::{Context, SharedPreferences},
5};
6use jni::{JNIEnv, JavaVM};
7use keyring_core::{
8    Credential, Entry,
9    api::{CredentialApi, CredentialStoreApi},
10};
11use std::{
12    collections::HashMap,
13    sync::{Arc, Mutex},
14};
15
16pub const KEY_ALGORITHM_AES: &str = "AES";
17pub const PROVIDER: &str = "AndroidKeyStore";
18pub const PURPOSE_ENCRYPT: i32 = 1;
19pub const PURPOSE_DECRYPT: i32 = 2;
20pub const BLOCK_MODE_GCM: &str = "GCM";
21pub const ENCRYPTION_PADDING_NONE: &str = "NoPadding";
22pub const MODE_PRIVATE: i32 = 0;
23pub const ENCRYPT_MODE: i32 = 1;
24pub const DECRYPT_MODE: i32 = 2;
25pub const CIPHER_TRANSFORMATION: &str = "AES/GCM/NoPadding";
26
27pub struct AndroidStore {
28    java_vm: JavaVM,
29    context: Context,
30}
31impl AndroidStore {
32    /// Initializes AndroidBuilder using the JNI context available
33    /// on the `ndk-context` crate.
34    #[cfg(feature = "ndk-context")]
35    pub fn from_ndk_context() -> AndroidKeyringResult<Arc<Self>> {
36        let ctx = ndk_context::android_context();
37        let vm = ctx.vm().cast();
38        let activity = ctx.context();
39
40        let java_vm = unsafe { jni::JavaVM::from_raw(vm)? };
41        let env = java_vm.attach_current_thread()?;
42
43        let context = unsafe { jni::objects::JObject::from_raw(activity as jni::sys::jobject) };
44        let context = Context::new(&env, context)?;
45
46        Self::new(&env, context)
47    }
48
49    pub fn new(env: &JNIEnv, context: Context) -> AndroidKeyringResult<Arc<Self>> {
50        let java_vm = env.get_java_vm()?;
51        Ok(Arc::new(Self { java_vm, context }))
52    }
53}
54impl CredentialStoreApi for AndroidStore {
55    fn vendor(&self) -> String {
56        "SharedPreferences/KeyStore, https://github.com/open-source-cooperative/android-native-keyring-store".to_string()
57    }
58
59    fn id(&self) -> String {
60        format!(
61            "Version {}, context {}",
62            env!("CARGO_PKG_VERSION"),
63            self.context.id()
64        )
65    }
66
67    fn build(
68        &self,
69        service: &str,
70        user: &str,
71        _modifiers: Option<&HashMap<&str, &str>>,
72    ) -> keyring_core::Result<Entry> {
73        let credential = self
74            .check_for_exception(|env| AndroidCredential::new(env, &self.context, service, user))?;
75
76        Ok(Entry::new_with_credential(Arc::new(credential)))
77    }
78
79    fn as_any(&self) -> &dyn std::any::Any {
80        self
81    }
82}
83
84pub struct AndroidCredential {
85    java_vm: JavaVM,
86    key: Key,
87    file: SharedPreferences,
88    service: String,
89    user: String,
90}
91impl AndroidCredential {
92    pub fn new(
93        env: &mut JNIEnv,
94        context: &Context,
95        service: &str,
96        user: &str,
97    ) -> AndroidKeyringResult<Self> {
98        let java_vm = env.get_java_vm()?;
99        let key = {
100            static SERVICE_LOCK: Mutex<()> = Mutex::new(());
101            let _lock = SERVICE_LOCK.lock().unwrap();
102            Self::get_key(env, service)?
103        };
104        let file = Self::get_file(env, context, service)?;
105
106        Ok(Self {
107            java_vm,
108            key,
109            file,
110            service: service.to_owned(),
111            user: user.to_owned(),
112        })
113    }
114
115    fn get_key(env: &mut JNIEnv, service: &str) -> AndroidKeyringResult<Key> {
116        let keystore = KeyStore::get_instance(env, PROVIDER)?;
117        keystore.load(env)?;
118
119        Ok(match keystore.get_key(env, service)? {
120            Some(key) => key,
121            None => {
122                let key_generator_spec = KeyGenParameterSpecBuilder::new(
123                    env,
124                    service,
125                    PURPOSE_DECRYPT | PURPOSE_ENCRYPT,
126                )?
127                .set_block_modes(env, &[BLOCK_MODE_GCM])?
128                .set_encryption_paddings(env, &[ENCRYPTION_PADDING_NONE])?
129                .set_user_authentication_required(env, false)?
130                .build(env)?;
131                let key_generator = KeyGenerator::get_instance(env, KEY_ALGORITHM_AES, PROVIDER)?;
132                key_generator.init(env, key_generator_spec.into())?;
133                let key = key_generator.generate_key(env)?;
134                key.into()
135            }
136        })
137    }
138
139    fn get_file(
140        env: &mut JNIEnv,
141        context: &Context,
142        service: &str,
143    ) -> AndroidKeyringResult<SharedPreferences> {
144        Ok(context.get_shared_preferences(env, service, MODE_PRIVATE)?)
145    }
146}
147impl CredentialApi for AndroidCredential {
148    fn set_secret(&self, secret: &[u8]) -> keyring_core::Result<()> {
149        self.check_for_exception(|env| {
150            let cipher = Cipher::get_instance(env, CIPHER_TRANSFORMATION)?;
151            cipher.init(env, ENCRYPT_MODE, &self.key)?;
152            let iv = cipher.get_iv(env)?;
153            let ciphertext = cipher.do_final(env, secret)?;
154
155            let iv_len = iv.len() as u8;
156
157            let edit = self.file.edit(env)?;
158            let mut value = vec![iv_len];
159            value.extend_from_slice(&iv);
160            value.extend_from_slice(&ciphertext);
161            edit.put_binary(env, &self.user, &value)?;
162            edit.commit(env)?;
163
164            Ok(())
165        })?;
166
167        Ok(())
168    }
169
170    fn get_secret(&self) -> keyring_core::Result<Vec<u8>> {
171        let r = self.check_for_exception(|env| {
172            let ciphertext = self.file.get_binary(env, &self.user)?;
173            Ok(match ciphertext {
174                Some(ciphertext) => {
175                    if ciphertext.is_empty() {
176                        return Err(AndroidKeyringError::CorruptedData);
177                    }
178
179                    let iv_len = ciphertext[0] as usize;
180                    let ciphertext = &ciphertext[1..];
181                    if ciphertext.len() < iv_len {
182                        return Err(AndroidKeyringError::CorruptedData);
183                    }
184
185                    let iv = &ciphertext[..iv_len];
186                    let ciphertext = &ciphertext[iv_len..];
187
188                    let spec = GCMParameterSpec::new(env, 128, iv)?;
189                    let cipher = Cipher::get_instance(env, CIPHER_TRANSFORMATION)?;
190                    cipher.init2(env, DECRYPT_MODE, &self.key, spec.into())?;
191                    let plaintext = cipher.do_final(env, ciphertext)?;
192
193                    Some(plaintext)
194                }
195                None => None,
196            })
197        })?;
198
199        match r {
200            Some(r) => Ok(r),
201            None => Err(keyring_core::Error::NoEntry),
202        }
203    }
204
205    fn delete_credential(&self) -> keyring_core::Result<()> {
206        self.check_for_exception(|env| {
207            let edit = self.file.edit(env)?;
208            edit.remove(env, &self.user)?.commit(env)?;
209            Ok(())
210        })?;
211
212        Ok(())
213    }
214
215    fn get_credential(&self) -> keyring_core::Result<Option<Arc<Credential>>> {
216        self.get_secret()?;
217        Ok(None)
218    }
219
220    fn get_specifiers(&self) -> Option<(String, String)> {
221        Some((self.service.clone(), self.user.clone()))
222    }
223
224    fn as_any(&self) -> &dyn std::any::Any {
225        self
226    }
227}
228
229pub trait HasJavaVm {
230    fn java_vm(&self) -> &JavaVM;
231    fn check_for_exception<T, F>(&self, f: F) -> AndroidKeyringResult<T>
232    where
233        F: FnOnce(&mut JNIEnv) -> AndroidKeyringResult<T>,
234    {
235        let vm = self.java_vm();
236        let mut env = vm.attach_current_thread()?;
237        let t_result = f(&mut env);
238        if env.exception_check()? {
239            env.exception_describe()?;
240            env.exception_clear()?;
241            if let Err(e) = t_result {
242                tracing::warn!(%e, "Result::Err being converted into JavaExceptionThrown");
243                tracing::debug!(?e);
244            }
245            return Err(AndroidKeyringError::JavaExceptionThrow);
246        }
247
248        t_result
249    }
250}
251impl HasJavaVm for AndroidStore {
252    fn java_vm(&self) -> &JavaVM {
253        &self.java_vm
254    }
255}
256impl HasJavaVm for AndroidCredential {
257    fn java_vm(&self) -> &JavaVM {
258        &self.java_vm
259    }
260}
261
262#[derive(thiserror::Error, Debug)]
263pub enum AndroidKeyringError {
264    #[error(transparent)]
265    JniError(#[from] jni::errors::Error),
266    #[error("Java exception was thrown")]
267    JavaExceptionThrow,
268    #[error("Corrupted data in SharedPreferences")]
269    CorruptedData,
270}
271impl From<AndroidKeyringError> for keyring_core::Error {
272    fn from(value: AndroidKeyringError) -> Self {
273        Self::PlatformFailure(Box::new(value))
274    }
275}
276type AndroidKeyringResult<T> = Result<T, AndroidKeyringError>;