Files
ai-app/scripts/xtask/src/keystore.rs
T

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",
)
})
}