Skip to content

Commit 1df6ad2

Browse files
committedFeb 21, 2025·
feat: lock device only after 10s of inactivity
1 parent 3e6970c commit 1df6ad2

File tree

9 files changed

+718
-238
lines changed

9 files changed

+718
-238
lines changed
 

‎Cargo.lock

+558-16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7-
anyhow = "1.0.95"
7+
anyhow = "1.0.96"
88
bluer = { version = "0.17.3", features = ["full"] }
99
clap = { version = "4.5.30", features = ["derive"] }
1010
dirs = "6.0.0"
1111
env_logger = "0.11.6"
1212
figment = { version = "0.10", features = ["toml", "env"] }
1313
futures = "0.3.31"
14-
serde = "1.0.217"
14+
log = "0.4.26"
15+
serde = "1.0.218"
1516
tokio = { version = "1.43.0", features = ["full"] }
17+
zbus = "5.5.0"
18+
zbus_systemd = { version = "0.25701.0", features = ["login1"] }

‎README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ security risk to unlock a device when a device is near. Locking it is fine since
1515
that's not a security risk. Paired device and better security approach to
1616
unlocking will be explored in the future.
1717

18-
## Roadmap
18+
## Roadmap and Changelog
1919

2020
- [ ] Secure unlocking of devices
2121
- [ ] System Tray for Quick Access
22-
- [ ] Detect user activity before locking the device
22+
- [x] Lock device when you're away and after 10 seconds of inactivity. This is
23+
to prevent accidental lockinng when your device fails to send its signal
24+
- [x] Add a `keep-unlocked` option for more secure replacement for `unlock`
2325

2426
## LICENSE
2527

‎config/example.toml

+10
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,13 @@ actions = [
88
{ type = "away", threshold=2.0, command = "lock"},
99
{ type = "nearby", threshold=1.0, command = "unlock"},
1010
]
11+
12+
[[connection]]
13+
name = "My Phone"
14+
type = "ble"
15+
mac = "AA:BB:CC:DD:EE:FF"
16+
owner = "self"
17+
actions = [
18+
{ type = "away", threshold=1.0, command = "lock"},
19+
{ type = "nearby", threshold=2.0, command = "keep-unlocked"},
20+
]

‎src/commands/mod.rs

+4-47
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,16 @@
1-
use crate::delayed::Delayed;
21
use serde::Deserialize;
3-
use std::{process::Output, sync::Mutex};
2+
use std::process::Output;
43

5-
#[derive(Debug, Deserialize, Clone)]
6-
#[serde(rename_all = "lowercase")]
4+
#[derive(Debug, Deserialize, Clone, PartialEq)]
5+
#[serde(rename_all = "kebab-case")]
76
pub enum Command {
87
Unlock,
8+
KeepUnlocked,
99
Lock,
1010
String(String),
1111
}
1212

13-
// fixme: don't use sudo, use proper permissions
1413
// fixme: use dbus to lock/unlock
15-
static LOCKED: Mutex<bool> = Mutex::new(false);
16-
static DELAYED_LOCK: Mutex<Option<Delayed>> = Mutex::new(None);
17-
18-
impl Command {
19-
pub fn run(&self) -> anyhow::Result<()> {
20-
let locked = *LOCKED.lock().unwrap();
21-
match self {
22-
Command::Unlock => {
23-
// cancel the delayed lock
24-
if let Some(delayed) = &mut *DELAYED_LOCK.lock().unwrap() {
25-
delayed.cancel();
26-
}
27-
if locked {
28-
println!("Unlocking desktop...");
29-
run("sudo loginctl unlock-sessions")?;
30-
*LOCKED.lock().unwrap() = false;
31-
};
32-
}
33-
34-
// fixme: unlocking might not be good idea if it wasn't locked automatically
35-
Command::Lock => {
36-
let duration = std::time::Duration::from_secs(15);
37-
if !locked {
38-
println!("Locking desktop in {:?}", duration);
39-
if let Some(delayed) = &mut *DELAYED_LOCK.lock().unwrap() {
40-
delayed.cancel();
41-
}
42-
// wait before actually locking the desktop
43-
let delayed = Delayed::new(duration, || async {
44-
run("sudo loginctl lock-sessions").expect("error running lock command");
45-
*LOCKED.lock().unwrap() = true;
46-
});
47-
*DELAYED_LOCK.lock().unwrap() = Some(delayed);
48-
}
49-
}
50-
Command::String(cmd) => {
51-
run(cmd)?;
52-
}
53-
};
54-
Ok(())
55-
}
56-
}
5714

5815
pub fn run(cmd: &str) -> anyhow::Result<Output> {
5916
let output = std::process::Command::new("sh")

‎src/config.rs

+71-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::commands::Command;
1+
use crate::{commands::Command, distance_rssi};
22
use figment::{
33
providers::{Env, Format, Toml},
44
Figment,
@@ -18,11 +18,39 @@ impl Config {
1818
.unwrap_or_default()
1919
}
2020

21-
pub fn get_connection_by_mac(&self, mac: &str) -> Option<&Connection> {
21+
pub fn update_rssi(&mut self, mac: &str, rssi: i16) {
22+
if let Some(connections) = &mut self.connection {
23+
for connection in connections.iter_mut() {
24+
match connection {
25+
Connection::Ble(ble) => {
26+
if ble.mac == mac {
27+
ble.rssi = Some(rssi);
28+
}
29+
}
30+
}
31+
}
32+
}
33+
}
34+
35+
pub fn should_lock(&self) -> bool {
36+
self.connections()
37+
.iter()
38+
.filter_map(|c| c.get_ble())
39+
.any(|ble| ble.should_lock())
40+
}
41+
42+
pub fn can_unlock(&self) -> bool {
43+
self.connections()
44+
.iter()
45+
.filter_map(|c| c.get_ble())
46+
.any(|ble| ble.can_unlock())
47+
}
48+
49+
pub fn keep_unlocked(&self) -> bool {
2250
self.connections()
2351
.iter()
24-
.find(|c| c.get_ble().is_some_and(|ble| ble.mac == mac))
25-
.copied()
52+
.filter_map(|c| c.get_ble())
53+
.any(|ble| ble.keep_unlocked())
2654
}
2755
}
2856

@@ -43,33 +71,55 @@ impl Connection {
4371

4472
#[derive(Deserialize, Debug)]
4573
pub struct BLEConnection {
46-
pub name: String,
4774
pub mac: String,
75+
pub rssi: Option<i16>,
4876
pub actions: Option<Vec<Action>>,
4977
}
5078

5179
impl BLEConnection {
52-
pub fn run_proximity_actions(&self, distance: f32) {
53-
if let Some(actions) = &self.actions {
54-
for action in actions {
55-
match action {
80+
pub fn can_unlock(&self) -> bool {
81+
let distance = self.rssi.map(distance_rssi).unwrap_or(1000.0);
82+
self.actions
83+
.as_ref()
84+
.map(|actions| {
85+
actions.iter().any(|a| match a {
5686
Action::Nearby(action) => {
57-
if distance > action.threshold {
58-
continue;
59-
}
87+
distance < action.threshold && action.command == Command::Unlock
88+
}
89+
Action::Away(_) => false,
90+
})
91+
})
92+
.unwrap_or(false)
93+
}
6094

61-
action.command.run().unwrap();
95+
pub fn keep_unlocked(&self) -> bool {
96+
let distance = self.rssi.map(distance_rssi).unwrap_or(1000.0);
97+
self.actions
98+
.as_ref()
99+
.map(|actions| {
100+
actions.iter().any(|a| match a {
101+
Action::Nearby(action) => {
102+
distance < action.threshold && action.command == Command::KeepUnlocked
62103
}
63-
Action::Away(action) => {
64-
if distance < action.threshold {
65-
continue;
66-
}
104+
Action::Away(_) => false,
105+
})
106+
})
107+
.unwrap_or(false)
108+
}
67109

68-
action.command.run().unwrap();
110+
pub fn should_lock(&self) -> bool {
111+
let distance = self.rssi.map(distance_rssi).unwrap_or(1000.0);
112+
self.actions
113+
.as_ref()
114+
.map(|actions| {
115+
actions.iter().any(|a| match a {
116+
Action::Nearby(_) => false,
117+
Action::Away(action) => {
118+
distance > action.threshold && action.command == Command::Lock
69119
}
70-
}
71-
}
72-
}
120+
})
121+
})
122+
.unwrap_or(false)
73123
}
74124
}
75125

‎src/delayed.rs

-113
This file was deleted.

‎src/idle.rs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use zbus_systemd::login1::ManagerProxy;
2+
3+
pub async fn get_idle_hint() -> zbus::Result<(bool, u64)> {
4+
let manager = login_manager().await?;
5+
let idle_hint = manager.idle_hint().await?;
6+
let idle_hint_time = manager.idle_since_hint().await?;
7+
Ok((idle_hint, idle_hint_time))
8+
}
9+
10+
async fn login_manager() -> zbus::Result<ManagerProxy<'static>> {
11+
let conn = zbus::Connection::system().await?;
12+
let manager = ManagerProxy::new(&conn).await?;
13+
Ok(manager)
14+
}

0 commit comments

Comments
 (0)
Please sign in to comment.