242 lines
7.9 KiB
Rust
242 lines
7.9 KiB
Rust
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
use crate::Fail;
|
|
|
|
pub struct Signer {
|
|
pub keystore: PathBuf,
|
|
pub password: String,
|
|
pub alias: String,
|
|
}
|
|
|
|
pub fn release_signer() -> Result<Signer, Fail> {
|
|
let keystore = std::env::var_os("AI_APP_KEYSTORE")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| {
|
|
let config_home = std::env::var_os("XDG_CONFIG_HOME")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| {
|
|
PathBuf::from(std::env::var_os("HOME").unwrap()).join(".config")
|
|
});
|
|
config_home.join("ai-app").join("release.jks")
|
|
});
|
|
let alias = "ai-app".to_string();
|
|
let password_file = keystore.with_extension("jks.password");
|
|
|
|
if keystore.is_file() {
|
|
let password = std::fs::read_to_string(&password_file)
|
|
.map_err(|e| {
|
|
Fail::new(
|
|
"release key exists but its password file is unreadable",
|
|
&format!("{}: {e}", password_file.display()),
|
|
"restore the password file, or delete both and let this regenerate them",
|
|
)
|
|
})?
|
|
.trim()
|
|
.to_string();
|
|
return Ok(Signer {
|
|
keystore,
|
|
password,
|
|
alias,
|
|
});
|
|
}
|
|
|
|
let keytool = which_keytool()?;
|
|
if let Some(parent) = keystore.parent() {
|
|
std::fs::create_dir_all(parent).map_err(|e| {
|
|
Fail::new(
|
|
"could not create the keystore's directory",
|
|
&format!("{}: {e}", parent.display()),
|
|
"check permissions on that path",
|
|
)
|
|
})?;
|
|
}
|
|
let password = random_password();
|
|
write_owner_only(&password_file, format!("{password}\n").as_bytes())?;
|
|
|
|
let status = Command::new(&keytool)
|
|
.args(["-genkeypair", "-keystore"])
|
|
.arg(&keystore)
|
|
.args([
|
|
"-alias",
|
|
&alias,
|
|
"-keyalg",
|
|
"RSA",
|
|
"-keysize",
|
|
"2048",
|
|
"-validity",
|
|
"10000",
|
|
])
|
|
.args(["-storepass", &password, "-keypass", &password])
|
|
.args(["-dname", "CN=ai-app"])
|
|
.status()
|
|
.map_err(|e| {
|
|
Fail::new(
|
|
"failed to run keytool",
|
|
&format!("{}: {e}", keytool.display()),
|
|
"set JAVA_HOME to the JDK Gradle uses",
|
|
)
|
|
})?;
|
|
if !status.success() {
|
|
return Err(Fail::new(
|
|
"keytool exited with an error while generating the release key",
|
|
&format!("status: {status}"),
|
|
"check the keytool output above",
|
|
));
|
|
}
|
|
// Owner-only, matching build-apk.sh -- this key is what the phone
|
|
// recognises the app by, so it never goes in the repo and it stays
|
|
// unreadable to anything else on this machine.
|
|
set_owner_only(&keystore)?;
|
|
|
|
Ok(Signer {
|
|
keystore,
|
|
password,
|
|
alias,
|
|
})
|
|
}
|
|
|
|
/// The conventional Android debug key (`~/.android/debug.keystore`,
|
|
/// well-known password `android`, alias `androiddebugkey`) -- generated on
|
|
/// first use exactly the way Android Studio and Gradle's own debug signing
|
|
/// config do, so a `--debug` build here needs no setup and never touches
|
|
/// the real release key.
|
|
pub fn debug_signer() -> Result<Signer, Fail> {
|
|
let home = PathBuf::from(std::env::var_os("HOME").ok_or_else(|| {
|
|
Fail::new(
|
|
"no $HOME set",
|
|
"the debug keystore lives under ~/.android",
|
|
"set $HOME",
|
|
)
|
|
})?);
|
|
let keystore = home.join(".android").join("debug.keystore");
|
|
let alias = "androiddebugkey".to_string();
|
|
let password = "android".to_string();
|
|
|
|
if !keystore.is_file() {
|
|
let keytool = which_keytool()?;
|
|
std::fs::create_dir_all(keystore.parent().unwrap()).map_err(|e| {
|
|
Fail::new(
|
|
"could not create ~/.android",
|
|
&format!("{e}"),
|
|
"check permissions on your home directory",
|
|
)
|
|
})?;
|
|
let status = Command::new(&keytool)
|
|
.args(["-genkeypair", "-keystore"])
|
|
.arg(&keystore)
|
|
.args([
|
|
"-alias",
|
|
&alias,
|
|
"-keyalg",
|
|
"RSA",
|
|
"-keysize",
|
|
"2048",
|
|
"-validity",
|
|
"10000",
|
|
])
|
|
.args(["-storepass", &password, "-keypass", &password])
|
|
.args(["-dname", "CN=Android Debug,O=Android,C=US"])
|
|
.status()
|
|
.map_err(|e| {
|
|
Fail::new(
|
|
"failed to run keytool",
|
|
&format!("{}: {e}", keytool.display()),
|
|
"set JAVA_HOME to the JDK Gradle uses",
|
|
)
|
|
})?;
|
|
if !status.success() {
|
|
return Err(Fail::new(
|
|
"keytool exited with an error while generating the debug key",
|
|
&format!("status: {status}"),
|
|
"check the keytool output above",
|
|
));
|
|
}
|
|
}
|
|
Ok(Signer {
|
|
keystore,
|
|
password,
|
|
alias,
|
|
})
|
|
}
|
|
|
|
fn which_keytool() -> Result<PathBuf, Fail> {
|
|
if let Some(java_home) = std::env::var_os("JAVA_HOME") {
|
|
let candidate = PathBuf::from(java_home).join("bin").join("keytool");
|
|
if candidate.is_file() {
|
|
return Ok(candidate);
|
|
}
|
|
}
|
|
if Command::new("keytool").arg("-help").output().is_ok() {
|
|
return Ok(PathBuf::from("keytool"));
|
|
}
|
|
Err(Fail::new(
|
|
"no keytool available to generate the release key",
|
|
"checked $JAVA_HOME/bin/keytool and keytool on PATH",
|
|
"set JAVA_HOME to the JDK Gradle uses, or set AI_APP_KEYSTORE to an existing key",
|
|
))
|
|
}
|
|
|
|
fn random_password() -> String {
|
|
let mut bytes = [0u8; 24];
|
|
std::fs::File::open("/dev/urandom")
|
|
.and_then(|mut f| std::io::Read::read_exact(&mut f, &mut bytes))
|
|
.expect("/dev/urandom must be readable to generate a signing key password");
|
|
base64_no_padding(&bytes)
|
|
}
|
|
|
|
fn base64_no_padding(bytes: &[u8]) -> String {
|
|
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
let mut out = String::new();
|
|
for chunk in bytes.chunks(3) {
|
|
let b0 = chunk[0] as u32;
|
|
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
|
|
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
|
|
let n = (b0 << 16) | (b1 << 8) | b2;
|
|
out.push(ALPHABET[(n >> 18 & 0x3f) as usize] as char);
|
|
out.push(ALPHABET[(n >> 12 & 0x3f) as usize] as char);
|
|
if chunk.len() > 1 {
|
|
out.push(ALPHABET[(n >> 6 & 0x3f) as usize] as char);
|
|
}
|
|
if chunk.len() > 2 {
|
|
out.push(ALPHABET[(n & 0x3f) as usize] as char);
|
|
}
|
|
}
|
|
// build-apk.sh strips '/', '+' and '=' from its password (tr -d
|
|
// '/+='), so the value never needs quoting when it is passed as a
|
|
// command-line argument later.
|
|
out.retain(|c| c != '/' && c != '+' && c != '=');
|
|
out
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn write_owner_only(path: &std::path::Path, contents: &[u8]) -> Result<(), Fail> {
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.mode(0o600)
|
|
.open(path)
|
|
.and_then(|mut f| std::io::Write::write_all(&mut f, contents))
|
|
.map_err(|e| {
|
|
Fail::new(
|
|
"could not write the keystore password file",
|
|
&format!("{}: {e}", path.display()),
|
|
"check permissions on that directory",
|
|
)
|
|
})
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn set_owner_only(path: &std::path::Path) -> Result<(), Fail> {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).map_err(|e| {
|
|
Fail::new(
|
|
"could not restrict the keystore's permissions",
|
|
&format!("{}: {e}", path.display()),
|
|
"chmod 600 it by hand",
|
|
)
|
|
})
|
|
}
|