use std::fs::OpenOptions;
use std::io::Read;
use std::io::Seek;
use std::io::Write;
use std::{
fs, io::{self, stdout},
path::{Path, PathBuf},
};
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
use anyhow::{Context, Result};
use tempfile::NamedTempFile;
use sequoia_openpgp::{
self as openpgp,
armor,
serialize::stream::{Armorer, Message},
};
use openpgp::crypto::mem::Protected;
use crate::{
cli::types::FileOrStdout,
sq::Sq,
};
impl FileOrStdout {
pub fn is_stdout(&self) -> bool {
self.path().is_none()
}
pub fn create_safe(
&self,
sq: &Sq,
) -> Result<Box<dyn Write + Sync + Send>> {
self.create(sq)
}
pub fn create_unsafe(
&self,
sq: &Sq,
) -> Result<Box<dyn Write + Sync + Send>> {
self.create(sq)
}
pub fn create_pgp_safe<'a>(
&self,
sq: &Sq,
binary: bool,
kind: armor::Kind,
) -> Result<Message<'a>> {
let mut o = self.clone();
if kind == armor::Kind::SecretKey {
o = o.for_secrets();
}
let sink = o.create_safe(sq)?;
let mut message = Message::new(sink);
if ! binary {
message = Armorer::new(message).kind(kind).build()?;
}
Ok(message)
}
fn create(&self, sq: &Sq) -> Result<Box<dyn Write + Sync + Send>> {
let sink = self._create_sink(sq)?;
if self.is_for_secrets() || ! cfg!(debug_assertions) {
Ok(sink)
} else {
Ok(Box::new(SecretLeakDetector::new(sink)))
}
}
fn _create_sink(&self, sq: &Sq) -> Result<Box<dyn Write + Sync + Send>>
{
if let Some(path) = self.path() {
if !path.exists() || sq.overwrite {
Ok(Box::new(
PartFileWriter::create_with_restricted_permissions(
path, self.is_for_secrets())
.context("Failed to create output file")?,
))
} else {
#[cfg(unix)]
if let Ok(p) = fs::metadata(path) {
if p.file_type().is_char_device()
|| p.file_type().is_block_device()
|| p.file_type().is_fifo()
|| p.file_type().is_socket()
{
return Ok(Box::new(
PartFileWriter::create_with_restricted_permissions(
path, self.is_for_secrets())
.with_context(|| {
format!("Failed to open special file {}",
path.display())
})?,
))
}
}
Err(anyhow::anyhow!(
"File {} exists, use \"sq --overwrite ...\" to overwrite",
path.display(),
))
}
} else {
Ok(Box::new(stdout()))
}
}
}
pub struct PartFileWriter {
path: PathBuf,
sink: Option<NamedTempFile>,
copy: bool,
restricted_permissions: bool,
}
impl io::Write for PartFileWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.sink()?.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.sink()?.flush()
}
}
impl Drop for PartFileWriter {
fn drop(&mut self) {
if let Err(e) = self.persist() {
weprintln!(initial_indent = "Error: ", "{}", e);
std::process::exit(1);
}
}
}
impl PartFileWriter {
pub fn create<P: AsRef<Path>>(path: P) -> Result<PartFileWriter> {
Self::create_with_restricted_permissions(path, false)
}
pub fn create_with_restricted_permissions<P: AsRef<Path>>(
path: P, restricted: bool)
-> Result<PartFileWriter>
{
let path = path.as_ref().to_path_buf();
let parent = path.parent()
.ok_or(anyhow::anyhow!("cannot write to the root"))?;
let file_name = path.file_name()
.ok_or(anyhow::anyhow!("cannot write to .."))?;
let mut sink = tempfile::Builder::new();
if ! restricted {
platform! {
unix => {
use std::os::unix::fs::PermissionsExt;
sink.permissions(std::fs::Permissions::from_mode(0o666));
},
windows => {
},
}
}
let mut copy = false;
let sink = match sink
.prefix(file_name)
.suffix(".part")
.tempfile_in(parent) {
Ok(s) => s,
Err(_) => {
copy = true;
sink
.prefix(file_name)
.suffix(".part")
.tempfile()?
},
};
Ok(PartFileWriter {
path,
sink: Some(sink),
copy,
restricted_permissions: restricted,
})
}
fn sink(&mut self) -> io::Result<&mut NamedTempFile> {
self.sink.as_mut().ok_or(io::Error::new(
io::ErrorKind::Other,
anyhow::anyhow!("file already persisted")))
}
const DEFAULT_BUF_SIZE: usize = 64 * 1024;
pub fn persist(&mut self) -> io::Result<()> {
if let Some(mut file) = self.sink.take() {
if self.copy {
file.rewind()?;
let mut open_options = OpenOptions::new();
open_options.create(true)
.write(true)
.truncate(true);
platform! {
unix => {
use std::os::unix::fs::FileTypeExt;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::fs::PermissionsExt;
let mut modify_permissions = true;
let meta_result = std::fs::metadata(&self.path);
if let Ok(meta) = &meta_result {
let ft = meta.file_type();
if ft.is_char_device()
|| ft.is_block_device()
|| ft.is_fifo()
|| ft.is_socket()
{
modify_permissions = false;
}
}
if modify_permissions && self.restricted_permissions {
if let Ok(meta) = &meta_result {
if meta.file_type().is_file() {
std::fs::set_permissions(
&self.path,
PermissionsExt::from_mode(0o600))?;
}
}
open_options.mode(0o600);
}
open_options.custom_flags(libc::O_NOFOLLOW);
},
windows => {
use std::fs::OpenOptions;
use std::os::windows::prelude::*;
open_options.share_mode(0);
},
}
let mut target = open_options.open(&self.path)?;
let mut buffer: Protected = [0; Self::DEFAULT_BUF_SIZE].into();
while let Ok(len) = file.read(buffer.as_mut()) {
if len == 0 {
break;
}
target.write_all(&buffer[0..len])?;
}
target.flush()?;
} else {
file.persist(&self.path)?;
}
}
Ok(())
}
}
struct SecretLeakDetector<W: io::Write + Send + Sync> {
sink: W,
data: Vec<u8>,
}
impl<W: io::Write + Send + Sync> io::Write for SecretLeakDetector<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.sink.write(buf)?;
self.data.extend_from_slice(&buf[..n]);
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.sink.flush()
}
}
impl<W: io::Write + Send + Sync> Drop for SecretLeakDetector<W> {
fn drop(&mut self) {
let _ = self.detect_leaks();
}
}
impl<W: io::Write + Send + Sync> SecretLeakDetector<W> {
fn new(sink: W) -> Self {
SecretLeakDetector {
sink,
data: Vec::with_capacity(4096),
}
}
fn detect_leaks(&self) -> Result<()> {
use openpgp::Packet;
use openpgp::parse::{Parse, PacketParserResult, PacketParser};
let mut ppr = PacketParser::from_bytes(&self.data)?;
while let PacketParserResult::Some(pp) = ppr {
match &pp.packet {
Packet::SecretKey(_) | Packet::SecretSubkey(_) =>
panic!("Leaked secret key: {:?}", pp.packet),
_ => (),
}
let (_, next_ppr) = pp.recurse()?;
ppr = next_ppr;
}
Ok(())
}
}