···2626- Fixed garnix docs links in documentation.
2727- Forces `bash` instead of remote user's potentially unsupported shell. This bug
2828 was causing strange and hard to diagnose issues.
2929+- Fixed a possible time-of-check to time-of-use bug while setting key permissions.
29303031## [v1.2.0] - 2026-03-18
3132
+1-1
Cargo.toml
···2727im = { version = "15.1.0", features = ["serde"] }
2828anyhow = "1.0.100"
2929prost = "0.14.1"
3030-nix = { version = "0.31.0", features = ["user", "poll", "term"] }
3030+nix = { version = "0.31.0", features = ["user", "poll", "term", "fs"] }
3131miette = { version = "7.6.0", features = ["fancy"] }
3232thiserror = "2.0.17"
3333sha2 = "0.11.0"
+52-18
crates/key_agent/src/main.rs
···22// Copyright 2024-2025 wire Contributors
3344#![deny(clippy::pedantic)]
55+use anyhow::Context;
56use base64::Engine;
67use base64::prelude::BASE64_STANDARD;
78use futures_util::stream::StreamExt;
99+use nix::sys::stat::fchmod;
1010+use nix::unistd::fchown;
811use nix::unistd::{Group, User};
912use prost::Message;
1013use prost::bytes::Bytes;
1114use sha2::{Digest, Sha256};
1212-use std::os::unix::fs::PermissionsExt;
1313-use std::os::unix::fs::chown;
1515+use std::os::fd::AsFd;
1416use std::path::{Path, PathBuf};
1515-use tokio::fs::File;
1717+use tokio::fs::OpenOptions;
1618use tokio::io::AsyncWriteExt;
1719use tokio_util::codec::{FramedRead, LengthDelimitedCodec};
1820use wire_key_agent::keys::KeySpec;
···6365 }
64666567 let path = PathBuf::from(&spec.destination);
6666- create_path(&path)?;
6868+ create_path(&path).context("creating directory for key")?;
67696868- let mut file = File::create(path).await?;
6969- let mut permissions = file.metadata().await?.permissions();
7070+ let mut file = OpenOptions::new()
7171+ .write(true)
7272+ .create(true)
7373+ .truncate(true)
7474+ // only applies if the file is created
7575+ .mode(spec.unix_mode)
7676+ .custom_flags(nix::libc::O_NOFOLLOW)
7777+ .open(&path)
7878+ .await
7979+ .context("opening file")?;
70807171- permissions.set_mode(spec.unix_mode);
7272- file.set_permissions(permissions).await?;
8181+ // enforce permission on existing files
8282+ let mode = nix::sys::stat::Mode::from_bits(spec.unix_mode)
8383+ .with_context(|| format!("failed to create unix mode: {:o}", spec.unix_mode))?;
73847474- let user = User::from_name(&spec.user)?;
7575- let group = Group::from_name(&spec.group)?;
8585+ fchmod(file.as_fd(), mode)
8686+ .with_context(|| format!("setting permissions of fd to {:o}", spec.unix_mode))?;
76877777- chown(
7878- spec.destination,
7979- // Default uid/gid to 0. This is then wrapped around an Option again for
8080- // the function.
8181- Some(user.map_or(0, |user| user.uid.into())),
8282- Some(group.map_or(0, |group| group.gid.into())),
8383- )?;
8888+ // Default uid/gid to 0. This is then wrapped around an Option again for
8989+ // the function.
9090+ let user = Some(
9191+ User::from_name(&spec.user)
9292+ .context("obtaining user")?
9393+ .map_or_else(
9494+ || {
9595+ println!("warning: defaulting uid to `0`");
9696+9797+ 0.into()
9898+ },
9999+ |user| user.uid,
100100+ ),
101101+ );
102102+ let group = Some(
103103+ Group::from_name(&spec.group)
104104+ .context("obtaining group")?
105105+ .map_or_else(
106106+ || {
107107+ println!("warning: defaulting gid to `0`");
108108+109109+ 0.into()
110110+ },
111111+ |group| group.gid,
112112+ ),
113113+ );
114114+115115+ // set permission on new files
116116+ fchown(&file, user, group)
117117+ .with_context(|| format!("setting ownership of fd to {user:?}, {group:?}"))?;
841188585- file.write_all(&key_bytes).await?;
119119+ file.write_all(&key_bytes).await.context("writing to fd")?;
8612087121 // last key, goobye
88122 if spec.last {