Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
370 changes: 47 additions & 323 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,19 @@ edition = "2024"
[dependencies]
anyhow = "1.0.100"
atomic_enum = "0.3.0"
cairo-rs = { version = "0.22.0", features = ["png"] }
cairo-rs = { version = "0.21.5", features = ["png"] }
chrono = "0.4.42"
clap = { version = "4.5.53", features = ["derive"] }
clap_complete = "4.5.64"
config = "0.15.18"
dirs = "6.0.0"
gdk-pixbuf = { version = "0.21.5", optional = true }
nix = { version = "0.31.3", features = ["event", "fs", "mman", "process", "time"] }
pam-rs = "0.9.5"
pango = { version = "0.22.0", optional = true }
pangocairo = { version = "0.22.0", optional = true }
nix = { version = "0.31.3", features = ["event", "fs", "mman", "process", "time", "user"] }
pango = { version = "0.21.5", optional = true }
pangocairo = { version = "0.21.5", optional = true }
serde = { version = "1.0.228", features = [ "derive" ] }
tracing = "0.1.41"
tracing-subscriber = "0.3.20"
uzers = "0.12.1"
wayland-client = "0.31.11"
wayland-protocols = { version = "0.32.9", features = ["client", "staging"] }
xkbcommon = "0.9.0"
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ compile with the latest stable Rust, I haven't tested older versions.
In addition, you'll need development libraries for the following, which can
probably be installed via your system package manager:

- clang
- glib
- gdk-pixbuf (optional, provides support for non-PNG image formats)
- pam
- pam (except openbsd)
- cairo
- pango (optional, provides support for system font loading)
- xkbcommon
Expand All @@ -37,6 +36,10 @@ gdk-pixbuf and pango are both optional, and are enabled with the features
`gdk-pixbuf` and `pango` respectively. These features are enabled by default,
you will need to disable them if you do not plan on using these libraries.

On OpenBSD targets, BSD Authentication is natively supported and will be
selected as the authentication backend automatically, PAM is not required
in this case.

With all of that, you should just be able to clone this repository, and run:

```sh
Expand Down
4 changes: 4 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@ fn main() {

println!("cargo:rerun-if-env-changed=NLOCK_VERSION");
println!("cargo:rerun-if-env-changed=NLOCK_COMMIT");

if cfg!(not(target_os = "openbsd")) {
println!("cargo:rustc-link-lib=pam");
}
}
3 changes: 1 addition & 2 deletions doc/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ The following correspond directly to configuration options. See
- `--fit-to-content <BOOL>`, resize the input box to fit password
- `--frame-radius <FLOAT>`, sets the border radius of the frame
- `--frame-border <FLOAT>`, sets the border width of the frame
- `--allow-empty-password <BOOL>`, validate empty passwords, this option is
only supported on Linux targets
- `--allow-empty-password <BOOL>`, validate empty passwords
- `--hide-cursor <BOOL>`, hide the mouse cursor
- `--bg-type <BACKGROUND TYPE>`, sets the background type
- `--image-path <PATH>`, path to a background image
Expand Down
1 change: 0 additions & 1 deletion examples/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
[general]
hideCursor = true # hide the mouse cursor
backgroundType = "color" # background type "color", or "image"
# this option is only supported on Linux targets
allowEmptyPassword = false # allow a blank password to be validated

# Colors section configures, well, colors.
Expand Down
2 changes: 0 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
rustPackages.clippy

cairo
clang
gdk-pixbuf
glib
libxkbcommon
Expand All @@ -71,7 +70,6 @@
];

RUST_SRC_PATH = rustPlatform.rustLibSrc;
LIBCLANG_PATH = "${clang.cc.lib}/lib";
};
});
};
Expand Down
3 changes: 0 additions & 3 deletions nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
rustPlatform,
installShellFiles,
cairo,
clang,
gdk-pixbuf,
glib,
libxkbcommon,
Expand Down Expand Up @@ -32,7 +31,6 @@ rustPlatform.buildRustPackage {

nativeBuildInputs = [
installShellFiles
clang
pkg-config
];

Expand All @@ -52,7 +50,6 @@ rustPlatform.buildRustPackage {
--fish <($out/bin/nlock completions fish)
'';

LIBCLANG_PATH = "${clang.cc.lib}/lib";
NLOCK_COMMIT = "${shortRev}"; # used to generate version string

meta = with lib; {
Expand Down
1 change: 0 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ pub struct NLockArgs {

/// Validate empty passwords
#[arg(long)]
#[cfg(target_os = "linux")]
pub pwd_allow_empty: Option<bool>,
/// Hide the mouse cursor
#[arg(long)]
Expand Down
106 changes: 57 additions & 49 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2026, Nathan Gill

use std::{os::fd::AsFd, sync::Arc};
use std::{os::fd::AsFd, sync::Arc, thread::JoinHandle};

use anyhow::{Result, anyhow};
use atomic_enum::atomic_enum;
use nix::{
errno::Errno,
poll::{PollFd, PollFlags, PollTimeout},
};
use pam_rs::{Client, PamFlag};
use tracing::{debug, warn};
use zeroize::Zeroizing;

use crate::{comm::PipeCommChannel, config::NLockConfig};
use crate::{auth_sys::AuthClient, comm::PipeCommChannel, config::NLockConfig};

pub struct AuthChannel {
pub request: PipeCommChannel<String>,
pub response: PipeCommChannel<bool>,
pub response: PipeCommChannel<AuthState>,
pub stop: PipeCommChannel<bool>,
}

Expand All @@ -31,74 +30,68 @@ impl AuthChannel {
}
}

#[atomic_enum]
pub enum AuthState {
Idle,
Success,
Fail,
}

#[derive(Debug)]
pub struct AuthConfig {
#[cfg(target_os = "linux")]
pub allow_empty: bool,
pwd_allow_empty: bool,
}

impl AuthConfig {
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
pub fn new(config: &NLockConfig) -> Self {
impl From<&NLockConfig> for AuthConfig {
fn from(value: &NLockConfig) -> Self {
Self {
#[cfg(target_os = "linux")]
allow_empty: config.general.pwd_allow_empty,
pwd_allow_empty: value.general.pwd_allow_empty,
}
}
}

#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
fn authenticate(config: &AuthConfig, username: &str, password: Zeroizing<String>) -> Result<()> {
let mut client = Client::with_password("nlock")?;
client
.conversation_mut()
.set_credentials(username, password.as_str());

#[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
let mut flags = PamFlag::None;

#[cfg(target_os = "linux")]
if !config.allow_empty {
flags = PamFlag::Disallow_Null_AuthTok;
}

client.authenticate(flags)?;
#[atomic_enum]
#[derive(PartialEq)]
pub enum AuthState {
Idle,
Success,
Fail,
}

#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
fn authenticate(client: &mut AuthClient, password: Zeroizing<String>) -> Result<()> {
client.set_password(password);
client.authenticate()?;
Ok(())
}

/// Handle an authentication request, returning a value to indicate success
fn handle_auth_request(config: &AuthConfig, auth_comm: Arc<AuthChannel>, username: &str) -> bool {
fn handle_auth_request(
config: &AuthConfig,
client: &mut AuthClient,
auth_comm: Arc<AuthChannel>,
) -> AuthState {
let pwd = match auth_comm.request.read().map(Zeroizing::new) {
Ok(p) => p,
Err(e) => {
warn!("Auth comm error: {e}");
return false;
return AuthState::Fail;
}
};

match authenticate(config, username, pwd) {
Ok(()) => true,
if !config.pwd_allow_empty && pwd.is_empty() {
debug!("Auth request ignored, password is empty");
return AuthState::Idle;
}

match authenticate(client, pwd) {
Ok(()) => AuthState::Success,
Err(e) => {
warn!("Auth failed: {e}");
false
AuthState::Fail
}
}
}

pub fn run_auth_loop(config: AuthConfig, auth_comm: Arc<AuthChannel>) -> Result<()> {
let username = uzers::get_current_username().ok_or(anyhow!("Current user does not exist"))?;
let username = username.to_string_lossy().to_string();

debug!("Running authenticator for '{username}'");

let mut success = false;
fn auth_loop(
config: AuthConfig,
mut client: AuthClient,
auth_comm: Arc<AuthChannel>,
) -> Result<()> {
let mut state = AuthState::Idle;

loop {
let req_fd = PollFd::new(auth_comm.request.rx().as_fd(), PollFlags::POLLIN);
Expand All @@ -115,11 +108,11 @@ pub fn run_auth_loop(config: AuthConfig, auth_comm: Arc<AuthChannel>) -> Result<
}

// auth was requested for a password
if events[0].any().unwrap_or_default() && !success {
success = handle_auth_request(&config, auth_comm.clone(), &username);
if events[0].any().unwrap_or_default() && state != AuthState::Success {
state = handle_auth_request(&config, &mut client, auth_comm.clone());

// dump auth result in response pipe
if let Err(e) = auth_comm.response.write(success) {
if let Err(e) = auth_comm.response.write(state) {
warn!("Failed to write auth response: {e}");
}
}
Expand All @@ -131,3 +124,18 @@ pub fn run_auth_loop(config: AuthConfig, auth_comm: Arc<AuthChannel>) -> Result<

Ok(())
}

pub fn setup_auth(config: AuthConfig, auth_comm: Arc<AuthChannel>) -> Result<JoinHandle<()>> {
let client = AuthClient::new("nlock")?;

let handle = std::thread::spawn({
move || {
if let Err(e) = auth_loop(config, client, auth_comm) {
warn!("Error in auth thread: {e}");
}
debug!("Auth thread exited");
}
});

Ok(handle)
}
Loading
Loading