use anyhow::{bail, Context, Result};
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
pub struct TokenHandoff {
path: PathBuf,
}
impl TokenHandoff {
pub fn create(token: &str, locale: crate::locale::Locale) -> Result<Self> {
let dir = std::env::temp_dir();
for _ in 0..8 {
let name = format!("rdsh-token-{}.txt", crate::local_http::random_token()?);
let path = dir.join(name);
let mut file = match open_restricted(&path, locale) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(error) => {
return Err(error).with_context(|| {
format!(
"{} {}",
handoff_text(
locale,
"cannot create protected token handoff file",
"无法创建受保护的令牌交接文件",
"保護されたトークン引き渡しファイルを作成できません"
),
path.display()
)
});
}
};
if let Err(error) = write_handoff(&mut file, &path, token, locale) {
drop(file);
let _ = fs::remove_file(&path);
return Err(error);
}
return Ok(Self { path });
}
bail!(
"{} {}",
handoff_text(
locale,
"could not create a unique token handoff file in",
"无法在以下位置创建唯一的令牌交接文件:",
"一意なトークン引き渡しファイルを作成できません:"
),
dir.display()
)
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TokenHandoff {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
fn open_restricted(path: &Path, locale: crate::locale::Locale) -> std::io::Result<File> {
#[cfg(unix)]
let _ = locale;
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let file = options.open(path)?;
#[cfg(windows)]
if let Err(error) = restrict_windows_acl(path, locale) {
drop(file);
let _ = fs::remove_file(path);
return Err(std::io::Error::other(error.to_string()));
}
#[cfg(not(any(unix, windows)))]
{
drop(file);
let _ = fs::remove_file(path);
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
handoff_text(
locale,
"secure token handoff is not supported on this platform",
"此平台不支持安全令牌交接",
"このプラットフォームでは安全なトークン引き渡しをサポートしていません",
),
));
}
Ok(file)
}
fn write_handoff(
file: &mut File,
path: &Path,
token: &str,
locale: crate::locale::Locale,
) -> Result<()> {
file.write_all(token.as_bytes()).with_context(|| {
format!(
"{} {}",
handoff_text(
locale,
"cannot write token handoff file",
"无法写入令牌交接文件",
"トークン引き渡しファイルを書き込めません"
),
path.display()
)
})?;
file.sync_all().with_context(|| {
format!(
"{} {}",
handoff_text(
locale,
"cannot flush token handoff file",
"无法刷新令牌交接文件",
"トークン引き渡しファイルをフラッシュできません"
),
path.display()
)
})
}
#[cfg(windows)]
fn restrict_windows_acl(path: &Path, locale: crate::locale::Locale) -> Result<()> {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let system_root = std::env::var_os("SystemRoot").context(handoff_text(
locale,
"SystemRoot is unavailable; cannot protect the token handoff file",
"SystemRoot 不可用;无法保护令牌交接文件",
"SystemRoot を利用できないためトークン引き渡しファイルを保護できません",
))?;
let system32 = PathBuf::from(system_root).join("System32");
let whoami = std::process::Command::new(system32.join("whoami.exe"))
.args(["/user", "/fo", "csv", "/nh"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.context(handoff_text(
locale,
"cannot determine the current Windows account SID",
"无法确定当前 Windows 帐户 SID",
"現在の Windows アカウント SID を取得できません",
))?;
if !whoami.status.success() {
bail!(handoff_text(
locale,
"cannot determine the current Windows account SID",
"无法确定当前 Windows 帐户 SID",
"現在の Windows アカウント SID を取得できません"
));
}
let output = String::from_utf8_lossy(&whoami.stdout);
let sid = output
.lines()
.find_map(parse_whoami_sid)
.context(handoff_text(
locale,
"whoami did not return a valid current-account SID",
"whoami 未返回有效的当前帐户 SID",
"whoami が有効な現在のアカウント SID を返しませんでした",
))?;
let acl = std::process::Command::new(system32.join("icacls.exe"))
.arg(path)
.arg("/inheritance:r")
.arg("/grant:r")
.arg(format!("*{sid}:(F)"))
.arg("*S-1-5-18:(F)")
.creation_flags(CREATE_NO_WINDOW)
.output()
.with_context(|| {
format!(
"{} {}",
handoff_text(
locale,
"cannot set permissions on token handoff file",
"无法设置令牌交接文件的权限",
"トークン引き渡しファイルの権限を設定できません"
),
path.display()
)
})?;
if !acl.status.success() {
bail!(
"{} {}",
handoff_text(
locale,
"cannot set permissions on token handoff file",
"无法设置令牌交接文件的权限",
"トークン引き渡しファイルの権限を設定できません"
),
path.display()
);
}
Ok(())
}
fn handoff_text(
locale: crate::locale::Locale,
english: &'static str,
chinese: &'static str,
japanese: &'static str,
) -> &'static str {
match locale {
crate::locale::Locale::English => english,
crate::locale::Locale::Chinese => chinese,
crate::locale::Locale::Japanese => japanese,
}
}
#[cfg(windows)]
fn parse_whoami_sid(line: &str) -> Option<String> {
let (_, sid) = line.rsplit_once("\",\"")?;
let sid = sid.trim().trim_matches('"').trim_start_matches('\u{feff}');
let mut parts = sid.split('-');
if parts.next()? != "S" || parts.next()? != "1" {
return None;
}
let rest: Vec<_> = parts.collect();
if rest.len() < 2
|| rest
.iter()
.any(|part| part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()))
{
return None;
}
Some(sid.to_owned())
}
#[cfg(test)]
mod tests {
use super::TokenHandoff;
#[test]
fn handoff_contains_token_and_is_removed_on_drop() {
let token = "credential-that-must-not-be-in-the-path";
let handoff = TokenHandoff::create(token, crate::locale::Locale::English).unwrap();
assert_ne!(handoff.path().file_name().unwrap().to_string_lossy(), token);
assert_eq!(std::fs::read_to_string(handoff.path()).unwrap(), token);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
std::fs::metadata(handoff.path())
.unwrap()
.permissions()
.mode()
& 0o777,
0o600
);
}
let path = handoff.path().to_owned();
drop(handoff);
assert!(!path.exists());
}
#[cfg(windows)]
#[test]
fn parses_current_user_sid_from_whoami_csv() {
assert_eq!(
super::parse_whoami_sid("\"DOMAIN\\\\user\",\"S-1-5-21-1-2-3-1000\""),
Some("S-1-5-21-1-2-3-1000".to_owned())
);
assert_eq!(super::parse_whoami_sid("invalid"), None);
}
}