Skip to main content

apk/sign/
keystore.rs

1//! Java keystores: JKS and PKCS#12 (the format `keytool` and Android Studio
2//! create by default).
3
4use std::fmt;
5use std::panic::{AssertUnwindSafe, catch_unwind};
6use std::path::Path;
7
8use zeroize::Zeroizing;
9
10use super::{Certificate, KeyError, PrivateKey, SigningIdentity};
11use crate::error::{Error, FileOperation};
12
13const JKS_MAGIC: [u8; 4] = [0xfe, 0xed, 0xfe, 0xed];
14
15enum Store {
16    Jks { store: jks::KeyStore, password: Zeroizing<String> },
17    Pkcs12 { store: p12_keystore::KeyStore, password: Zeroizing<String> },
18}
19
20/// An unlocked JKS or PKCS#12 keystore.
21///
22/// ```no_run
23/// use apk::sign::Keystore;
24///
25/// let keystore = Keystore::open("release.keystore", "store password")?;
26/// let identity = keystore.identity("upload", None)?;
27/// # Ok::<(), apk::Error>(())
28/// ```
29pub struct Keystore(Store);
30
31impl fmt::Debug for Keystore {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        let format = match self.0 {
34            Store::Jks { .. } => "JKS",
35            Store::Pkcs12 { .. } => "PKCS#12",
36        };
37        f.debug_struct("Keystore").field("format", &format).field("aliases", &self.aliases()).finish()
38    }
39}
40
41impl Keystore {
42    /// Opens and unlocks the keystore at `path`.
43    pub fn open(path: impl AsRef<Path>, password: &str) -> Result<Self, Error> {
44        let path = path.as_ref();
45        let bytes = Zeroizing::new(std::fs::read(path).map_err(Error::file(FileOperation::Read, path))?);
46        Ok(Self::from_bytes(&bytes, password)?)
47    }
48
49    /// Unlocks a keystore held in memory, detecting its format.
50    pub fn from_bytes(bytes: &[u8], password: &str) -> Result<Self, KeyError> {
51        // The parsers can panic on malformed input; treat that as a format error.
52        let parsed = catch_unwind(AssertUnwindSafe(|| -> Result<Self, KeyError> {
53            if bytes.starts_with(&JKS_MAGIC) {
54                let mut store = jks::KeyStore::new();
55                store.load(bytes, password.as_bytes())?;
56                Ok(Self(Store::Jks { store, password: Zeroizing::new(password.to_owned()) }))
57            } else {
58                let policy = p12_keystore::Pkcs12ImportPolicy::Strict;
59                let store = p12_keystore::KeyStore::from_pkcs12(bytes, password, policy)?;
60                Ok(Self(Store::Pkcs12 { store, password: Zeroizing::new(password.to_owned()) }))
61            }
62        }));
63        parsed.map_err(|_| KeyError::MalformedKeystore)?
64    }
65
66    /// The aliases of all entries.
67    pub fn aliases(&self) -> Vec<String> {
68        match &self.0 {
69            Store::Jks { store, .. } => store.aliases(),
70            Store::Pkcs12 { store, .. } => store.entries().map(|(alias, _)| alias.clone()).collect(),
71        }
72    }
73
74    /// The private key entry `alias`. JKS entries may have their own
75    /// password; it defaults to the store password. PKCS#12 entries always
76    /// use the store password.
77    pub fn identity(&self, alias: &str, key_password: Option<&str>) -> Result<SigningIdentity, KeyError> {
78        let unknown = || KeyError::UnknownAlias { alias: alias.to_owned() };
79        let (key, chain) = match &self.0 {
80            Store::Jks { store, password } => {
81                if !store.is_private_key_entry(alias) {
82                    return Err(if store.aliases().iter().any(|a| a == alias) {
83                        KeyError::NotAPrivateKey { alias: alias.to_owned() }
84                    } else {
85                        unknown()
86                    });
87                }
88                let password = key_password.unwrap_or(password);
89                let entry = store.get_private_key_entry(alias, password.as_bytes())?;
90                let chain: Vec<Vec<u8>> = entry.certificate_chain.into_iter().map(|c| c.content).collect();
91                (PrivateKey::from_pkcs8_der(&entry.private_key)?, chain)
92            }
93            Store::Pkcs12 { store, password } => {
94                if key_password.is_some_and(|p| p != password.as_str()) {
95                    return Err(KeyError::WrongPassword);
96                }
97                match store.entry(alias).ok_or_else(unknown)? {
98                    p12_keystore::KeyStoreEntry::PrivateKeyChain(chain) => (
99                        PrivateKey::from_pkcs8_der(chain.key().as_der())?,
100                        chain.certs().iter().map(|c| c.as_der().to_vec()).collect(),
101                    ),
102                    _ => return Err(KeyError::NotAPrivateKey { alias: alias.to_owned() }),
103                }
104            }
105        };
106        if chain.is_empty() {
107            return Err(KeyError::MissingCertificate);
108        }
109        let chain = chain.iter().map(|der| Certificate::from_der(der)).collect::<Result<_, _>>()?;
110        SigningIdentity::new(key, chain)
111    }
112
113    /// The identity of the keystore's only private key entry.
114    pub fn sole_identity(&self, key_password: Option<&str>) -> Result<SigningIdentity, KeyError> {
115        let aliases: Vec<String> = self.aliases().into_iter().filter(|alias| self.is_private_key(alias)).collect();
116        match aliases.as_slice() {
117            [alias] => self.identity(alias, key_password),
118            _ => Err(KeyError::AmbiguousAlias { count: aliases.len() }),
119        }
120    }
121
122    fn is_private_key(&self, alias: &str) -> bool {
123        match &self.0 {
124            Store::Jks { store, .. } => store.is_private_key_entry(alias),
125            Store::Pkcs12 { store, .. } => {
126                matches!(store.entry(alias), Some(p12_keystore::KeyStoreEntry::PrivateKeyChain(_)))
127            }
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    const P12: &[u8] = include_bytes!("../../tests/fixtures/keys/test-only.p12");
137
138    #[test]
139    fn unlocks_pkcs12_created_by_keytool() {
140        let keystore = Keystore::from_bytes(P12, "android-test").unwrap();
141        assert_eq!(keystore.aliases(), ["postcard"]);
142        assert!(keystore.identity("postcard", None).is_ok());
143        assert!(keystore.sole_identity(None).is_ok());
144        assert!(matches!(keystore.identity("missing", None), Err(KeyError::UnknownAlias { .. })));
145        assert!(matches!(keystore.identity("postcard", Some("other")), Err(KeyError::WrongPassword)));
146        assert!(Keystore::from_bytes(P12, "wrong").is_err());
147    }
148
149    #[test]
150    fn unlocks_jks_with_separate_key_password() {
151        let identity = SigningIdentity::from_pem(include_str!("../../tests/fixtures/keys/debug.pem")).unwrap();
152        let key = identity.key().to_pkcs8_der().unwrap();
153        let mut store = jks::KeyStore::new();
154        let entry = jks::PrivateKeyEntry {
155            creation_time: std::time::UNIX_EPOCH,
156            private_key: key.to_vec(),
157            certificate_chain: vec![jks::Certificate {
158                cert_type: "X509".into(),
159                content: identity.certificate().der().to_vec(),
160            }],
161        };
162        store.set_private_key_entry("release", entry, b"key-pass").unwrap();
163        let mut bytes = Vec::new();
164        store.store(&mut bytes, b"store-pass").unwrap();
165
166        let keystore = Keystore::from_bytes(&bytes, "store-pass").unwrap();
167        let loaded = keystore.identity("release", Some("key-pass")).unwrap();
168        assert_eq!(loaded.certificate(), identity.certificate());
169        assert!(keystore.identity("release", None).is_err());
170    }
171}