Skip to content

Commit 4474b8e

Browse files
authored
fix: Prevent timing attacks (#79)
Make an effort to prevent timing attacks when checking the API key received in requests. While this change should be considered an improvement, it does not guarantee that timing attacks can be prevented under all conditions.
1 parent 8e2bbd9 commit 4474b8e

File tree

3 files changed

+60
-1
lines changed

3 files changed

+60
-1
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ log = { version = "0.4", optional = true }
1515
env_logger = { version = "0.11.8", optional = true }
1616
clap = { version = "4.5.46", features = ["color", "derive", "help", "usage", "std", "env"] }
1717
redis = { version = "0.25.4", features = [ "ahash", "aio", "tokio-comp" ] }
18+
sha2 = "0.10.9"
1819

1920
[features]
2021
default = ["logging"]

src/auth.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
use std::{hint::black_box, io::Read};
2+
13
use axum::{headers::Authorization, TypedHeader};
24
use http::HeaderValue;
5+
use sha2::Digest;
36

47
pub struct ApiKey(String);
58

@@ -26,7 +29,50 @@ pub fn accept_auth(
2629
None => return true,
2730
};
2831
match header {
29-
Some(TypedHeader(Authorization(ApiKey(presented)))) => expected == &presented,
32+
Some(TypedHeader(Authorization(ApiKey(presented)))) => const_comp(expected, &presented),
3033
None => false,
3134
}
3235
}
36+
37+
/// Function for comparing two strings in equal time. I.e. the similarity of the strings should
38+
/// have no bearing on the time it takes to compare them.
39+
///
40+
/// A naive solution to compare two strings for equality would most likely return at the first byte
41+
/// that differs, which means that the more similar two strings are, the longer the execution time
42+
/// for comparing the strings. This would make such naive implementation susceptible to a
43+
/// [timing attack](https://en.wikipedia.org/wiki/Timing_attack), which should ideally be avoided.
44+
fn const_comp(i0: impl AsRef<[u8]>, i1: impl AsRef<[u8]>) -> bool {
45+
// Hash inputs so we always get equal length when comparing, so we do not risk leaking the
46+
// length of the expected API key via a timing attack.
47+
let h0 = sha2::Sha384::digest(i0);
48+
let h1 = sha2::Sha384::digest(i1);
49+
// The documentation for black box explicitly states that _"this function does not offer any
50+
// guarantees for cryptographic or security purposes"_. But the other two options are to
51+
// either
52+
// 1. Take no measures at all to prevent unwanted optimizations
53+
// 2. Try to hand roll a constant time comparison algorithm implemented in assembler
54+
//
55+
// The first option seems worse than taking no action at all, and the later is not really
56+
// feasible.
57+
black_box(
58+
h0.bytes()
59+
.zip(h1.bytes())
60+
.fold(0, |acc, (x, y)| acc | (x.unwrap() ^ y.unwrap()))
61+
== 0,
62+
)
63+
}
64+
65+
#[cfg(test)]
66+
mod tests {
67+
use crate::auth::const_comp;
68+
69+
#[test]
70+
fn compare_on_equal() {
71+
assert!(const_comp("foo", "foo"));
72+
}
73+
74+
#[test]
75+
fn compare_on_not_equal() {
76+
assert!(!const_comp("foo", "bar"));
77+
}
78+
}

0 commit comments

Comments
 (0)