use std::{
ffi::OsStr,
fs::File,
io::Write,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
process::{Command, Stdio},
thread::spawn,
};
use log::{debug, info};
use tempfile::tempdir;
pub struct Sop {
kind: SopKind,
}
impl Sop {
pub fn hardware_key(sop: &Path, sop_decrypt: &Path, cert: &Path) -> Self {
Self {
kind: SopKind::HardwareKey(HardwareKeySop::new(sop, sop_decrypt, cert)),
}
}
pub fn software_key(sop: &Path, key: &Path) -> Self {
Self {
kind: SopKind::SoftwareKey(SoftwareKeySop::new(sop, key)),
}
}
pub fn extract_cert(&self) -> Result<Certificate, SopError> {
match &self.kind {
SopKind::HardwareKey(sop) => sop.extract_cert(),
SopKind::SoftwareKey(sop) => sop.extract_cert(),
}
}
pub fn encrypt(
&self,
data: Vec<u8>,
certs: &[Certificate],
output: &Path,
) -> Result<Vec<u8>, SopError> {
match &self.kind {
SopKind::HardwareKey(sop) => sop.encrypt(data, certs, output),
SopKind::SoftwareKey(sop) => sop.encrypt(data, certs, output),
}
}
pub fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, SopError> {
match &self.kind {
SopKind::HardwareKey(sop) => sop.decrypt(data),
SopKind::SoftwareKey(sop) => sop.decrypt(data),
}
}
}
enum SopKind {
HardwareKey(HardwareKeySop),
SoftwareKey(SoftwareKeySop),
}
struct SoftwareKeySop {
sop: PathBuf,
key: PathBuf,
}
impl SoftwareKeySop {
pub fn new(sop: &Path, key: &Path) -> Self {
Self {
sop: sop.into(),
key: key.into(),
}
}
pub fn extract_cert(&self) -> Result<Certificate, SopError> {
let output = run_sop(
&self.sop,
&args(&["extract-cert"]),
self.key()?.as_bytes().to_vec(),
None,
)?;
Ok(Certificate::new(output))
}
pub fn encrypt(
&self,
data: Vec<u8>,
certs: &[Certificate],
output_file: &Path,
) -> Result<Vec<u8>, SopError> {
info!("encrypt data to {}", output_file.display());
let tmp = tempdir().map_err(SopError::TempDir)?;
let mut filenames = vec![];
for (i, cert) in certs.iter().enumerate() {
let filename = format!("cert-{i}");
let filename = tmp.path().join(&filename);
std::fs::write(&filename, cert.as_bytes()).map_err(SopError::TempWrite)?;
filenames.push(PathBuf::from(&filename));
}
let mut args = args(&["encrypt"]);
for filename in filenames.iter() {
args.push(filename.as_os_str());
}
let stdout = File::create(output_file)
.map_err(|err| SopError::CreateFile(output_file.into(), err))?;
run_sop(&self.sop, &args, data, Some(stdout))
}
pub fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, SopError> {
let mut args = args(&["decrypt"]);
args.push(self.key.as_os_str());
run_sop(&self.sop, &args, data, None)
}
fn key(&self) -> Result<Key, SopError> {
let data =
std::fs::read(&self.key).map_err(|err| SopError::ReadKey(self.key.clone(), err))?;
Ok(Key::new(data))
}
}
#[allow(dead_code)]
struct HardwareKeySop {
sop: PathBuf,
sop_decrypt: PathBuf,
cert: PathBuf,
}
#[allow(unused_variables)]
impl HardwareKeySop {
pub fn new(sop: &Path, sop_decrypt: &Path, cert: &Path) -> Self {
Self {
sop: sop.into(),
sop_decrypt: sop_decrypt.into(),
cert: cert.into(),
}
}
pub fn extract_cert(&self) -> Result<Certificate, SopError> {
info!("read certificate from {}", self.cert.display());
let cert =
std::fs::read(&self.cert).map_err(|err| SopError::ReadCert(self.cert.clone(), err))?;
Ok(Certificate::new(cert))
}
pub fn encrypt(
&self,
data: Vec<u8>,
certs: &[Certificate],
output_file: &Path,
) -> Result<Vec<u8>, SopError> {
info!("encrypt data to {}", output_file.display());
let tmp = tempdir().map_err(SopError::TempDir)?;
let mut filenames = vec![];
for (i, cert) in certs.iter().enumerate() {
let filename = format!("cert-{i}");
let filename = tmp.path().join(&filename);
std::fs::write(&filename, cert.as_bytes()).map_err(SopError::TempWrite)?;
filenames.push(PathBuf::from(&filename));
}
let mut args = args(&["encrypt"]);
for filename in filenames.iter() {
args.push(filename.as_os_str());
}
let stdout = File::create(output_file)
.map_err(|err| SopError::CreateFile(output_file.into(), err))?;
run_sop(&self.sop, &args, data, Some(stdout))
}
pub fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, SopError> {
let mut args = args(&["decrypt"]);
args.push(self.cert.as_os_str());
run_sop(&self.sop_decrypt, &args, data, None)
}
}
fn args<'a>(strs: &'a [&str]) -> Vec<&'a OsStr> {
strs.iter()
.map(|s| OsStr::from_bytes(s.as_bytes()))
.collect()
}
fn run_sop(
bin: &Path,
args: &[&OsStr],
feed_stdin: Vec<u8>,
stdout: Option<File>,
) -> Result<Vec<u8>, SopError> {
fn s(os: &OsStr) -> String {
os.to_str().unwrap().to_string()
}
let mut cmd = Command::new(bin);
cmd.args(args)
.stdin(Stdio::piped())
.stdout(stdout.map(Stdio::from).unwrap_or(Stdio::piped()))
.stderr(Stdio::piped());
let mut argv = vec![s(cmd.get_program())];
argv.append(&mut cmd.get_args().map(s).collect());
debug!("run SOP: {:?}", argv);
let mut child = cmd
.spawn()
.map_err(|err| SopError::Invoke(bin.to_path_buf(), err))?;
let mut stdin = child.stdin.take().ok_or(SopError::TakeStdin)?;
let writer = spawn(move || stdin.write_all(&feed_stdin));
writer
.join()
.map_err(|_| SopError::JoinThread)?
.map_err(SopError::WriteStdin)?;
let output = child.wait_with_output().map_err(SopError::WaitChild)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(SopError::Failed(
bin.to_path_buf(),
output.status.code().unwrap_or(999),
stderr,
));
}
Ok(output.stdout)
}
pub struct Key {
data: Vec<u8>,
}
impl Key {
pub fn new(data: Vec<u8>) -> Self {
Self { data }
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
}
#[derive(Debug)]
pub struct Certificate {
data: Vec<u8>,
}
impl Certificate {
pub fn new(data: Vec<u8>) -> Self {
Self { data }
}
pub fn load(filename: &Path) -> Result<Self, SopError> {
let bytes =
std::fs::read(filename).map_err(|err| SopError::ReadCert(filename.into(), err))?;
Ok(Self::new(bytes))
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
}
#[derive(Debug, thiserror::Error)]
pub enum SopError {
#[error("failed to run {0}")]
Invoke(PathBuf, #[source] std::io::Error),
#[error("{0} failed with exit code {1}, stderr:\n{2}")]
Failed(PathBuf, i32, String),
#[error("failed to get stdin from child process handle")]
TakeStdin,
#[error("failed to read key from {0}")]
ReadKey(PathBuf, #[source] std::io::Error),
#[error("failed to join thread that writes to child stdin")]
JoinThread,
#[error("failed to write key to child stdin")]
WriteStdin(#[source] std::io::Error),
#[error("failed when waiting for child process to end")]
WaitChild(#[source] std::io::Error),
#[error("failed to create file {0}")]
CreateFile(PathBuf, #[source] std::io::Error),
#[error("failed to create a temporary directory")]
TempDir(#[source] std::io::Error),
#[error("failed to write to temporary file")]
TempWrite(#[source] std::io::Error),
#[error("failed to read certificate from file {0}")]
ReadCert(PathBuf, #[source] std::io::Error),
}