use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Charset {
Utf8,
Ascii,
Latin1,
Utf16Le,
Utf16Be,
}
impl Charset {
pub fn encode(self, chars: &[char]) -> Vec<u8> {
match self {
Charset::Utf8 => {
let mut out = Vec::with_capacity(chars.len());
for &c in chars {
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
out
}
Charset::Ascii => chars
.iter()
.map(|&c| if (c as u32) <= 0x7f { c as u8 } else { b'?' })
.collect(),
Charset::Latin1 => chars
.iter()
.map(|&c| if (c as u32) <= 0xff { c as u8 } else { b'?' })
.collect(),
Charset::Utf16Le => {
let mut out = Vec::with_capacity(chars.len() * 2);
for &c in chars {
let mut buf = [0u16; 2];
for unit in c.encode_utf16(&mut buf) {
out.extend_from_slice(&unit.to_le_bytes());
}
}
out
}
Charset::Utf16Be => {
let mut out = Vec::with_capacity(chars.len() * 2);
for &c in chars {
let mut buf = [0u16; 2];
for unit in c.encode_utf16(&mut buf) {
out.extend_from_slice(&unit.to_be_bytes());
}
}
out
}
}
}
pub fn decode(self, bytes: &[u8]) -> Vec<char> {
match self {
Charset::Utf8 => String::from_utf8_lossy(bytes).chars().collect(),
Charset::Ascii => bytes
.iter()
.map(|&b| if b <= 0x7f { b as char } else { '\u{FFFD}' })
.collect(),
Charset::Latin1 => bytes.iter().map(|&b| b as char).collect(),
Charset::Utf16Le => decode_utf16(bytes, true),
Charset::Utf16Be => decode_utf16(bytes, false),
}
}
}
fn decode_utf16(bytes: &[u8], little_endian: bool) -> Vec<char> {
let mut units = Vec::with_capacity(bytes.len() / 2);
let mut i = 0;
while i + 1 < bytes.len() {
let pair = [bytes[i], bytes[i + 1]];
units.push(if little_endian {
u16::from_le_bytes(pair)
} else {
u16::from_be_bytes(pair)
});
i += 2;
}
if i < bytes.len() {
units.push(0xFFFD);
}
char::decode_utf16(units)
.map(|r| r.unwrap_or('\u{FFFD}'))
.collect()
}
pub fn default_charset() -> Charset {
Charset::Utf8
}
pub fn get_charset_by_name(charset_name: &str) -> Result<Charset, String> {
if charset_name.eq_ignore_ascii_case("cp65001") {
return Ok(Charset::Utf8);
}
let normalized = charset_name.to_ascii_lowercase().replace('_', "-");
match normalized.as_str() {
"utf-8" | "utf8" => Ok(Charset::Utf8),
"us-ascii" | "ascii" | "ansi-x3.4-1968" | "iso646-us" => Ok(Charset::Ascii),
"iso-8859-1" | "iso8859-1" | "latin1" | "l1" | "cp819" | "ibm819" => Ok(Charset::Latin1),
"utf-16le" | "utf16le" | "unicode" | "unicode-little" => Ok(Charset::Utf16Le),
"utf-16be" | "utf16be" | "unicode-big" | "unicodebigunmarked" => Ok(Charset::Utf16Be),
_ => Err(format!("Unsupported charset: {}", charset_name)),
}
}
pub type Password = Vec<char>;
pub const SPEC_STDIN: &str = "stdin";
struct PasswordSource {
reader: Box<dyn Read>,
pushback: Option<u8>,
}
impl PasswordSource {
fn new(reader: Box<dyn Read>) -> Self {
PasswordSource {
reader,
pushback: None,
}
}
fn next_byte(&mut self) -> io::Result<Option<u8>> {
if let Some(byte) = self.pushback.take() {
return Ok(Some(byte));
}
let mut buf = [0u8; 1];
loop {
match self.reader.read(&mut buf) {
Ok(0) => return Ok(None),
Ok(_) => return Ok(Some(buf[0])),
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
}
fn read_encoded_password(&mut self) -> io::Result<Vec<u8>> {
let mut result = Vec::new();
while let Some(b) = self.next_byte()? {
if b == b'\n' {
break;
} else if b == b'\r' {
match self.next_byte()? {
None => break,
Some(b'\n') => break,
Some(next) => self.pushback = Some(next),
}
}
result.push(b);
}
Ok(result)
}
}
pub struct PasswordRetriever {
console_encoding: Option<Charset>,
file_input_streams: HashMap<PathBuf, PasswordSource>,
stdin_source: Option<PasswordSource>,
closed: bool,
}
impl Default for PasswordRetriever {
fn default() -> Self {
Self::new()
}
}
impl PasswordRetriever {
pub fn new() -> Self {
PasswordRetriever {
console_encoding: get_console_encoding(),
file_input_streams: HashMap::new(),
stdin_source: None,
closed: false,
}
}
pub fn get_passwords(
&mut self,
spec: &str,
description: &str,
additional_pwd_encodings: &[Charset],
) -> io::Result<Vec<Password>> {
self.assert_not_closed()?;
if let Some(inline) = spec.strip_prefix("pass:") {
let pwd: Vec<char> = inline.chars().collect();
return Ok(self.passwords_from_chars(&pwd, additional_pwd_encodings));
}
if spec == SPEC_STDIN {
print!("{}: ", description);
io::stdout().flush()?;
let source = match self.stdin_source.as_mut() {
Some(source) => source,
None => {
self.stdin_source = Some(PasswordSource::new(Box::new(io::stdin())));
self.stdin_source.as_mut().unwrap()
}
};
let encoded_pwd = source.read_encoded_password()?;
if encoded_pwd.is_empty() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("Failed to read {}: standard input closed", description),
));
}
return Ok(self.passwords_from_bytes(
&encoded_pwd,
default_charset(),
additional_pwd_encodings,
));
}
if let Some(name) = spec.strip_prefix("file:") {
let file = canonicalize(name)?;
if !self.file_input_streams.contains_key(&file) {
let stream = File::open(&file)?;
self.file_input_streams
.insert(file.clone(), PasswordSource::new(Box::new(stream)));
}
let encoded_pwd = self
.file_input_streams
.get_mut(&file)
.expect("just inserted")
.read_encoded_password()?;
if encoded_pwd.is_empty() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"Failed to read {} : end of file reached in {}",
description,
file.display()
),
));
}
return Ok(self.passwords_from_bytes(
&encoded_pwd,
default_charset(),
additional_pwd_encodings,
));
}
if let Some(name) = spec.strip_prefix("env:") {
let value = std::env::var(name).ok();
let Some(value) = value else {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"Failed to read {}: environment variable {} not specified",
description, "null"
),
));
};
let chars: Vec<char> = value.chars().collect();
return Ok(self.passwords_from_chars(&chars, additional_pwd_encodings));
}
Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unsupported password spec for {}: {}", description, spec),
))
}
fn passwords_from_chars(
&self,
pwd: &[char],
additional_encodings: &[Charset],
) -> Vec<Password> {
let mut passwords = Vec::with_capacity(3);
self.add_passwords(&mut passwords, pwd, additional_encodings);
passwords
}
fn passwords_from_bytes(
&self,
encoded_pwd: &[u8],
encoding_for_decoding: Charset,
additional_encodings: &[Charset],
) -> Vec<Password> {
let mut passwords = Vec::with_capacity(4);
let pwd = encoding_for_decoding.decode(encoded_pwd);
self.add_passwords(&mut passwords, &pwd, additional_encodings);
add_password(&mut passwords, cast_bytes_to_chars(encoded_pwd));
passwords
}
fn add_passwords(
&self,
passwords: &mut Vec<Password>,
pwd: &[char],
additional_encodings: &[Charset],
) {
for &encoding in additional_encodings {
add_password(passwords, cast_bytes_to_chars(&encoding.encode(pwd)));
}
add_password(passwords, pwd.to_vec());
if let Some(console_encoding) = self.console_encoding {
add_password(
passwords,
cast_bytes_to_chars(&console_encoding.encode(pwd)),
);
}
add_password(
passwords,
cast_bytes_to_chars(&default_charset().encode(pwd)),
);
}
fn assert_not_closed(&self) -> io::Result<()> {
if self.closed {
return Err(io::Error::new(io::ErrorKind::BrokenPipe, "Closed"));
}
Ok(())
}
pub fn close(&mut self) {
self.file_input_streams.clear();
self.stdin_source = None;
self.closed = true;
}
}
impl Drop for PasswordRetriever {
fn drop(&mut self) {
self.close();
}
}
fn add_password(passwords: &mut Vec<Password>, password: Password) {
if passwords.contains(&password) {
return;
}
passwords.push(password);
}
fn cast_bytes_to_chars(bytes: &[u8]) -> Vec<char> {
bytes.iter().map(|&b| b as char).collect()
}
fn canonicalize(name: &str) -> io::Result<PathBuf> {
let path = PathBuf::from(name);
std::fs::canonicalize(&path)
}
fn get_console_encoding() -> Option<Charset> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn pass_spec_yields_the_password_verbatim() {
let mut r = PasswordRetriever::new();
let passwords = r
.get_passwords("pass:hunter2", "KeyStore password for signer #1", &[])
.unwrap();
assert!(passwords.contains(&"hunter2".chars().collect::<Vec<char>>()));
assert_eq!(passwords.len(), 1);
}
#[test]
fn env_spec_reads_the_named_variable() {
let name = "APKSIGNER_TEST_PASSWORD_ENV";
unsafe { std::env::set_var(name, "s3cret") };
let mut r = PasswordRetriever::new();
let passwords = r
.get_passwords(&format!("env:{}", name), "KeyStore password", &[])
.unwrap();
assert!(passwords.contains(&"s3cret".chars().collect::<Vec<char>>()));
unsafe { std::env::remove_var(name) };
}
#[test]
fn env_spec_reports_a_missing_variable_with_the_java_wording() {
let mut r = PasswordRetriever::new();
let err = r
.get_passwords(
"env:APKSIGNER_DEFINITELY_NOT_SET_12345",
"KeyStore password",
&[],
)
.unwrap_err();
assert_eq!(
err.to_string(),
"Failed to read KeyStore password: environment variable null not specified"
);
}
#[test]
fn unsupported_spec_is_rejected() {
let mut r = PasswordRetriever::new();
let err = r
.get_passwords("magic:thing", "KeyStore password", &[])
.unwrap_err();
assert_eq!(
err.to_string(),
"Unsupported password spec for KeyStore password: magic:thing"
);
}
#[test]
fn file_spec_reads_successive_lines() {
let dir = std::env::temp_dir();
let path = dir.join("apksigner_password_retriever_test.txt");
std::fs::write(&path, "first\nsecond\n").unwrap();
let mut r = PasswordRetriever::new();
let spec = format!("file:{}", path.display());
let first = r.get_passwords(&spec, "KeyStore password", &[]).unwrap();
let second = r.get_passwords(&spec, "Key password", &[]).unwrap();
assert!(first.contains(&"first".chars().collect::<Vec<char>>()));
assert!(second.contains(&"second".chars().collect::<Vec<char>>()));
std::fs::remove_file(&path).ok();
}
#[test]
fn additional_encodings_add_variants_for_non_ascii_passwords() {
let mut r = PasswordRetriever::new();
let passwords = r
.get_passwords("pass:\u{00e4}", "KeyStore password", &[Charset::Utf8])
.unwrap();
assert!(
passwords.len() >= 2,
"expected variants, got {:?}",
passwords
);
assert!(passwords.contains(&vec!['\u{00e4}']));
assert!(passwords.contains(&vec!['\u{00c3}', '\u{00a4}']));
}
#[test]
fn console_encoding_is_unknown_so_it_adds_no_variant() {
let r = PasswordRetriever::new();
assert!(r.console_encoding.is_none());
}
#[test]
fn closed_retriever_rejects_further_reads() {
let mut r = PasswordRetriever::new();
r.close();
let err = r.get_passwords("pass:x", "desc", &[]).unwrap_err();
assert_eq!(err.to_string(), "Closed");
}
#[test]
fn read_encoded_password_handles_crlf_lf_and_cr() {
fn read_all(input: &[u8]) -> Vec<Vec<u8>> {
let mut source = PasswordSource::new(Box::new(Cursor::new(input.to_vec())));
let mut lines = Vec::new();
loop {
let line = source.read_encoded_password().unwrap();
if line.is_empty() {
break;
}
lines.push(line);
}
lines
}
assert_eq!(
read_all(b"a\r\nb\nc\r"),
vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()]
);
}
#[test]
fn read_encoded_password_keeps_a_lone_carriage_return() {
let mut source = PasswordSource::new(Box::new(Cursor::new(b"a\rb\n".to_vec())));
assert_eq!(source.read_encoded_password().unwrap(), b"a\rb".to_vec());
}
#[test]
fn known_charset_names_resolve() {
assert_eq!(get_charset_by_name("utf-8").unwrap(), Charset::Utf8);
assert_eq!(get_charset_by_name("UTF-8").unwrap(), Charset::Utf8);
assert_eq!(get_charset_by_name("cp65001").unwrap(), Charset::Utf8);
assert_eq!(get_charset_by_name("US-ASCII").unwrap(), Charset::Ascii);
assert_eq!(get_charset_by_name("ISO-8859-1").unwrap(), Charset::Latin1);
assert!(get_charset_by_name("ibm437").is_err());
}
#[test]
fn charset_round_trips_ascii() {
let chars: Vec<char> = "hunter2".chars().collect();
let encoded = Charset::Utf8.encode(&chars);
assert_eq!(encoded, b"hunter2".to_vec());
assert_eq!(Charset::Utf8.decode(&encoded), chars);
}
}