apksigner 1.0.0

APK signer and verifier CLI — a faithful Rust port of AOSP apksigner
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
//! KeyStore reading for [`crate::signer_params::SignerParams`].
//!
//! Mirrors the JCA `java.security.KeyStore` operations that
//! `com.android.apksigner.SignerParams.loadPrivateKeyAndCertsFromKeyStore` performs: load the
//! store with each candidate password, enumerate the aliases, find the private key entry, unlock
//! the key, and read its certificate chain.
//!
//! # Mapping notes
//!
//! * The Java code obtains a `KeyStore` through the JCA provider registry, honouring
//!   `--ks-provider-name` / `--ks-provider-class` / `--ks-provider-arg`. Rust has no such
//!   registry, so only the built-in implementations below exist; the provider options are
//!   accepted (and validated) by `SignerParams` but cannot select anything else.
//! * Java's `KeyStore.getDefaultType()` is `pkcs12`. This port instead *sniffs* the file when
//!   `--ks-type` is absent: a `0xFEEDFEED` magic means JKS, a leading `0x30` means PKCS #12.
//!   That is a deliberate reliability improvement, not a behavioural requirement: the two
//!   formats are unambiguous from their first bytes.
//! * `jks`'s own `pkcs12` feature is broken against every published `p12-keystore`, so JKS and
//!   PKCS #12 are read through the `jks` and `p12-keystore` crates respectively.

use std::fmt;
use std::path::Path;

use crate::password_retriever::{Charset, Password};

/// The JKS magic number, `0xFEEDFEED`, big-endian at the start of the file.
pub const JKS_MAGIC: u32 = 0xFEED_FEED;

/// The KeyStore storage formats this port can read.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyStoreFormat {
    /// The Java KeyStore format (`JKS`).
    Jks,
    /// PKCS #12 / PFX (`PKCS12`, `PKCS#12`, `p12`, `pfx`).
    Pkcs12,
}

impl KeyStoreFormat {
    /// Resolves a `--ks-type` value, mirroring `KeyStore.getInstance(type)`.
    ///
    /// Returns `Err` where the Java method throws `KeyStoreException("<type> not found")`.
    pub fn from_name(name: &str) -> Result<Self, KeyStoreError> {
        let normalized = name.to_ascii_lowercase().replace(['#', '-', '_', ' '], "");
        match normalized.as_str() {
            "jks" => Ok(KeyStoreFormat::Jks),
            "pkcs12" | "p12" | "pfx" => Ok(KeyStoreFormat::Pkcs12),
            _ => Err(KeyStoreError::UnsupportedType(format!(
                "{} not found: this port provides only the JKS and PKCS12 KeyStore types",
                name
            ))),
        }
    }

    /// Detects the format from the keystore's first bytes.
    ///
    /// Returns `None` when the contents match neither format.
    pub fn detect(data: &[u8]) -> Option<Self> {
        if data.len() >= 4 && u32::from_be_bytes([data[0], data[1], data[2], data[3]]) == JKS_MAGIC
        {
            return Some(KeyStoreFormat::Jks);
        }
        // A PKCS #12 PFX is a DER SEQUENCE, so it always starts with the SEQUENCE tag.
        if data.first() == Some(&0x30) {
            return Some(KeyStoreFormat::Pkcs12);
        }
        None
    }
}

/// Errors raised while reading a KeyStore.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyStoreError {
    /// No candidate password unlocked the store.
    ///
    /// Mirrors the `IOException` that `KeyStore.load` raises when the store's integrity check
    /// fails, which is what a wrong password looks like.
    WrongKeystorePassword(String),
    /// The private key could not be recovered with any candidate key password.
    /// Mirrors `java.security.UnrecoverableKeyException`.
    UnrecoverableKey(String),
    /// The alias does not name a key entry. Holds the alias; the caller qualifies it with the
    /// keystore file name to reproduce Java's
    /// `"<file> entry \"<alias>\" does not contain a key"`.
    NoKeyEntry(String),
    /// The alias names a trusted-certificate entry rather than a key entry. Holds the alias; the
    /// caller qualifies it with the keystore file name to reproduce Java's
    /// `"<file> entry \"<alias>\" does not contain a key"`.
    NotAKeyEntry(String),
    /// The entry holds a key that is not a private key. Holds the alias and the entry's key
    /// algorithm; the caller qualifies the alias with the keystore file name to reproduce Java's
    /// `"<file> entry \"<alias>\" does not contain a private key. It contains a key of
    /// algorithm: <algorithm>"`.
    NotAPrivateKey {
        /// The alias of the offending entry.
        alias: String,
        /// The entry's key algorithm.
        algorithm: String,
    },
    /// The keystore could not be read from disk or parsed.
    Malformed(String),
    /// The requested KeyStore type is not one this port provides.
    UnsupportedType(String),
}

impl fmt::Display for KeyStoreError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            KeyStoreError::WrongKeystorePassword(m) => write!(f, "{}", m),
            KeyStoreError::UnrecoverableKey(m) => write!(f, "{}", m),
            KeyStoreError::NoKeyEntry(alias) | KeyStoreError::NotAKeyEntry(alias) => {
                write!(f, "entry \"{}\" does not contain a key", alias)
            }
            KeyStoreError::NotAPrivateKey { alias, algorithm } => {
                write!(
                    f,
                    "entry \"{}\" does not contain a private key. It contains a key of \
                     algorithm: {}",
                    alias, algorithm
                )
            }
            KeyStoreError::Malformed(m) => write!(f, "{}", m),
            KeyStoreError::UnsupportedType(m) => write!(f, "{}", m),
        }
    }
}

impl std::error::Error for KeyStoreError {}

/// An opened KeyStore.
///
/// Mirrors the subset of `java.security.KeyStore` that `SignerParams` uses. The Java type is
/// keyed by alias and can also be asked `isKeyEntry`; `getKey` unlocks a private key with a
/// password, and `getCertificateChain` returns its chain.
pub enum KeyStore {
    /// A JKS store. Private keys are stored encrypted with their own password.
    Jks(Box<jks::KeyStore>),
    /// A PKCS #12 store. Private keys are decrypted while the store is parsed, so `get_key`
    /// ignores its password list.
    Pkcs12(Box<p12_keystore::KeyStore>),
}

impl fmt::Debug for KeyStore {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Deliberately does not print key material or the store's contents.
        match self {
            KeyStore::Jks(_) => f.write_str("KeyStore::Jks(<redacted>)"),
            KeyStore::Pkcs12(_) => f.write_str("KeyStore::Pkcs12(<redacted>)"),
        }
    }
}

impl KeyStore {
    /// Reads a KeyStore from `path`, trying each password in `passwords`.
    ///
    /// Mirrors `SignerParams.loadKeyStoreFromFile`: each candidate password is tried in order and
    /// the first that unlocks the store wins, with the last failure reported when none does.
    /// `format_hint` is the `--ks-type` value; when absent the format is detected from the file's
    /// first bytes.
    pub fn load_from_file(
        path: &str,
        passwords: &[Password],
        format_hint: Option<&str>,
    ) -> Result<Self, KeyStoreError> {
        // `--ks-type` is resolved before the file is read, as `KeyStore.getInstance(type)` is.
        let hinted_format = match format_hint {
            Some(name) => Some(KeyStoreFormat::from_name(name)?),
            None => None,
        };

        let data = std::fs::read(Path::new(path))
            .map_err(|e| KeyStoreError::Malformed(format!("Failed to read {}: {}", path, e)))?;

        let format = match hinted_format {
            Some(format) => format,
            None => KeyStoreFormat::detect(&data).ok_or_else(|| {
                KeyStoreError::Malformed(format!(
                    "Failed to determine the KeyStore type of {}: it is neither a JKS \
                     (0xFEEDFEED magic) nor a PKCS #12 (DER SEQUENCE) file. Specify --ks-type.",
                    path
                ))
            })?,
        };
        Self::load(&data, passwords, format)
    }

    /// Reads a KeyStore from memory, trying each password in `passwords`.
    ///
    /// Mirrors `KeyStore.load(InputStream, char[])`. A wrong keystore password fails the store's
    /// integrity check, so the failure of the final candidate password is what is reported.
    pub fn load(
        data: &[u8],
        passwords: &[Password],
        format: KeyStoreFormat,
    ) -> Result<Self, KeyStoreError> {
        if passwords.is_empty() {
            // Mirrors the Java `RuntimeException("No keystore passwords")`.
            return Err(KeyStoreError::Malformed(
                "No keystore passwords".to_string(),
            ));
        }

        let mut last_failure: Option<KeyStoreError> = None;
        for password in passwords {
            match Self::load_once(data, password, format) {
                Ok(store) => return Ok(store),
                Err(e) => last_failure = Some(e),
            }
        }
        Err(last_failure.expect("at least one password was tried"))
    }

    /// Attempts to read a KeyStore with a single password.
    fn load_once(
        data: &[u8],
        password: &[char],
        format: KeyStoreFormat,
    ) -> Result<Self, KeyStoreError> {
        // Both formats are specified in terms of the password's UTF-8 octets.
        let password_bytes = Charset::Utf8.encode(password);
        match format {
            KeyStoreFormat::Jks => {
                let mut store = jks::KeyStore::new();
                store.load(data, &password_bytes).map_err(|e| {
                    KeyStoreError::WrongKeystorePassword(format!(
                        "Failed to load the KeyStore: {}",
                        e
                    ))
                })?;
                Ok(KeyStore::Jks(Box::new(store)))
            }
            KeyStoreFormat::Pkcs12 => {
                let password_str = String::from_iter(password.iter().copied());
                let store = p12_keystore::KeyStore::from_pkcs12(
                    data,
                    &password_str,
                    p12_keystore::Pkcs12ImportPolicy::Strict,
                )
                .map_err(|e| {
                    KeyStoreError::WrongKeystorePassword(format!(
                        "Failed to load the KeyStore: {}",
                        e
                    ))
                })?;
                Ok(KeyStore::Pkcs12(Box::new(store)))
            }
        }
    }

    /// Returns every alias in the store, mirroring `KeyStore.aliases()`.
    pub fn aliases(&self) -> Vec<String> {
        match self {
            KeyStore::Jks(store) => store.aliases(),
            KeyStore::Pkcs12(store) => store.entries().map(|(alias, _)| alias.clone()).collect(),
        }
    }

    /// Returns whether the alias names an entry holding a private key.
    /// Mirrors `KeyStore.isKeyEntry(String)`.
    pub fn is_key_entry(&self, alias: &str) -> bool {
        match self {
            KeyStore::Jks(store) => store.is_private_key_entry(alias),
            KeyStore::Pkcs12(store) => matches!(
                store.entry(alias),
                Some(p12_keystore::KeyStoreEntry::PrivateKeyChain(_))
            ),
        }
    }

    /// Unlocks the private key stored under `alias`, trying each password in `passwords`.
    ///
    /// Mirrors `KeyStore.getKey(alias, password)`: the first password that recovers the key wins,
    /// and an `UnrecoverableKeyException` is reported when none does. Returns the key in PKCS #8
    /// DER form.
    pub fn get_key(&self, alias: &str, passwords: &[Password]) -> Result<Vec<u8>, KeyStoreError> {
        match self {
            KeyStore::Jks(store) => {
                if !store.is_private_key_entry(alias) {
                    return Err(if store.aliases().iter().any(|a| a == alias) {
                        KeyStoreError::NotAKeyEntry(alias.to_string())
                    } else {
                        KeyStoreError::NoKeyEntry(alias.to_string())
                    });
                }
                if passwords.is_empty() {
                    return Err(KeyStoreError::Malformed("No key passwords".to_string()));
                }
                let mut last_failure: Option<KeyStoreError> = None;
                for password in passwords {
                    let password_bytes = Charset::Utf8.encode(password);
                    match store.get_private_key_entry(alias, &password_bytes) {
                        Ok(entry) => return Ok(entry.private_key),
                        Err(jks::KeyStoreError::EntryNotFound) => {
                            last_failure = Some(KeyStoreError::NoKeyEntry(alias.to_string()))
                        }
                        Err(jks::KeyStoreError::WrongEntryType) => {
                            last_failure = Some(KeyStoreError::NotAKeyEntry(alias.to_string()))
                        }
                        // A wrong key password surfaces as a decryption failure, which is what
                        // Java reports as `UnrecoverableKeyException`.
                        Err(e) => {
                            last_failure = Some(KeyStoreError::UnrecoverableKey(format!(
                                "Failed to obtain key with alias \"{}\": {}",
                                alias, e
                            )))
                        }
                    }
                }
                Err(last_failure.expect("at least one password was tried"))
            }
            KeyStore::Pkcs12(store) => match store.entry(alias) {
                Some(p12_keystore::KeyStoreEntry::PrivateKeyChain(chain)) => {
                    // PKCS #12 private keys are decrypted while the store is parsed, so the
                    // password list is not consulted here.
                    Ok(chain.key().as_der().to_vec())
                }
                Some(p12_keystore::KeyStoreEntry::Certificate(_)) => {
                    Err(KeyStoreError::NotAKeyEntry(alias.to_string()))
                }
                Some(p12_keystore::KeyStoreEntry::Secret(secret)) => {
                    Err(KeyStoreError::NotAPrivateKey {
                        alias: alias.to_string(),
                        algorithm: format!("{:?}", secret.key_type()),
                    })
                }
                None => Err(KeyStoreError::NoKeyEntry(alias.to_string())),
            },
        }
    }

    /// Returns the certificate chain stored under `alias`, leaf first, each DER encoded.
    ///
    /// Mirrors `KeyStore.getCertificateChain(String)`, which returns `null` for an entry that
    /// has no chain; that becomes an empty vector here.
    pub fn get_certificate_chain(&self, alias: &str) -> Result<Vec<Vec<u8>>, KeyStoreError> {
        match self {
            KeyStore::Jks(store) => match store.get_private_key_entry_certificate_chain(alias) {
                Ok(chain) => Ok(chain.into_iter().map(|c| c.content).collect()),
                Err(jks::KeyStoreError::EntryNotFound) => {
                    Err(KeyStoreError::NoKeyEntry(alias.to_string()))
                }
                Err(jks::KeyStoreError::WrongEntryType) => {
                    // A trusted-certificate entry has a certificate, not a chain.
                    Ok(Vec::new())
                }
                Err(e) => Err(KeyStoreError::Malformed(format!(
                    "Failed to read the certificate chain of \"{}\": {}",
                    alias, e
                ))),
            },
            KeyStore::Pkcs12(store) => match store.entry(alias) {
                Some(p12_keystore::KeyStoreEntry::PrivateKeyChain(chain)) => {
                    Ok(chain.certs().iter().map(|c| c.as_der().to_vec()).collect())
                }
                Some(p12_keystore::KeyStoreEntry::Certificate(cert)) => {
                    Ok(vec![cert.as_der().to_vec()])
                }
                Some(p12_keystore::KeyStoreEntry::Secret(_)) | None => Ok(Vec::new()),
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn type_names_resolve_like_key_store_get_instance() {
        assert_eq!(
            KeyStoreFormat::from_name("JKS").unwrap(),
            KeyStoreFormat::Jks
        );
        assert_eq!(
            KeyStoreFormat::from_name("jks").unwrap(),
            KeyStoreFormat::Jks
        );
        assert_eq!(
            KeyStoreFormat::from_name("PKCS12").unwrap(),
            KeyStoreFormat::Pkcs12
        );
        assert_eq!(
            KeyStoreFormat::from_name("pkcs#12").unwrap(),
            KeyStoreFormat::Pkcs12
        );
        assert_eq!(
            KeyStoreFormat::from_name("pkcs12").unwrap(),
            KeyStoreFormat::Pkcs12
        );
        // A type the port cannot provide is reported as not found, as the JCA does.
        assert!(matches!(
            KeyStoreFormat::from_name("JCEKS"),
            Err(KeyStoreError::UnsupportedType(_))
        ));
    }

    #[test]
    fn format_is_detected_from_the_first_bytes() {
        let mut jks = JKS_MAGIC.to_be_bytes().to_vec();
        jks.extend_from_slice(&[0u8; 8]);
        assert_eq!(KeyStoreFormat::detect(&jks), Some(KeyStoreFormat::Jks));

        let asn1 = vec![0x30, 0x82, 0x01, 0x00];
        assert_eq!(KeyStoreFormat::detect(&asn1), Some(KeyStoreFormat::Pkcs12));

        assert_eq!(KeyStoreFormat::detect(&[0x00, 0x01, 0x02, 0x03]), None);
        assert_eq!(KeyStoreFormat::detect(&[]), None);
    }

    #[test]
    fn loading_without_passwords_reports_the_java_runtime_exception() {
        let err = KeyStore::load(&[], &[], KeyStoreFormat::Jks).unwrap_err();
        assert_eq!(err.to_string(), "No keystore passwords");
    }

    #[test]
    fn a_wrong_keystore_password_is_reported() {
        // Not a real keystore, so the magic check fails for every password.
        let data = JKS_MAGIC.to_be_bytes().to_vec();
        let err = KeyStore::load(&data, &[vec!['x']], KeyStoreFormat::Jks).unwrap_err();
        assert!(matches!(err, KeyStoreError::WrongKeystorePassword(_)));
    }

    #[test]
    fn a_not_a_private_key_error_names_the_alias_and_algorithm() {
        let err = KeyStoreError::NotAPrivateKey {
            alias: "secret".to_string(),
            algorithm: "Aes".to_string(),
        };
        let rendered = err.to_string();
        assert!(rendered.contains("entry \"secret\""));
        assert!(rendered.contains("algorithm: Aes"));
    }

    #[test]
    fn key_entry_errors_name_the_alias() {
        assert_eq!(
            KeyStoreError::NoKeyEntry("release".to_string()).to_string(),
            "entry \"release\" does not contain a key"
        );
    }

    /// The `--ks-type` name is validated before the file is opened, matching
    /// `KeyStore.getInstance(type)` being called first in `loadPrivateKeyAndCertsFromKeyStore`.
    #[test]
    fn an_unsupported_type_is_reported_without_touching_the_file() {
        let err =
            KeyStore::load_from_file("/nonexistent/keystore.bks", &[vec!['x'; 8]], Some("BKS"))
                .unwrap_err();
        assert!(matches!(err, KeyStoreError::UnsupportedType(_)));
    }

    #[test]
    fn a_missing_keystore_file_is_reported() {
        let err = KeyStore::load_from_file("/nonexistent/release.jks", &[vec!['x'; 8]], None)
            .unwrap_err();
        assert!(matches!(err, KeyStoreError::Malformed(_)));
    }

    /// A JKS store written by the `jks` crate must load back through this module with the same
    /// alias, key and chain. This is the round trip `SignerParams` depends on.
    #[test]
    fn a_jks_store_round_trips_through_file_loading() {
        use crate::password_retriever::Password;

        const PASSWORD: &str = "pass123";
        const KEY_BYTES: [u8; 5] = [0x30, 0x03, 0x02, 0x01, 0x2a];

        let mut store = jks::KeyStore::new();
        store
            .set_private_key_entry(
                "k",
                jks::PrivateKeyEntry {
                    creation_time: std::time::SystemTime::UNIX_EPOCH,
                    private_key: KEY_BYTES.to_vec(),
                    certificate_chain: Vec::new(),
                },
                PASSWORD.as_bytes(),
            )
            .expect("write entry");
        let mut encoded = Vec::new();
        store
            .store(&mut encoded, PASSWORD.as_bytes())
            .expect("encode store");

        let path = std::env::temp_dir().join("apksigner_keystore_round_trip.jks");
        std::fs::write(&path, &encoded).expect("write file");

        let passwords: Vec<Password> = vec![PASSWORD.chars().collect()];
        let loaded =
            KeyStore::load_from_file(path.to_str().unwrap(), &passwords, None).expect("load");
        assert_eq!(loaded.aliases(), vec!["k".to_string()]);
        assert!(loaded.is_key_entry("k"));
        assert_eq!(loaded.get_key("k", &passwords).unwrap(), KEY_BYTES.to_vec());
        assert!(loaded.get_certificate_chain("k").unwrap().is_empty());

        // The wrong key password must not unlock the key.
        assert!(matches!(
            loaded.get_key("k", &[vec!['w'; 8]]),
            Err(KeyStoreError::UnrecoverableKey(_))
        ));

        std::fs::remove_file(&path).ok();
    }

    /// An explicit `--ks-type` overrides detection, and a mismatched type fails to load.
    #[test]
    fn an_explicit_type_overrides_detection() {
        let data = JKS_MAGIC.to_be_bytes().to_vec();
        let err = KeyStore::load(&data, &[vec!['x'; 8]], KeyStoreFormat::Pkcs12).unwrap_err();
        assert!(matches!(err, KeyStoreError::WrongKeystorePassword(_)));
    }
}