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