Skip to content

Commit c23046b

Browse files
committed
Remove a potential sidechannel attack on cow names
1 parent cd20e3b commit c23046b

File tree

6 files changed

+72
-27
lines changed

6 files changed

+72
-27
lines changed

moooodotfarm-backend/Cargo.lock

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

moooodotfarm-backend/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ include_dir = "0.7.4"
3333
tonic = "0.12.3"
3434
prost = "0.13.5"
3535
async-trait = "0.1"
36-
36+
rand = "0.8"
3737
[build-dependencies]
3838
tonic-build = "0.12.3"

moooodotfarm-backend/src/app/get_herd.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use crate::app;
21
use crate::app::{Herd, Inventory, Metrics};
3-
use crate::errors::{Error, Result};
2+
use crate::domain::CensoredHerd;
3+
use crate::errors::Result;
4+
use crate::{app, domain};
45
use async_trait::async_trait;
56

67
#[derive(Clone)]
@@ -19,13 +20,12 @@ where
1920
}
2021

2122
async fn handle_inner(&self) -> Result<Herd> {
22-
let mut statuses = vec![];
23-
for cow in self.inventory.list()? {
24-
let censored_status = crate::domain::CensoredCow::new(&cow)?;
25-
statuses.push(censored_status);
26-
}
27-
let herd: Herd = statuses.try_into()?;
28-
Ok::<Herd, Error>(herd)
23+
let cows = self.inventory.list()?;
24+
let censored_cows = cows
25+
.into_iter()
26+
.map(|cow| domain::CensoredCow::new(&cow))
27+
.collect::<Result<Vec<domain::CensoredCow>>>()?;
28+
CensoredHerd::new(censored_cows).try_into()
2929
}
3030
}
3131

moooodotfarm-backend/src/app/mod.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,16 @@ impl Herd {
118118
}
119119
}
120120

121-
impl TryFrom<Vec<domain::CensoredCow>> for Herd {
121+
impl TryFrom<domain::CensoredHerd> for Herd {
122122
type Error = Error;
123123

124-
fn try_from(value: Vec<domain::CensoredCow>) -> Result<Self> {
125-
let cows: Result<Vec<_>> = value.iter().map(Cow::try_from).collect();
126-
Ok(Self { cows: cows? })
124+
fn try_from(value: domain::CensoredHerd) -> Result<Self> {
125+
let cows: Vec<Cow> = value
126+
.cows()
127+
.iter()
128+
.map(Cow::try_from)
129+
.collect::<Result<Vec<_>>>()?;
130+
Ok(Self { cows })
127131
}
128132
}
129133

moooodotfarm-backend/src/app/update.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::app::{CowTxtDownloader, Inventory, Metrics};
2+
use crate::domain::CensoredHerd;
23
use crate::errors::{Error, Result};
34
use crate::{app, domain};
45
use async_trait::async_trait;
@@ -38,38 +39,42 @@ where
3839
}
3940

4041
async fn handle_inner(&self) -> Result<()> {
41-
let mut censored_statuses = vec![];
42+
let mut cows: Vec<domain::Cow> = vec![];
4243

43-
for cow in self.inventory.list()? {
44-
if !cow.should_check() {
44+
for peeked_cow in self.inventory.list()? {
45+
if !peeked_cow.should_check() {
4546
continue;
4647
}
4748

48-
let result = self.downloader.download(cow.name()).await;
49+
let result = self.downloader.download(peeked_cow.name()).await;
4950

50-
self.inventory.update(cow.name(), |status| {
51-
if let Some(mut status) = status {
51+
self.inventory.update(peeked_cow.name(), |cow| {
52+
if let Some(mut cow) = cow {
5253
match result {
5354
Ok(_) => {
54-
status.mark_as_ok();
55+
cow.mark_as_ok();
5556
}
5657
Err(err) => {
5758
log::warn!("cow is missing {}: {}", cow, err);
58-
status.mark_as_missing();
59+
cow.mark_as_missing();
5960
}
6061
}
6162

62-
let censored_status = domain::CensoredCow::new(&cow)?;
63-
censored_statuses.push(censored_status);
63+
cows.push(cow.clone());
6464

65-
return Ok(Some(status));
65+
return Ok(Some(cow));
6666
}
6767

6868
Ok(None)
6969
})?;
7070
}
7171

72-
let herd: app::Herd = censored_statuses.try_into()?;
72+
let censored_cows: Vec<domain::CensoredCow> =
73+
cows.iter()
74+
.map(domain::CensoredCow::new)
75+
.collect::<Result<Vec<domain::CensoredCow>>>()?;
76+
let censored_herd = CensoredHerd::new(censored_cows);
77+
let herd: app::Herd = censored_herd.try_into()?;
7378
self.metrics.update_herd_numbers(&herd);
7479

7580
Ok::<(), Error>(())

moooodotfarm-backend/src/domain/mod.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::domain::time::{DateTime, Duration};
44
use crate::errors::Error;
55
use crate::errors::Result;
66
use anyhow::anyhow;
7+
use rand::seq::SliceRandom;
78
use std::fmt;
89
use std::fmt::{Display, Formatter};
910

@@ -118,7 +119,7 @@ impl fmt::Display for Cow {
118119
}
119120
}
120121

121-
#[derive(Debug, Clone, PartialEq, Eq)]
122+
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
122123
pub struct VisibleName {
123124
url: url::Url,
124125
}
@@ -283,6 +284,40 @@ impl CensoredCow {
283284
}
284285
}
285286

287+
impl TryFrom<&Cow> for CensoredCow {
288+
type Error = Error;
289+
290+
fn try_from(value: &Cow) -> Result<Self> {
291+
Self::new(value)
292+
}
293+
}
294+
pub struct CensoredHerd {
295+
cows: Vec<CensoredCow>,
296+
}
297+
298+
impl CensoredHerd {
299+
pub fn new(mut cows: Vec<CensoredCow>) -> Self {
300+
CensoredHerd::guard_against_side_channel_attacks(&mut cows);
301+
Self { cows }
302+
}
303+
304+
fn guard_against_side_channel_attacks(cows: &mut [CensoredCow]) {
305+
// we could rely on implementing Ord for VisibleName, but we want to be explicit here.
306+
// this type is supposed guard against sidechannel attacks and if someone ever changes
307+
// Ord for VisibleName this could lead to accidentally removing this safeguard
308+
let mut rng = rand::thread_rng();
309+
cows.shuffle(&mut rng);
310+
cows.sort_by(|a, b| match (a.name(), b.name()) {
311+
(Name::Censored(_), Name::Censored(_)) => std::cmp::Ordering::Equal,
312+
(Name::Censored(_), Name::Visible(_)) => std::cmp::Ordering::Greater,
313+
(Name::Visible(_), Name::Censored(_)) => std::cmp::Ordering::Less,
314+
(Name::Visible(a), Name::Visible(b)) => a.cmp(b),
315+
});
316+
}
317+
pub fn cows(&self) -> &[CensoredCow] {
318+
&self.cows
319+
}
320+
}
286321
pub struct CowTxt<'a> {
287322
content: std::borrow::Cow<'a, str>,
288323
}

0 commit comments

Comments
 (0)