Update toes-matter and bump to 0.1.6
This commit is contained in:
parent
e1e6c99f08
commit
5c9f1d07d0
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -2394,7 +2394,7 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toes-matter"
|
name = "toes-matter"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-io",
|
"async-io",
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "toes-matter"
|
name = "toes-matter"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -39,10 +39,10 @@ fn main() -> toes_matter::Result<()> {
|
||||||
|
|
||||||
## Post-flash headless provisioning
|
## Post-flash headless provisioning
|
||||||
|
|
||||||
After flashing a Linux device reachable over SSH, run the host-side provisioning command from Linux or macOS:
|
After flashing a Linux device reachable over SSH, run the host-side credentials command from Linux or macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-provision -- \
|
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-credentials -- \
|
||||||
user@device-host device-001
|
user@device-host device-001
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -57,7 +57,7 @@ The older script is now just a wrapper around that command:
|
||||||
Set these if your service uses different paths:
|
Set these if your service uses different paths:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-provision -- \
|
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-credentials -- \
|
||||||
--remote-creds-dir /var/lib/my-app/creds \
|
--remote-creds-dir /var/lib/my-app/creds \
|
||||||
--remote-state-dir /var/lib/my-app/state \
|
--remote-state-dir /var/lib/my-app/state \
|
||||||
--service my-app.service \
|
--service my-app.service \
|
||||||
|
|
@ -67,7 +67,7 @@ cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --
|
||||||
Serial instead of network/SSH:
|
Serial instead of network/SSH:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-provision -- \
|
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-credentials -- \
|
||||||
--serial /dev/cu.usbserial-0001 \
|
--serial /dev/cu.usbserial-0001 \
|
||||||
--device-id device-001
|
--device-id device-001
|
||||||
```
|
```
|
||||||
|
|
@ -75,7 +75,7 @@ cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --
|
||||||
If the serial console is already at a shell prompt, omit login flags. If it is at `login:`, provide credentials:
|
If the serial console is already at a shell prompt, omit login flags. If it is at `login:`, provide credentials:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-provision -- \
|
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-credentials -- \
|
||||||
--serial /dev/cu.usbserial-0001 \
|
--serial /dev/cu.usbserial-0001 \
|
||||||
--login root \
|
--login root \
|
||||||
--password 'your-password' \
|
--password 'your-password' \
|
||||||
|
|
|
||||||
|
|
@ -62,4 +62,4 @@ else
|
||||||
args+=(--service "$SERVICE")
|
args+=(--service "$SERVICE")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec cargo run --quiet --no-default-features --manifest-path "$CRATE_DIR/Cargo.toml" --bin toes-matter-provision -- "${args[@]}" "${TARGET_ARGS[@]}"
|
exec cargo run --quiet --no-default-features --manifest-path "$CRATE_DIR/Cargo.toml" --bin toes-matter-credentials -- "${args[@]}" "${TARGET_ARGS[@]}"
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,766 @@
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
fn main() -> toes_matter::Result<()> {
|
use std::ffi::OsString;
|
||||||
env_logger::init_from_env(
|
use std::io::{Read, Write};
|
||||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
use std::path::{Path, PathBuf};
|
||||||
);
|
use std::process::Command;
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
let mut args = std::env::args_os().skip(1);
|
use rand::RngCore;
|
||||||
let dir = args.next().unwrap_or_else(|| "./creds".into());
|
|
||||||
|
|
||||||
if args.next().is_some() {
|
use rs_matter::dm::clusters::dev_att::DeviceAttestation;
|
||||||
eprintln!("Usage: toes-matter-creds [CREDS_DIR]");
|
use rs_matter::dm::devices::test::{TEST_DEV_ATT, TEST_DEV_DET};
|
||||||
std::process::exit(2);
|
use rs_matter::pairing::qr::{no_optional_data, CommFlowType, QrPayload};
|
||||||
|
use rs_matter::pairing::DiscoveryCapabilities;
|
||||||
|
use rs_matter::sc::pase::{Spake2pVerifierPassword, Spake2pVerifierPasswordRef};
|
||||||
|
use rs_matter::BasicCommData;
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
const DEFAULT_REMOTE_CREDS_DIR: &str = "/var/lib/toes-matter/creds";
|
||||||
|
const DEFAULT_REMOTE_STATE_DIR: &str = "/var/lib/toes-matter/state";
|
||||||
|
const DEFAULT_SERVICE: &str = "toes-matter.service";
|
||||||
|
const DEFAULT_BAUD: u32 = 115_200;
|
||||||
|
const DEFAULT_SERIAL_DELAY_MS: u64 = 15;
|
||||||
|
const DEFAULT_SERIAL_LOGIN_TIMEOUT_SECS: u64 = 30;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let opts = Options::parse()?;
|
||||||
|
|
||||||
|
let generated = generate_credentials(&opts.creds_dir)?;
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("==> Pairing info for device {}", opts.device_id);
|
||||||
|
println!("Manual code: {}", generated.manual_code);
|
||||||
|
println!("QR payload : {}", generated.qr_code);
|
||||||
|
|
||||||
|
render_qr(&generated.qr_code);
|
||||||
|
|
||||||
|
if let Some(serial) = opts.serial.as_deref() {
|
||||||
|
write_to_serial(serial, &opts)?;
|
||||||
|
} else if let Some(target) = opts.target.as_deref() {
|
||||||
|
copy_to_device(target, &opts)?;
|
||||||
|
} else {
|
||||||
|
println!();
|
||||||
|
println!("==> No target/serial specified; generated local creds only");
|
||||||
}
|
}
|
||||||
|
|
||||||
futures_lite::future::block_on(toes_matter::generate_credentials(dir))
|
println!();
|
||||||
|
println!("==> Done");
|
||||||
|
println!("Local copy saved at: {}", opts.creds_dir.display());
|
||||||
|
|
||||||
|
if opts.target.is_some() || opts.serial.is_some() {
|
||||||
|
println!("Remote app env should use:");
|
||||||
|
println!(" TOES_MATTER_CREDS_DIR={}", opts.remote_creds_dir);
|
||||||
|
println!(" TOES_MATTER_STATE_DIR={}", opts.remote_state_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Options {
|
||||||
|
target: Option<String>,
|
||||||
|
serial: Option<String>,
|
||||||
|
device_id: String,
|
||||||
|
creds_dir: PathBuf,
|
||||||
|
remote_creds_dir: String,
|
||||||
|
remote_state_dir: String,
|
||||||
|
service: Option<String>,
|
||||||
|
ssh_opts: Vec<String>,
|
||||||
|
baud: u32,
|
||||||
|
serial_delay: Duration,
|
||||||
|
serial_login: Option<String>,
|
||||||
|
serial_password: Option<String>,
|
||||||
|
serial_prompts: Vec<String>,
|
||||||
|
serial_login_timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Options {
|
||||||
|
fn parse() -> Result<Self> {
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
|
||||||
|
let mut target = None;
|
||||||
|
let mut serial = std::env::var("SERIAL_PORT").ok();
|
||||||
|
let mut device_id = None;
|
||||||
|
let mut creds_dir = std::env::var_os("CREDS_DIR").map(PathBuf::from);
|
||||||
|
let mut remote_creds_dir =
|
||||||
|
std::env::var("REMOTE_CREDS_DIR").unwrap_or_else(|_| DEFAULT_REMOTE_CREDS_DIR.into());
|
||||||
|
let mut remote_state_dir =
|
||||||
|
std::env::var("REMOTE_STATE_DIR").unwrap_or_else(|_| DEFAULT_REMOTE_STATE_DIR.into());
|
||||||
|
let mut service = match std::env::var("SERVICE") {
|
||||||
|
Ok(value) if value.is_empty() => None,
|
||||||
|
Ok(value) => Some(value),
|
||||||
|
Err(_) => Some(DEFAULT_SERVICE.into()),
|
||||||
|
};
|
||||||
|
let mut ssh_opts = split_ssh_opts(std::env::var("SSH_OPTS").unwrap_or_default());
|
||||||
|
let mut baud = std::env::var("SERIAL_BAUD")
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(DEFAULT_BAUD);
|
||||||
|
let mut serial_delay_ms = std::env::var("SERIAL_DELAY_MS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(DEFAULT_SERIAL_DELAY_MS);
|
||||||
|
let mut serial_login = std::env::var("SERIAL_LOGIN").ok();
|
||||||
|
let mut serial_password = std::env::var("SERIAL_PASSWORD").ok();
|
||||||
|
let mut serial_prompts = std::env::var("SERIAL_PROMPT")
|
||||||
|
.ok()
|
||||||
|
.map(|prompt| vec![prompt])
|
||||||
|
.unwrap_or_else(|| vec!["# ".into(), "$ ".into()]);
|
||||||
|
let mut serial_login_timeout_secs = std::env::var("SERIAL_LOGIN_TIMEOUT_SECS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(DEFAULT_SERIAL_LOGIN_TIMEOUT_SECS);
|
||||||
|
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"-h" | "--help" => {
|
||||||
|
print_usage();
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
"--creds-dir" => {
|
||||||
|
creds_dir = Some(PathBuf::from(next_arg(&mut args, "--creds-dir")?))
|
||||||
|
}
|
||||||
|
"--remote-creds-dir" => {
|
||||||
|
remote_creds_dir = next_arg(&mut args, "--remote-creds-dir")?
|
||||||
|
}
|
||||||
|
"--remote-state-dir" => {
|
||||||
|
remote_state_dir = next_arg(&mut args, "--remote-state-dir")?
|
||||||
|
}
|
||||||
|
"--service" => service = Some(next_arg(&mut args, "--service")?),
|
||||||
|
"--no-restart" => service = None,
|
||||||
|
"--device-id" => device_id = Some(next_arg(&mut args, "--device-id")?),
|
||||||
|
"--ssh-opt" => ssh_opts.push(next_arg(&mut args, "--ssh-opt")?),
|
||||||
|
"--serial" => serial = Some(next_arg(&mut args, "--serial")?),
|
||||||
|
"--baud" => baud = next_arg(&mut args, "--baud")?.parse()?,
|
||||||
|
"--serial-delay-ms" => {
|
||||||
|
serial_delay_ms = next_arg(&mut args, "--serial-delay-ms")?.parse()?
|
||||||
|
}
|
||||||
|
"--login" => serial_login = Some(next_arg(&mut args, "--login")?),
|
||||||
|
"--password" => serial_password = Some(next_arg(&mut args, "--password")?),
|
||||||
|
"--prompt" => serial_prompts.push(next_arg(&mut args, "--prompt")?),
|
||||||
|
"--login-timeout-secs" => {
|
||||||
|
serial_login_timeout_secs =
|
||||||
|
next_arg(&mut args, "--login-timeout-secs")?.parse()?
|
||||||
|
}
|
||||||
|
_ if arg.starts_with('-') => return Err(format!("unknown option: {arg}").into()),
|
||||||
|
_ => {
|
||||||
|
if target.is_none() {
|
||||||
|
target = Some(arg);
|
||||||
|
} else if device_id.is_none() {
|
||||||
|
device_id = Some(arg);
|
||||||
|
} else {
|
||||||
|
return Err("too many positional arguments".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if serial.is_some() && target.is_some() && device_id.is_none() {
|
||||||
|
device_id = target.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
if serial.is_some() && target.is_some() {
|
||||||
|
return Err("use either SSH target or --serial, not both".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_id = device_id.unwrap_or_else(default_device_id);
|
||||||
|
let creds_dir = creds_dir.unwrap_or_else(|| {
|
||||||
|
PathBuf::from("manufacturing")
|
||||||
|
.join(&device_id)
|
||||||
|
.join("creds")
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
target,
|
||||||
|
serial,
|
||||||
|
device_id,
|
||||||
|
creds_dir,
|
||||||
|
remote_creds_dir,
|
||||||
|
remote_state_dir,
|
||||||
|
service,
|
||||||
|
ssh_opts,
|
||||||
|
baud,
|
||||||
|
serial_delay: Duration::from_millis(serial_delay_ms),
|
||||||
|
serial_login,
|
||||||
|
serial_password,
|
||||||
|
serial_prompts,
|
||||||
|
serial_login_timeout: Duration::from_secs(serial_login_timeout_secs),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_usage() {
|
||||||
|
eprintln!(
|
||||||
|
"Usage: toes-matter-credentials [OPTIONS] [user@device-host] [device-id]\n\n\
|
||||||
|
Generates Matter setup data, prints the QR/manual code, and optionally copies it to a Linux device over SSH or serial console.\n\n\
|
||||||
|
Options:\n\
|
||||||
|
--creds-dir DIR Local output dir [env: CREDS_DIR]\n\
|
||||||
|
--remote-creds-dir DIR Remote creds dir [default: {DEFAULT_REMOTE_CREDS_DIR}]\n\
|
||||||
|
--remote-state-dir DIR Remote state dir [default: {DEFAULT_REMOTE_STATE_DIR}]\n\
|
||||||
|
--service NAME Service to restart [default: {DEFAULT_SERVICE}]\n\
|
||||||
|
--no-restart Do not restart a remote service\n\
|
||||||
|
--device-id ID Device id for local-only generation\n\
|
||||||
|
--ssh-opt ARG Extra ssh/scp option; may be repeated [env: SSH_OPTS]\n\
|
||||||
|
--serial PORT Write install script to a serial shell [env: SERIAL_PORT]\n\
|
||||||
|
--baud BAUD Configure serial baud [default: 115200, env: SERIAL_BAUD]\n\
|
||||||
|
--serial-delay-ms MS Delay between serial lines [default: 15, env: SERIAL_DELAY_MS]\n\
|
||||||
|
--login USER Log in on serial before provisioning [env: SERIAL_LOGIN]\n\
|
||||||
|
--password PASS Serial login password [env: SERIAL_PASSWORD]\n\
|
||||||
|
--prompt PROMPT Shell prompt marker; may repeat [env: SERIAL_PROMPT]\n\
|
||||||
|
--login-timeout-secs N Serial login timeout [default: 30, env: SERIAL_LOGIN_TIMEOUT_SECS]\n\
|
||||||
|
-h, --help Show this help"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_arg(args: &mut impl Iterator<Item = String>, option: &str) -> Result<String> {
|
||||||
|
args.next()
|
||||||
|
.ok_or_else(|| format!("{option} requires an argument").into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_device_id() -> String {
|
||||||
|
let secs = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
format!("device-{secs}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_ssh_opts(value: String) -> Vec<String> {
|
||||||
|
value
|
||||||
|
.split_whitespace()
|
||||||
|
.filter(|part| !part.is_empty())
|
||||||
|
.map(str::to_owned)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GeneratedCredentials {
|
||||||
|
manual_code: String,
|
||||||
|
qr_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_credentials(path: &Path) -> Result<GeneratedCredentials> {
|
||||||
|
std::fs::create_dir_all(path)?;
|
||||||
|
|
||||||
|
std::fs::write(path.join("dac.der"), TEST_DEV_ATT.dac())?;
|
||||||
|
std::fs::write(path.join("pai.der"), TEST_DEV_ATT.pai())?;
|
||||||
|
std::fs::write(
|
||||||
|
path.join("certification-declaration.der"),
|
||||||
|
TEST_DEV_ATT.cert_declaration(),
|
||||||
|
)?;
|
||||||
|
std::fs::write(
|
||||||
|
path.join("dac-private-key.raw"),
|
||||||
|
TEST_DEV_ATT.dac_priv_key().access(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let comm = read_comm_data(path)?.unwrap_or_else(random_comm_data);
|
||||||
|
let payload = QrPayload::new_from_basic_info(
|
||||||
|
DiscoveryCapabilities::BLE,
|
||||||
|
CommFlowType::Standard,
|
||||||
|
comm.clone(),
|
||||||
|
&TEST_DEV_DET,
|
||||||
|
no_optional_data,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut qr_buf = [0_u8; 512];
|
||||||
|
let (qr_text, _) = payload.as_str(&mut qr_buf)?;
|
||||||
|
|
||||||
|
let manual_code = comm.compute_pretty_pairing_code().to_string();
|
||||||
|
let qr_code = qr_text.to_owned();
|
||||||
|
|
||||||
|
let setup = format!(
|
||||||
|
"setup_passcode={}\n\
|
||||||
|
discriminator={}\n\
|
||||||
|
manual_code={}\n\
|
||||||
|
qr_code={}\n",
|
||||||
|
comm_passcode(&comm),
|
||||||
|
comm.discriminator,
|
||||||
|
manual_code,
|
||||||
|
qr_code,
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::write(path.join("setup.txt"), setup)?;
|
||||||
|
|
||||||
|
Ok(GeneratedCredentials {
|
||||||
|
manual_code,
|
||||||
|
qr_code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_comm_data(path: &Path) -> Result<Option<BasicCommData>> {
|
||||||
|
let setup_path = path.join("setup.txt");
|
||||||
|
let setup = match std::fs::read_to_string(setup_path) {
|
||||||
|
Ok(setup) => setup,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let passcode = parse_setup_u32(&setup, "setup_passcode")?;
|
||||||
|
let discriminator = parse_setup_u32(&setup, "discriminator")?;
|
||||||
|
|
||||||
|
if discriminator > 0x0fff || !valid_setup_passcode(passcode) {
|
||||||
|
return Err("invalid setup.txt passcode/discriminator".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(make_comm_data(passcode, discriminator as u16)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_setup_u32(setup: &str, key: &str) -> Result<u32> {
|
||||||
|
let value = setup
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix(key)?.strip_prefix('='))
|
||||||
|
.ok_or_else(|| format!("setup.txt missing {key}"))?;
|
||||||
|
|
||||||
|
value
|
||||||
|
.parse()
|
||||||
|
.map_err(|err| format!("invalid {key}: {err}").into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_comm_data() -> BasicCommData {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
let passcode = loop {
|
||||||
|
let passcode = (rng.next_u32() % 99_999_998) + 1;
|
||||||
|
if valid_setup_passcode(passcode) {
|
||||||
|
break passcode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let discriminator = (rng.next_u32() & 0x0fff) as u16;
|
||||||
|
|
||||||
|
make_comm_data(passcode, discriminator)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_comm_data(passcode: u32, discriminator: u16) -> BasicCommData {
|
||||||
|
BasicCommData {
|
||||||
|
password: Spake2pVerifierPassword::new_from_ref(Spake2pVerifierPasswordRef::new(
|
||||||
|
&passcode.to_le_bytes(),
|
||||||
|
)),
|
||||||
|
discriminator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn comm_passcode(comm: &BasicCommData) -> u32 {
|
||||||
|
u32::from_le_bytes(*comm.password.access())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_setup_passcode(passcode: u32) -> bool {
|
||||||
|
const INVALID: &[u32] = &[
|
||||||
|
0, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888,
|
||||||
|
99999999, 12345678, 87654321,
|
||||||
|
];
|
||||||
|
|
||||||
|
(1..=99_999_998).contains(&passcode) && !INVALID.contains(&passcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_qr(qr_code: &str) {
|
||||||
|
let status = Command::new("qrencode")
|
||||||
|
.arg("-t")
|
||||||
|
.arg("ANSIUTF8")
|
||||||
|
.arg(qr_code)
|
||||||
|
.status();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Ok(status) if status.success() => {}
|
||||||
|
Ok(_) | Err(_) => {
|
||||||
|
println!();
|
||||||
|
println!("Tip: install qrencode to render the QR in this terminal:");
|
||||||
|
println!(" macOS: brew install qrencode");
|
||||||
|
println!(" Debian/Ubuntu: sudo apt install qrencode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_to_serial(port: &str, opts: &Options) -> Result<()> {
|
||||||
|
configure_serial(port, opts.baud)?;
|
||||||
|
|
||||||
|
let script = build_remote_install_script(opts)?;
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"==> Writing install script to serial port {port} at {} baud",
|
||||||
|
opts.baud
|
||||||
|
);
|
||||||
|
if opts.serial_login.is_some() {
|
||||||
|
println!(" Will log in on the serial console first.");
|
||||||
|
} else {
|
||||||
|
println!(" This assumes the serial console is already at a shell prompt.");
|
||||||
|
}
|
||||||
|
if cfg!(target_os = "macos") && port.starts_with("/dev/tty.") {
|
||||||
|
println!(
|
||||||
|
" Tip: on macOS, /dev/cu.* is usually better than /dev/tty.* for outbound serial."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut serial = std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(port)?;
|
||||||
|
serial.write_all(b"\n")?;
|
||||||
|
serial.flush()?;
|
||||||
|
|
||||||
|
if opts.serial_login.is_some() {
|
||||||
|
login_to_serial(&mut serial, opts)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in script.lines() {
|
||||||
|
serial.write_all(line.as_bytes())?;
|
||||||
|
serial.write_all(b"\n")?;
|
||||||
|
serial.flush()?;
|
||||||
|
std::thread::sleep(opts.serial_delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("==> Serial install script sent");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_serial(port: &str, baud: u32) -> Result<()> {
|
||||||
|
let port_flag = if cfg!(any(
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_os = "openbsd",
|
||||||
|
target_os = "netbsd"
|
||||||
|
)) {
|
||||||
|
"-f"
|
||||||
|
} else {
|
||||||
|
"-F"
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = Command::new("stty")
|
||||||
|
.arg(port_flag)
|
||||||
|
.arg(port)
|
||||||
|
.arg(baud.to_string())
|
||||||
|
.arg("raw")
|
||||||
|
.arg("-echo")
|
||||||
|
.arg("-ixon")
|
||||||
|
.arg("-ixoff")
|
||||||
|
.arg("min")
|
||||||
|
.arg("0")
|
||||||
|
.arg("time")
|
||||||
|
.arg("5")
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("stty failed for {port} with status {status}").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login_to_serial(serial: &mut std::fs::File, opts: &Options) -> Result<()> {
|
||||||
|
let login = opts
|
||||||
|
.serial_login
|
||||||
|
.as_deref()
|
||||||
|
.ok_or("serial login requested without a username")?;
|
||||||
|
|
||||||
|
println!("==> Waiting for serial login prompt or shell prompt...");
|
||||||
|
serial.write_all(b"\n")?;
|
||||||
|
serial.flush()?;
|
||||||
|
|
||||||
|
let mut initial_needles = vec![b"login:".to_vec(), b"Login:".to_vec()];
|
||||||
|
initial_needles.extend(
|
||||||
|
opts.serial_prompts
|
||||||
|
.iter()
|
||||||
|
.map(|prompt| prompt.as_bytes().to_vec()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let initial = wait_for_any(serial, &initial_needles, opts.serial_login_timeout)?;
|
||||||
|
|
||||||
|
if initial >= 2 {
|
||||||
|
println!("==> Serial console already appears to be at a shell prompt");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("==> Sending serial login username: {login}");
|
||||||
|
serial.write_all(login.as_bytes())?;
|
||||||
|
serial.write_all(b"\n")?;
|
||||||
|
serial.flush()?;
|
||||||
|
|
||||||
|
let mut after_user_needles = vec![b"Password:".to_vec(), b"password:".to_vec()];
|
||||||
|
after_user_needles.extend(
|
||||||
|
opts.serial_prompts
|
||||||
|
.iter()
|
||||||
|
.map(|prompt| prompt.as_bytes().to_vec()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let after_user = wait_for_any(serial, &after_user_needles, opts.serial_login_timeout)?;
|
||||||
|
|
||||||
|
if after_user < 2 {
|
||||||
|
let password = opts
|
||||||
|
.serial_password
|
||||||
|
.as_deref()
|
||||||
|
.ok_or("serial password prompt detected; pass --password or set SERIAL_PASSWORD")?;
|
||||||
|
|
||||||
|
println!("==> Sending serial login password");
|
||||||
|
serial.write_all(password.as_bytes())?;
|
||||||
|
serial.write_all(b"\n")?;
|
||||||
|
serial.flush()?;
|
||||||
|
|
||||||
|
let prompt_needles = opts
|
||||||
|
.serial_prompts
|
||||||
|
.iter()
|
||||||
|
.map(|prompt| prompt.as_bytes().to_vec())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
wait_for_any(serial, &prompt_needles, opts.serial_login_timeout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("==> Serial login complete");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_any(
|
||||||
|
serial: &mut std::fs::File,
|
||||||
|
needles: &[Vec<u8>],
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<usize> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let mut seen = Vec::<u8>::new();
|
||||||
|
let mut buf = [0_u8; 256];
|
||||||
|
|
||||||
|
while start.elapsed() < timeout {
|
||||||
|
match serial.read(&mut buf) {
|
||||||
|
Ok(0) => {
|
||||||
|
std::thread::sleep(Duration::from_millis(25));
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
std::io::stdout().write_all(&buf[..n])?;
|
||||||
|
std::io::stdout().flush()?;
|
||||||
|
|
||||||
|
seen.extend_from_slice(&buf[..n]);
|
||||||
|
if seen.len() > 8192 {
|
||||||
|
let keep_from = seen.len() - 4096;
|
||||||
|
seen.drain(..keep_from);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(index) = find_needle(&seen, needles) {
|
||||||
|
return Ok(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tail = String::from_utf8_lossy(&seen);
|
||||||
|
Err(format!("timed out waiting for serial prompt; recent output: {tail:?}").into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_needle(haystack: &[u8], needles: &[Vec<u8>]) -> Option<usize> {
|
||||||
|
needles.iter().position(|needle| {
|
||||||
|
!needle.is_empty()
|
||||||
|
&& haystack
|
||||||
|
.windows(needle.len())
|
||||||
|
.any(|window| window == needle.as_slice())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_remote_install_script(opts: &Options) -> Result<String> {
|
||||||
|
let mut script = String::new();
|
||||||
|
|
||||||
|
script.push_str("set -e\n");
|
||||||
|
script.push_str(&format!(
|
||||||
|
"REMOTE_CREDS_DIR={}\nREMOTE_STATE_DIR={}\n",
|
||||||
|
shell_quote(&opts.remote_creds_dir),
|
||||||
|
shell_quote(&opts.remote_state_dir),
|
||||||
|
));
|
||||||
|
script.push_str("sudo rm -rf \"$REMOTE_CREDS_DIR\"\n");
|
||||||
|
script.push_str("sudo mkdir -p \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"\n");
|
||||||
|
|
||||||
|
for file in [
|
||||||
|
"dac.der",
|
||||||
|
"pai.der",
|
||||||
|
"certification-declaration.der",
|
||||||
|
"dac-private-key.raw",
|
||||||
|
"setup.txt",
|
||||||
|
] {
|
||||||
|
let path = opts.creds_dir.join(file);
|
||||||
|
let data = std::fs::read(&path)?;
|
||||||
|
let marker = format!(
|
||||||
|
"TOES_MATTER_{}",
|
||||||
|
file.replace(['.', '-'], "_").to_uppercase()
|
||||||
|
);
|
||||||
|
|
||||||
|
script.push_str(&format!(
|
||||||
|
"base64 -d <<'{marker}' | sudo tee \"$REMOTE_CREDS_DIR/{file}\" >/dev/null\n"
|
||||||
|
));
|
||||||
|
script.push_str(&base64_wrapped(&data));
|
||||||
|
script.push_str(&format!("\n{marker}\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
script.push_str("sudo chmod -R go-rwx \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"\n");
|
||||||
|
|
||||||
|
if let Some(service) = opts.service.as_deref() {
|
||||||
|
script.push_str(&format!(
|
||||||
|
"if command -v systemctl >/dev/null 2>&1 && \\\n (systemctl list-unit-files {service} >/dev/null 2>&1 || systemctl status {service} >/dev/null 2>&1); then\n\
|
||||||
|
sudo systemctl restart {service}\n\
|
||||||
|
else\n\
|
||||||
|
echo 'Service {service_display} not found; skipping restart'\n\
|
||||||
|
fi\n",
|
||||||
|
service = shell_quote(service),
|
||||||
|
service_display = service,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
script.push_str("echo 'toes-matter serial provisioning complete'\n");
|
||||||
|
|
||||||
|
Ok(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base64_wrapped(data: &[u8]) -> String {
|
||||||
|
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut line_len = 0_usize;
|
||||||
|
|
||||||
|
for chunk in data.chunks(3) {
|
||||||
|
let b0 = chunk[0];
|
||||||
|
let b1 = *chunk.get(1).unwrap_or(&0);
|
||||||
|
let b2 = *chunk.get(2).unwrap_or(&0);
|
||||||
|
|
||||||
|
let encoded = [
|
||||||
|
TABLE[(b0 >> 2) as usize] as char,
|
||||||
|
TABLE[(((b0 & 0b0000_0011) << 4) | (b1 >> 4)) as usize] as char,
|
||||||
|
if chunk.len() > 1 {
|
||||||
|
TABLE[(((b1 & 0b0000_1111) << 2) | (b2 >> 6)) as usize] as char
|
||||||
|
} else {
|
||||||
|
'='
|
||||||
|
},
|
||||||
|
if chunk.len() > 2 {
|
||||||
|
TABLE[(b2 & 0b0011_1111) as usize] as char
|
||||||
|
} else {
|
||||||
|
'='
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for ch in encoded {
|
||||||
|
if line_len == 76 {
|
||||||
|
out.push('\n');
|
||||||
|
line_len = 0;
|
||||||
|
}
|
||||||
|
out.push(ch);
|
||||||
|
line_len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_to_device(target: &str, opts: &Options) -> Result<()> {
|
||||||
|
let remote_tmp = format!(
|
||||||
|
"/tmp/toes-matter-creds-{}-{}",
|
||||||
|
sanitize_path_component(&opts.device_id),
|
||||||
|
std::process::id(),
|
||||||
|
);
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("==> Copying creds to {target}:{}", opts.remote_creds_dir);
|
||||||
|
|
||||||
|
run_status(
|
||||||
|
"ssh",
|
||||||
|
command_with_args(
|
||||||
|
"ssh",
|
||||||
|
opts.ssh_opts.iter().map(OsString::from).chain([
|
||||||
|
OsString::from(target),
|
||||||
|
OsString::from(format!(
|
||||||
|
"rm -rf {} && mkdir -p {}",
|
||||||
|
shell_quote(&remote_tmp),
|
||||||
|
shell_quote(&remote_tmp),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut scp_args = opts.ssh_opts.iter().map(OsString::from).collect::<Vec<_>>();
|
||||||
|
scp_args.push(OsString::from("-r"));
|
||||||
|
scp_args.push(opts.creds_dir.join(".").into_os_string());
|
||||||
|
scp_args.push(OsString::from(format!("{target}:{remote_tmp}/")));
|
||||||
|
|
||||||
|
run_status("scp", command_with_args("scp", scp_args))?;
|
||||||
|
|
||||||
|
let parent = remote_parent(&opts.remote_creds_dir);
|
||||||
|
let mut remote_script = format!(
|
||||||
|
"set -e\n\
|
||||||
|
sudo rm -rf {remote_creds}\n\
|
||||||
|
sudo mkdir -p {parent} {remote_state}\n\
|
||||||
|
sudo mv {remote_tmp} {remote_creds}\n\
|
||||||
|
sudo chmod -R go-rwx {remote_creds} {remote_state}\n",
|
||||||
|
remote_creds = shell_quote(&opts.remote_creds_dir),
|
||||||
|
parent = shell_quote(&parent),
|
||||||
|
remote_state = shell_quote(&opts.remote_state_dir),
|
||||||
|
remote_tmp = shell_quote(&remote_tmp),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(service) = opts.service.as_deref() {
|
||||||
|
remote_script.push_str(&format!(
|
||||||
|
"if systemctl list-unit-files {service} >/dev/null 2>&1 || systemctl status {service} >/dev/null 2>&1; then\n\
|
||||||
|
sudo systemctl restart {service}\n\
|
||||||
|
else\n\
|
||||||
|
echo 'Service {service_display} not found; skipping restart'\n\
|
||||||
|
fi\n",
|
||||||
|
service = shell_quote(service),
|
||||||
|
service_display = service,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
run_status(
|
||||||
|
"ssh",
|
||||||
|
command_with_args(
|
||||||
|
"ssh",
|
||||||
|
opts.ssh_opts
|
||||||
|
.iter()
|
||||||
|
.map(OsString::from)
|
||||||
|
.chain([OsString::from(target), OsString::from(remote_script)]),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_with_args(program: &str, args: impl IntoIterator<Item = OsString>) -> Command {
|
||||||
|
let mut command = Command::new(program);
|
||||||
|
command.args(args);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_status(label: &str, mut command: Command) -> Result<()> {
|
||||||
|
let status = command.status()?;
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("{label} failed with status {status}").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_path_component(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.chars()
|
||||||
|
.map(|ch| match ch {
|
||||||
|
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => ch,
|
||||||
|
_ => '_',
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_parent(path: &str) -> String {
|
||||||
|
path.rsplit_once('/')
|
||||||
|
.map(|(parent, _)| if parent.is_empty() { "/" } else { parent })
|
||||||
|
.unwrap_or(".")
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shell_quote(value: &str) -> String {
|
||||||
|
let mut quoted = String::with_capacity(value.len() + 2);
|
||||||
|
quoted.push('\'');
|
||||||
|
for ch in value.chars() {
|
||||||
|
if ch == '\'' {
|
||||||
|
quoted.push_str("'\\''");
|
||||||
|
} else {
|
||||||
|
quoted.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quoted.push('\'');
|
||||||
|
quoted
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,766 +1,12 @@
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
use std::ffi::OsString;
|
fn main() -> toes_matter::Result<()> {
|
||||||
use std::io::{Read, Write};
|
env_logger::init_from_env(
|
||||||
use std::path::{Path, PathBuf};
|
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||||
use std::process::Command;
|
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use rand::RngCore;
|
|
||||||
|
|
||||||
use rs_matter::dm::clusters::dev_att::DeviceAttestation;
|
|
||||||
use rs_matter::dm::devices::test::{TEST_DEV_ATT, TEST_DEV_DET};
|
|
||||||
use rs_matter::pairing::qr::{no_optional_data, CommFlowType, QrPayload};
|
|
||||||
use rs_matter::pairing::DiscoveryCapabilities;
|
|
||||||
use rs_matter::sc::pase::{Spake2pVerifierPassword, Spake2pVerifierPasswordRef};
|
|
||||||
use rs_matter::BasicCommData;
|
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
|
||||||
|
|
||||||
const DEFAULT_REMOTE_CREDS_DIR: &str = "/var/lib/toes-matter/creds";
|
|
||||||
const DEFAULT_REMOTE_STATE_DIR: &str = "/var/lib/toes-matter/state";
|
|
||||||
const DEFAULT_SERVICE: &str = "toes-matter.service";
|
|
||||||
const DEFAULT_BAUD: u32 = 115_200;
|
|
||||||
const DEFAULT_SERIAL_DELAY_MS: u64 = 15;
|
|
||||||
const DEFAULT_SERIAL_LOGIN_TIMEOUT_SECS: u64 = 30;
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let opts = Options::parse()?;
|
|
||||||
|
|
||||||
let generated = generate_credentials(&opts.creds_dir)?;
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("==> Pairing info for device {}", opts.device_id);
|
|
||||||
println!("Manual code: {}", generated.manual_code);
|
|
||||||
println!("QR payload : {}", generated.qr_code);
|
|
||||||
|
|
||||||
render_qr(&generated.qr_code);
|
|
||||||
|
|
||||||
if let Some(serial) = opts.serial.as_deref() {
|
|
||||||
write_to_serial(serial, &opts)?;
|
|
||||||
} else if let Some(target) = opts.target.as_deref() {
|
|
||||||
copy_to_device(target, &opts)?;
|
|
||||||
} else {
|
|
||||||
println!();
|
|
||||||
println!("==> No target/serial specified; generated local creds only");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("==> Done");
|
|
||||||
println!("Local copy saved at: {}", opts.creds_dir.display());
|
|
||||||
|
|
||||||
if opts.target.is_some() || opts.serial.is_some() {
|
|
||||||
println!("Remote app env should use:");
|
|
||||||
println!(" TOES_MATTER_CREDS_DIR={}", opts.remote_creds_dir);
|
|
||||||
println!(" TOES_MATTER_STATE_DIR={}", opts.remote_state_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Options {
|
|
||||||
target: Option<String>,
|
|
||||||
serial: Option<String>,
|
|
||||||
device_id: String,
|
|
||||||
creds_dir: PathBuf,
|
|
||||||
remote_creds_dir: String,
|
|
||||||
remote_state_dir: String,
|
|
||||||
service: Option<String>,
|
|
||||||
ssh_opts: Vec<String>,
|
|
||||||
baud: u32,
|
|
||||||
serial_delay: Duration,
|
|
||||||
serial_login: Option<String>,
|
|
||||||
serial_password: Option<String>,
|
|
||||||
serial_prompts: Vec<String>,
|
|
||||||
serial_login_timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Options {
|
|
||||||
fn parse() -> Result<Self> {
|
|
||||||
let mut args = std::env::args().skip(1);
|
|
||||||
|
|
||||||
let mut target = None;
|
|
||||||
let mut serial = std::env::var("SERIAL_PORT").ok();
|
|
||||||
let mut device_id = None;
|
|
||||||
let mut creds_dir = std::env::var_os("CREDS_DIR").map(PathBuf::from);
|
|
||||||
let mut remote_creds_dir =
|
|
||||||
std::env::var("REMOTE_CREDS_DIR").unwrap_or_else(|_| DEFAULT_REMOTE_CREDS_DIR.into());
|
|
||||||
let mut remote_state_dir =
|
|
||||||
std::env::var("REMOTE_STATE_DIR").unwrap_or_else(|_| DEFAULT_REMOTE_STATE_DIR.into());
|
|
||||||
let mut service = match std::env::var("SERVICE") {
|
|
||||||
Ok(value) if value.is_empty() => None,
|
|
||||||
Ok(value) => Some(value),
|
|
||||||
Err(_) => Some(DEFAULT_SERVICE.into()),
|
|
||||||
};
|
|
||||||
let mut ssh_opts = split_ssh_opts(std::env::var("SSH_OPTS").unwrap_or_default());
|
|
||||||
let mut baud = std::env::var("SERIAL_BAUD")
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(DEFAULT_BAUD);
|
|
||||||
let mut serial_delay_ms = std::env::var("SERIAL_DELAY_MS")
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(DEFAULT_SERIAL_DELAY_MS);
|
|
||||||
let mut serial_login = std::env::var("SERIAL_LOGIN").ok();
|
|
||||||
let mut serial_password = std::env::var("SERIAL_PASSWORD").ok();
|
|
||||||
let mut serial_prompts = std::env::var("SERIAL_PROMPT")
|
|
||||||
.ok()
|
|
||||||
.map(|prompt| vec![prompt])
|
|
||||||
.unwrap_or_else(|| vec!["# ".into(), "$ ".into()]);
|
|
||||||
let mut serial_login_timeout_secs = std::env::var("SERIAL_LOGIN_TIMEOUT_SECS")
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(DEFAULT_SERIAL_LOGIN_TIMEOUT_SECS);
|
|
||||||
|
|
||||||
while let Some(arg) = args.next() {
|
|
||||||
match arg.as_str() {
|
|
||||||
"-h" | "--help" => {
|
|
||||||
print_usage();
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
"--creds-dir" => {
|
|
||||||
creds_dir = Some(PathBuf::from(next_arg(&mut args, "--creds-dir")?))
|
|
||||||
}
|
|
||||||
"--remote-creds-dir" => {
|
|
||||||
remote_creds_dir = next_arg(&mut args, "--remote-creds-dir")?
|
|
||||||
}
|
|
||||||
"--remote-state-dir" => {
|
|
||||||
remote_state_dir = next_arg(&mut args, "--remote-state-dir")?
|
|
||||||
}
|
|
||||||
"--service" => service = Some(next_arg(&mut args, "--service")?),
|
|
||||||
"--no-restart" => service = None,
|
|
||||||
"--device-id" => device_id = Some(next_arg(&mut args, "--device-id")?),
|
|
||||||
"--ssh-opt" => ssh_opts.push(next_arg(&mut args, "--ssh-opt")?),
|
|
||||||
"--serial" => serial = Some(next_arg(&mut args, "--serial")?),
|
|
||||||
"--baud" => baud = next_arg(&mut args, "--baud")?.parse()?,
|
|
||||||
"--serial-delay-ms" => {
|
|
||||||
serial_delay_ms = next_arg(&mut args, "--serial-delay-ms")?.parse()?
|
|
||||||
}
|
|
||||||
"--login" => serial_login = Some(next_arg(&mut args, "--login")?),
|
|
||||||
"--password" => serial_password = Some(next_arg(&mut args, "--password")?),
|
|
||||||
"--prompt" => serial_prompts.push(next_arg(&mut args, "--prompt")?),
|
|
||||||
"--login-timeout-secs" => {
|
|
||||||
serial_login_timeout_secs =
|
|
||||||
next_arg(&mut args, "--login-timeout-secs")?.parse()?
|
|
||||||
}
|
|
||||||
_ if arg.starts_with('-') => return Err(format!("unknown option: {arg}").into()),
|
|
||||||
_ => {
|
|
||||||
if target.is_none() {
|
|
||||||
target = Some(arg);
|
|
||||||
} else if device_id.is_none() {
|
|
||||||
device_id = Some(arg);
|
|
||||||
} else {
|
|
||||||
return Err("too many positional arguments".into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if serial.is_some() && target.is_some() && device_id.is_none() {
|
|
||||||
device_id = target.take();
|
|
||||||
}
|
|
||||||
|
|
||||||
if serial.is_some() && target.is_some() {
|
|
||||||
return Err("use either SSH target or --serial, not both".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let device_id = device_id.unwrap_or_else(default_device_id);
|
|
||||||
let creds_dir = creds_dir.unwrap_or_else(|| {
|
|
||||||
PathBuf::from("manufacturing")
|
|
||||||
.join(&device_id)
|
|
||||||
.join("creds")
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
target,
|
|
||||||
serial,
|
|
||||||
device_id,
|
|
||||||
creds_dir,
|
|
||||||
remote_creds_dir,
|
|
||||||
remote_state_dir,
|
|
||||||
service,
|
|
||||||
ssh_opts,
|
|
||||||
baud,
|
|
||||||
serial_delay: Duration::from_millis(serial_delay_ms),
|
|
||||||
serial_login,
|
|
||||||
serial_password,
|
|
||||||
serial_prompts,
|
|
||||||
serial_login_timeout: Duration::from_secs(serial_login_timeout_secs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_usage() {
|
|
||||||
eprintln!(
|
|
||||||
"Usage: toes-matter-provision [OPTIONS] [user@device-host] [device-id]\n\n\
|
|
||||||
Generates Matter setup data, prints the QR/manual code, and optionally copies it to a Linux device over SSH or serial console.\n\n\
|
|
||||||
Options:\n\
|
|
||||||
--creds-dir DIR Local output dir [env: CREDS_DIR]\n\
|
|
||||||
--remote-creds-dir DIR Remote creds dir [default: {DEFAULT_REMOTE_CREDS_DIR}]\n\
|
|
||||||
--remote-state-dir DIR Remote state dir [default: {DEFAULT_REMOTE_STATE_DIR}]\n\
|
|
||||||
--service NAME Service to restart [default: {DEFAULT_SERVICE}]\n\
|
|
||||||
--no-restart Do not restart a remote service\n\
|
|
||||||
--device-id ID Device id for local-only generation\n\
|
|
||||||
--ssh-opt ARG Extra ssh/scp option; may be repeated [env: SSH_OPTS]\n\
|
|
||||||
--serial PORT Write install script to a serial shell [env: SERIAL_PORT]\n\
|
|
||||||
--baud BAUD Configure serial baud [default: 115200, env: SERIAL_BAUD]\n\
|
|
||||||
--serial-delay-ms MS Delay between serial lines [default: 15, env: SERIAL_DELAY_MS]\n\
|
|
||||||
--login USER Log in on serial before provisioning [env: SERIAL_LOGIN]\n\
|
|
||||||
--password PASS Serial login password [env: SERIAL_PASSWORD]\n\
|
|
||||||
--prompt PROMPT Shell prompt marker; may repeat [env: SERIAL_PROMPT]\n\
|
|
||||||
--login-timeout-secs N Serial login timeout [default: 30, env: SERIAL_LOGIN_TIMEOUT_SECS]\n\
|
|
||||||
-h, --help Show this help"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_arg(args: &mut impl Iterator<Item = String>, option: &str) -> Result<String> {
|
|
||||||
args.next()
|
|
||||||
.ok_or_else(|| format!("{option} requires an argument").into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_device_id() -> String {
|
|
||||||
let secs = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|duration| duration.as_secs())
|
|
||||||
.unwrap_or(0);
|
|
||||||
format!("device-{secs}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_ssh_opts(value: String) -> Vec<String> {
|
|
||||||
value
|
|
||||||
.split_whitespace()
|
|
||||||
.filter(|part| !part.is_empty())
|
|
||||||
.map(str::to_owned)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GeneratedCredentials {
|
|
||||||
manual_code: String,
|
|
||||||
qr_code: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_credentials(path: &Path) -> Result<GeneratedCredentials> {
|
|
||||||
std::fs::create_dir_all(path)?;
|
|
||||||
|
|
||||||
std::fs::write(path.join("dac.der"), TEST_DEV_ATT.dac())?;
|
|
||||||
std::fs::write(path.join("pai.der"), TEST_DEV_ATT.pai())?;
|
|
||||||
std::fs::write(
|
|
||||||
path.join("certification-declaration.der"),
|
|
||||||
TEST_DEV_ATT.cert_declaration(),
|
|
||||||
)?;
|
|
||||||
std::fs::write(
|
|
||||||
path.join("dac-private-key.raw"),
|
|
||||||
TEST_DEV_ATT.dac_priv_key().access(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let comm = read_comm_data(path)?.unwrap_or_else(random_comm_data);
|
|
||||||
let payload = QrPayload::new_from_basic_info(
|
|
||||||
DiscoveryCapabilities::BLE,
|
|
||||||
CommFlowType::Standard,
|
|
||||||
comm.clone(),
|
|
||||||
&TEST_DEV_DET,
|
|
||||||
no_optional_data,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut qr_buf = [0_u8; 512];
|
futures_lite::future::block_on(async {
|
||||||
let (qr_text, _) = payload.as_str(&mut qr_buf)?;
|
toes_matter::provision().await?;
|
||||||
|
toes_matter::listen().await
|
||||||
let manual_code = comm.compute_pretty_pairing_code().to_string();
|
|
||||||
let qr_code = qr_text.to_owned();
|
|
||||||
|
|
||||||
let setup = format!(
|
|
||||||
"setup_passcode={}\n\
|
|
||||||
discriminator={}\n\
|
|
||||||
manual_code={}\n\
|
|
||||||
qr_code={}\n",
|
|
||||||
comm_passcode(&comm),
|
|
||||||
comm.discriminator,
|
|
||||||
manual_code,
|
|
||||||
qr_code,
|
|
||||||
);
|
|
||||||
|
|
||||||
std::fs::write(path.join("setup.txt"), setup)?;
|
|
||||||
|
|
||||||
Ok(GeneratedCredentials {
|
|
||||||
manual_code,
|
|
||||||
qr_code,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_comm_data(path: &Path) -> Result<Option<BasicCommData>> {
|
|
||||||
let setup_path = path.join("setup.txt");
|
|
||||||
let setup = match std::fs::read_to_string(setup_path) {
|
|
||||||
Ok(setup) => setup,
|
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let passcode = parse_setup_u32(&setup, "setup_passcode")?;
|
|
||||||
let discriminator = parse_setup_u32(&setup, "discriminator")?;
|
|
||||||
|
|
||||||
if discriminator > 0x0fff || !valid_setup_passcode(passcode) {
|
|
||||||
return Err("invalid setup.txt passcode/discriminator".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(make_comm_data(passcode, discriminator as u16)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_setup_u32(setup: &str, key: &str) -> Result<u32> {
|
|
||||||
let value = setup
|
|
||||||
.lines()
|
|
||||||
.find_map(|line| line.strip_prefix(key)?.strip_prefix('='))
|
|
||||||
.ok_or_else(|| format!("setup.txt missing {key}"))?;
|
|
||||||
|
|
||||||
value
|
|
||||||
.parse()
|
|
||||||
.map_err(|err| format!("invalid {key}: {err}").into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_comm_data() -> BasicCommData {
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
|
|
||||||
let passcode = loop {
|
|
||||||
let passcode = (rng.next_u32() % 99_999_998) + 1;
|
|
||||||
if valid_setup_passcode(passcode) {
|
|
||||||
break passcode;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let discriminator = (rng.next_u32() & 0x0fff) as u16;
|
|
||||||
|
|
||||||
make_comm_data(passcode, discriminator)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_comm_data(passcode: u32, discriminator: u16) -> BasicCommData {
|
|
||||||
BasicCommData {
|
|
||||||
password: Spake2pVerifierPassword::new_from_ref(Spake2pVerifierPasswordRef::new(
|
|
||||||
&passcode.to_le_bytes(),
|
|
||||||
)),
|
|
||||||
discriminator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn comm_passcode(comm: &BasicCommData) -> u32 {
|
|
||||||
u32::from_le_bytes(*comm.password.access())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valid_setup_passcode(passcode: u32) -> bool {
|
|
||||||
const INVALID: &[u32] = &[
|
|
||||||
0, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888,
|
|
||||||
99999999, 12345678, 87654321,
|
|
||||||
];
|
|
||||||
|
|
||||||
(1..=99_999_998).contains(&passcode) && !INVALID.contains(&passcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_qr(qr_code: &str) {
|
|
||||||
let status = Command::new("qrencode")
|
|
||||||
.arg("-t")
|
|
||||||
.arg("ANSIUTF8")
|
|
||||||
.arg(qr_code)
|
|
||||||
.status();
|
|
||||||
|
|
||||||
match status {
|
|
||||||
Ok(status) if status.success() => {}
|
|
||||||
Ok(_) | Err(_) => {
|
|
||||||
println!();
|
|
||||||
println!("Tip: install qrencode to render the QR in this terminal:");
|
|
||||||
println!(" macOS: brew install qrencode");
|
|
||||||
println!(" Debian/Ubuntu: sudo apt install qrencode");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_to_serial(port: &str, opts: &Options) -> Result<()> {
|
|
||||||
configure_serial(port, opts.baud)?;
|
|
||||||
|
|
||||||
let script = build_remote_install_script(opts)?;
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!(
|
|
||||||
"==> Writing install script to serial port {port} at {} baud",
|
|
||||||
opts.baud
|
|
||||||
);
|
|
||||||
if opts.serial_login.is_some() {
|
|
||||||
println!(" Will log in on the serial console first.");
|
|
||||||
} else {
|
|
||||||
println!(" This assumes the serial console is already at a shell prompt.");
|
|
||||||
}
|
|
||||||
if cfg!(target_os = "macos") && port.starts_with("/dev/tty.") {
|
|
||||||
println!(
|
|
||||||
" Tip: on macOS, /dev/cu.* is usually better than /dev/tty.* for outbound serial."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut serial = std::fs::OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.open(port)?;
|
|
||||||
serial.write_all(b"\n")?;
|
|
||||||
serial.flush()?;
|
|
||||||
|
|
||||||
if opts.serial_login.is_some() {
|
|
||||||
login_to_serial(&mut serial, opts)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for line in script.lines() {
|
|
||||||
serial.write_all(line.as_bytes())?;
|
|
||||||
serial.write_all(b"\n")?;
|
|
||||||
serial.flush()?;
|
|
||||||
std::thread::sleep(opts.serial_delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("==> Serial install script sent");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configure_serial(port: &str, baud: u32) -> Result<()> {
|
|
||||||
let port_flag = if cfg!(any(
|
|
||||||
target_os = "macos",
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "openbsd",
|
|
||||||
target_os = "netbsd"
|
|
||||||
)) {
|
|
||||||
"-f"
|
|
||||||
} else {
|
|
||||||
"-F"
|
|
||||||
};
|
|
||||||
|
|
||||||
let status = Command::new("stty")
|
|
||||||
.arg(port_flag)
|
|
||||||
.arg(port)
|
|
||||||
.arg(baud.to_string())
|
|
||||||
.arg("raw")
|
|
||||||
.arg("-echo")
|
|
||||||
.arg("-ixon")
|
|
||||||
.arg("-ixoff")
|
|
||||||
.arg("min")
|
|
||||||
.arg("0")
|
|
||||||
.arg("time")
|
|
||||||
.arg("5")
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
if status.success() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(format!("stty failed for {port} with status {status}").into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login_to_serial(serial: &mut std::fs::File, opts: &Options) -> Result<()> {
|
|
||||||
let login = opts
|
|
||||||
.serial_login
|
|
||||||
.as_deref()
|
|
||||||
.ok_or("serial login requested without a username")?;
|
|
||||||
|
|
||||||
println!("==> Waiting for serial login prompt or shell prompt...");
|
|
||||||
serial.write_all(b"\n")?;
|
|
||||||
serial.flush()?;
|
|
||||||
|
|
||||||
let mut initial_needles = vec![b"login:".to_vec(), b"Login:".to_vec()];
|
|
||||||
initial_needles.extend(
|
|
||||||
opts.serial_prompts
|
|
||||||
.iter()
|
|
||||||
.map(|prompt| prompt.as_bytes().to_vec()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let initial = wait_for_any(serial, &initial_needles, opts.serial_login_timeout)?;
|
|
||||||
|
|
||||||
if initial >= 2 {
|
|
||||||
println!("==> Serial console already appears to be at a shell prompt");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("==> Sending serial login username: {login}");
|
|
||||||
serial.write_all(login.as_bytes())?;
|
|
||||||
serial.write_all(b"\n")?;
|
|
||||||
serial.flush()?;
|
|
||||||
|
|
||||||
let mut after_user_needles = vec![b"Password:".to_vec(), b"password:".to_vec()];
|
|
||||||
after_user_needles.extend(
|
|
||||||
opts.serial_prompts
|
|
||||||
.iter()
|
|
||||||
.map(|prompt| prompt.as_bytes().to_vec()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let after_user = wait_for_any(serial, &after_user_needles, opts.serial_login_timeout)?;
|
|
||||||
|
|
||||||
if after_user < 2 {
|
|
||||||
let password = opts
|
|
||||||
.serial_password
|
|
||||||
.as_deref()
|
|
||||||
.ok_or("serial password prompt detected; pass --password or set SERIAL_PASSWORD")?;
|
|
||||||
|
|
||||||
println!("==> Sending serial login password");
|
|
||||||
serial.write_all(password.as_bytes())?;
|
|
||||||
serial.write_all(b"\n")?;
|
|
||||||
serial.flush()?;
|
|
||||||
|
|
||||||
let prompt_needles = opts
|
|
||||||
.serial_prompts
|
|
||||||
.iter()
|
|
||||||
.map(|prompt| prompt.as_bytes().to_vec())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
wait_for_any(serial, &prompt_needles, opts.serial_login_timeout)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("==> Serial login complete");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_for_any(
|
|
||||||
serial: &mut std::fs::File,
|
|
||||||
needles: &[Vec<u8>],
|
|
||||||
timeout: Duration,
|
|
||||||
) -> Result<usize> {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let mut seen = Vec::<u8>::new();
|
|
||||||
let mut buf = [0_u8; 256];
|
|
||||||
|
|
||||||
while start.elapsed() < timeout {
|
|
||||||
match serial.read(&mut buf) {
|
|
||||||
Ok(0) => {
|
|
||||||
std::thread::sleep(Duration::from_millis(25));
|
|
||||||
}
|
|
||||||
Ok(n) => {
|
|
||||||
std::io::stdout().write_all(&buf[..n])?;
|
|
||||||
std::io::stdout().flush()?;
|
|
||||||
|
|
||||||
seen.extend_from_slice(&buf[..n]);
|
|
||||||
if seen.len() > 8192 {
|
|
||||||
let keep_from = seen.len() - 4096;
|
|
||||||
seen.drain(..keep_from);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(index) = find_needle(&seen, needles) {
|
|
||||||
return Ok(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tail = String::from_utf8_lossy(&seen);
|
|
||||||
Err(format!("timed out waiting for serial prompt; recent output: {tail:?}").into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_needle(haystack: &[u8], needles: &[Vec<u8>]) -> Option<usize> {
|
|
||||||
needles.iter().position(|needle| {
|
|
||||||
!needle.is_empty()
|
|
||||||
&& haystack
|
|
||||||
.windows(needle.len())
|
|
||||||
.any(|window| window == needle.as_slice())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_remote_install_script(opts: &Options) -> Result<String> {
|
|
||||||
let mut script = String::new();
|
|
||||||
|
|
||||||
script.push_str("set -e\n");
|
|
||||||
script.push_str(&format!(
|
|
||||||
"REMOTE_CREDS_DIR={}\nREMOTE_STATE_DIR={}\n",
|
|
||||||
shell_quote(&opts.remote_creds_dir),
|
|
||||||
shell_quote(&opts.remote_state_dir),
|
|
||||||
));
|
|
||||||
script.push_str("sudo rm -rf \"$REMOTE_CREDS_DIR\"\n");
|
|
||||||
script.push_str("sudo mkdir -p \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"\n");
|
|
||||||
|
|
||||||
for file in [
|
|
||||||
"dac.der",
|
|
||||||
"pai.der",
|
|
||||||
"certification-declaration.der",
|
|
||||||
"dac-private-key.raw",
|
|
||||||
"setup.txt",
|
|
||||||
] {
|
|
||||||
let path = opts.creds_dir.join(file);
|
|
||||||
let data = std::fs::read(&path)?;
|
|
||||||
let marker = format!(
|
|
||||||
"TOES_MATTER_{}",
|
|
||||||
file.replace(['.', '-'], "_").to_uppercase()
|
|
||||||
);
|
|
||||||
|
|
||||||
script.push_str(&format!(
|
|
||||||
"base64 -d <<'{marker}' | sudo tee \"$REMOTE_CREDS_DIR/{file}\" >/dev/null\n"
|
|
||||||
));
|
|
||||||
script.push_str(&base64_wrapped(&data));
|
|
||||||
script.push_str(&format!("\n{marker}\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
script.push_str("sudo chmod -R go-rwx \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"\n");
|
|
||||||
|
|
||||||
if let Some(service) = opts.service.as_deref() {
|
|
||||||
script.push_str(&format!(
|
|
||||||
"if command -v systemctl >/dev/null 2>&1 && \\\n (systemctl list-unit-files {service} >/dev/null 2>&1 || systemctl status {service} >/dev/null 2>&1); then\n\
|
|
||||||
sudo systemctl restart {service}\n\
|
|
||||||
else\n\
|
|
||||||
echo 'Service {service_display} not found; skipping restart'\n\
|
|
||||||
fi\n",
|
|
||||||
service = shell_quote(service),
|
|
||||||
service_display = service,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
script.push_str("echo 'toes-matter serial provisioning complete'\n");
|
|
||||||
|
|
||||||
Ok(script)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn base64_wrapped(data: &[u8]) -> String {
|
|
||||||
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
||||||
|
|
||||||
let mut out = String::new();
|
|
||||||
let mut line_len = 0_usize;
|
|
||||||
|
|
||||||
for chunk in data.chunks(3) {
|
|
||||||
let b0 = chunk[0];
|
|
||||||
let b1 = *chunk.get(1).unwrap_or(&0);
|
|
||||||
let b2 = *chunk.get(2).unwrap_or(&0);
|
|
||||||
|
|
||||||
let encoded = [
|
|
||||||
TABLE[(b0 >> 2) as usize] as char,
|
|
||||||
TABLE[(((b0 & 0b0000_0011) << 4) | (b1 >> 4)) as usize] as char,
|
|
||||||
if chunk.len() > 1 {
|
|
||||||
TABLE[(((b1 & 0b0000_1111) << 2) | (b2 >> 6)) as usize] as char
|
|
||||||
} else {
|
|
||||||
'='
|
|
||||||
},
|
|
||||||
if chunk.len() > 2 {
|
|
||||||
TABLE[(b2 & 0b0011_1111) as usize] as char
|
|
||||||
} else {
|
|
||||||
'='
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for ch in encoded {
|
|
||||||
if line_len == 76 {
|
|
||||||
out.push('\n');
|
|
||||||
line_len = 0;
|
|
||||||
}
|
|
||||||
out.push(ch);
|
|
||||||
line_len += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn copy_to_device(target: &str, opts: &Options) -> Result<()> {
|
|
||||||
let remote_tmp = format!(
|
|
||||||
"/tmp/toes-matter-creds-{}-{}",
|
|
||||||
sanitize_path_component(&opts.device_id),
|
|
||||||
std::process::id(),
|
|
||||||
);
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("==> Copying creds to {target}:{}", opts.remote_creds_dir);
|
|
||||||
|
|
||||||
run_status(
|
|
||||||
"ssh",
|
|
||||||
command_with_args(
|
|
||||||
"ssh",
|
|
||||||
opts.ssh_opts.iter().map(OsString::from).chain([
|
|
||||||
OsString::from(target),
|
|
||||||
OsString::from(format!(
|
|
||||||
"rm -rf {} && mkdir -p {}",
|
|
||||||
shell_quote(&remote_tmp),
|
|
||||||
shell_quote(&remote_tmp),
|
|
||||||
)),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut scp_args = opts.ssh_opts.iter().map(OsString::from).collect::<Vec<_>>();
|
|
||||||
scp_args.push(OsString::from("-r"));
|
|
||||||
scp_args.push(opts.creds_dir.join(".").into_os_string());
|
|
||||||
scp_args.push(OsString::from(format!("{target}:{remote_tmp}/")));
|
|
||||||
|
|
||||||
run_status("scp", command_with_args("scp", scp_args))?;
|
|
||||||
|
|
||||||
let parent = remote_parent(&opts.remote_creds_dir);
|
|
||||||
let mut remote_script = format!(
|
|
||||||
"set -e\n\
|
|
||||||
sudo rm -rf {remote_creds}\n\
|
|
||||||
sudo mkdir -p {parent} {remote_state}\n\
|
|
||||||
sudo mv {remote_tmp} {remote_creds}\n\
|
|
||||||
sudo chmod -R go-rwx {remote_creds} {remote_state}\n",
|
|
||||||
remote_creds = shell_quote(&opts.remote_creds_dir),
|
|
||||||
parent = shell_quote(&parent),
|
|
||||||
remote_state = shell_quote(&opts.remote_state_dir),
|
|
||||||
remote_tmp = shell_quote(&remote_tmp),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(service) = opts.service.as_deref() {
|
|
||||||
remote_script.push_str(&format!(
|
|
||||||
"if systemctl list-unit-files {service} >/dev/null 2>&1 || systemctl status {service} >/dev/null 2>&1; then\n\
|
|
||||||
sudo systemctl restart {service}\n\
|
|
||||||
else\n\
|
|
||||||
echo 'Service {service_display} not found; skipping restart'\n\
|
|
||||||
fi\n",
|
|
||||||
service = shell_quote(service),
|
|
||||||
service_display = service,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
run_status(
|
|
||||||
"ssh",
|
|
||||||
command_with_args(
|
|
||||||
"ssh",
|
|
||||||
opts.ssh_opts
|
|
||||||
.iter()
|
|
||||||
.map(OsString::from)
|
|
||||||
.chain([OsString::from(target), OsString::from(remote_script)]),
|
|
||||||
),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_with_args(program: &str, args: impl IntoIterator<Item = OsString>) -> Command {
|
|
||||||
let mut command = Command::new(program);
|
|
||||||
command.args(args);
|
|
||||||
command
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_status(label: &str, mut command: Command) -> Result<()> {
|
|
||||||
let status = command.status()?;
|
|
||||||
if status.success() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(format!("{label} failed with status {status}").into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sanitize_path_component(value: &str) -> String {
|
|
||||||
value
|
|
||||||
.chars()
|
|
||||||
.map(|ch| match ch {
|
|
||||||
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => ch,
|
|
||||||
_ => '_',
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remote_parent(path: &str) -> String {
|
|
||||||
path.rsplit_once('/')
|
|
||||||
.map(|(parent, _)| if parent.is_empty() { "/" } else { parent })
|
|
||||||
.unwrap_or(".")
|
|
||||||
.to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shell_quote(value: &str) -> String {
|
|
||||||
let mut quoted = String::with_capacity(value.len() + 2);
|
|
||||||
quoted.push('\'');
|
|
||||||
for ch in value.chars() {
|
|
||||||
if ch == '\'' {
|
|
||||||
quoted.push_str("'\\''");
|
|
||||||
} else {
|
|
||||||
quoted.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
quoted.push('\'');
|
|
||||||
quoted
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user