Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit f5b34b5

Browse files
committed
Flatten the passwords config section
1 parent 8bc35f6 commit f5b34b5

File tree

4 files changed

+123
-104
lines changed

4 files changed

+123
-104
lines changed

crates/cli/src/util.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ pub async fn password_manager_from_config(
4141
.load()
4242
.await?
4343
.into_iter()
44-
.map(|(version, algorithm, secret)| {
44+
.map(|(version, algorithm, cost, secret)| {
4545
use mas_handlers::passwords::Hasher;
4646
let hasher = match algorithm {
4747
mas_config::PasswordAlgorithm::Pbkdf2 => Hasher::pbkdf2(secret),
48-
mas_config::PasswordAlgorithm::Bcrypt { cost } => Hasher::bcrypt(cost, secret),
48+
mas_config::PasswordAlgorithm::Bcrypt => Hasher::bcrypt(cost, secret),
4949
mas_config::PasswordAlgorithm::Argon2id => Hasher::argon2id(secret),
5050
};
5151

crates/config/src/sections/passwords.rs

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use std::borrow::Cow;
16-
1715
use anyhow::bail;
1816
use async_trait::async_trait;
1917
use camino::Utf8PathBuf;
@@ -27,7 +25,9 @@ fn default_schemes() -> Vec<HashingScheme> {
2725
vec![HashingScheme {
2826
version: 1,
2927
algorithm: Algorithm::Argon2id,
28+
cost: None,
3029
secret: None,
30+
secret_file: None,
3131
}]
3232
}
3333

@@ -66,6 +66,36 @@ impl ConfigurationSection for PasswordsConfig {
6666
Ok(Self::default())
6767
}
6868

69+
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
70+
let annotate = |mut error: figment::Error| {
71+
error.metadata = figment.find_metadata(Self::PATH.unwrap()).cloned();
72+
error.profile = Some(figment::Profile::Default);
73+
error.path = vec![Self::PATH.unwrap().to_owned()];
74+
Err(error)
75+
};
76+
77+
if !self.enabled {
78+
// Skip validation if password-based authentication is disabled
79+
return Ok(());
80+
}
81+
82+
if self.schemes.is_empty() {
83+
return annotate(figment::Error::from(
84+
"Requires at least one password scheme in the config".to_owned(),
85+
));
86+
}
87+
88+
for scheme in &self.schemes {
89+
if scheme.secret.is_some() && scheme.secret_file.is_some() {
90+
return annotate(figment::Error::from(
91+
"Cannot specify both `secret` and `secret_file`".to_owned(),
92+
));
93+
}
94+
}
95+
96+
Ok(())
97+
}
98+
6999
fn test() -> Self {
70100
Self::default()
71101
}
@@ -84,7 +114,9 @@ impl PasswordsConfig {
84114
///
85115
/// Returns an error if the config is invalid, or if the secret file could
86116
/// not be read.
87-
pub async fn load(&self) -> Result<Vec<(u16, Algorithm, Option<Vec<u8>>)>, anyhow::Error> {
117+
pub async fn load(
118+
&self,
119+
) -> Result<Vec<(u16, Algorithm, Option<u32>, Option<Vec<u8>>)>, anyhow::Error> {
88120
let mut schemes: Vec<&HashingScheme> = self.schemes.iter().collect();
89121
schemes.sort_unstable_by_key(|a| a.version);
90122
schemes.dedup_by_key(|a| a.version);
@@ -102,64 +134,53 @@ impl PasswordsConfig {
102134
let mut mapped_result = Vec::with_capacity(schemes.len());
103135

104136
for scheme in schemes {
105-
let secret = if let Some(secret_or_file) = &scheme.secret {
106-
Some(secret_or_file.load().await?.into_owned())
107-
} else {
108-
None
137+
let secret = match (&scheme.secret, &scheme.secret_file) {
138+
(Some(secret), None) => Some(secret.clone().into_bytes()),
139+
(None, Some(secret_file)) => {
140+
let secret = tokio::fs::read(secret_file).await?;
141+
Some(secret)
142+
}
143+
(Some(_), Some(_)) => bail!("Cannot specify both `secret` and `secret_file`"),
144+
(None, None) => None,
109145
};
110146

111-
mapped_result.push((scheme.version, scheme.algorithm, secret));
147+
mapped_result.push((scheme.version, scheme.algorithm, scheme.cost, secret));
112148
}
113149

114150
Ok(mapped_result)
115151
}
116152
}
117153

118-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
119-
#[serde(rename_all = "snake_case")]
120-
pub enum SecretOrFile {
121-
Secret(String),
122-
#[schemars(with = "String")]
123-
SecretFile(Utf8PathBuf),
124-
}
125-
126-
impl SecretOrFile {
127-
async fn load(&self) -> Result<Cow<'_, [u8]>, std::io::Error> {
128-
match self {
129-
Self::Secret(secret) => Ok(Cow::Borrowed(secret.as_bytes())),
130-
Self::SecretFile(path) => {
131-
let secret = tokio::fs::read(path).await?;
132-
Ok(Cow::Owned(secret))
133-
}
134-
}
135-
}
136-
}
137-
138154
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
139155
pub struct HashingScheme {
140156
version: u16,
141157

142-
#[serde(flatten)]
143158
algorithm: Algorithm,
144159

145-
#[serde(flatten)]
146-
secret: Option<SecretOrFile>,
160+
/// Cost for the bcrypt algorithm
161+
#[serde(skip_serializing_if = "Option::is_none")]
162+
#[schemars(default = "default_bcrypt_cost")]
163+
cost: Option<u32>,
164+
165+
#[serde(skip_serializing_if = "Option::is_none")]
166+
secret: Option<String>,
167+
168+
#[serde(skip_serializing_if = "Option::is_none")]
169+
#[schemars(with = "Option<String>")]
170+
secret_file: Option<Utf8PathBuf>,
147171
}
148172

149-
fn default_bcrypt_cost() -> u32 {
150-
12
173+
#[allow(clippy::unnecessary_wraps)]
174+
fn default_bcrypt_cost() -> Option<u32> {
175+
Some(12)
151176
}
152177

153178
/// A hashing algorithm
154179
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
155-
#[serde(rename_all = "lowercase", tag = "algorithm")]
180+
#[serde(rename_all = "lowercase")]
156181
pub enum Algorithm {
157182
/// bcrypt
158-
Bcrypt {
159-
/// Hashing cost
160-
#[serde(default = "default_bcrypt_cost")]
161-
cost: u32,
162-
},
183+
Bcrypt,
163184

164185
/// argon2id
165186
Argon2id,

crates/handlers/src/passwords.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ impl PasswordManager {
5353
/// PasswordManager::new([
5454
/// (3, Hasher::argon2id(Some(b"a-secret-pepper".to_vec()))),
5555
/// (2, Hasher::argon2id(None)),
56-
/// (1, Hasher::bcrypt(10, None)),
56+
/// (1, Hasher::bcrypt(Some(10), None)),
5757
/// ]).unwrap();
5858
/// ```
5959
///
@@ -216,7 +216,7 @@ pub struct Hasher {
216216
impl Hasher {
217217
/// Creates a new hashing scheme based on the bcrypt algorithm
218218
#[must_use]
219-
pub const fn bcrypt(cost: u32, pepper: Option<Vec<u8>>) -> Self {
219+
pub const fn bcrypt(cost: Option<u32>, pepper: Option<Vec<u8>>) -> Self {
220220
let algorithm = Algorithm::Bcrypt { cost };
221221
Self { algorithm, pepper }
222222
}
@@ -252,7 +252,7 @@ impl Hasher {
252252

253253
#[derive(Debug, Clone, Copy)]
254254
enum Algorithm {
255-
Bcrypt { cost: u32 },
255+
Bcrypt { cost: Option<u32> },
256256
Argon2id,
257257
Pbkdf2,
258258
}
@@ -273,7 +273,7 @@ impl Algorithm {
273273

274274
let salt = rng.gen();
275275

276-
let hashed = bcrypt::hash_with_salt(password, cost, salt)?;
276+
let hashed = bcrypt::hash_with_salt(password, cost.unwrap_or(12), salt)?;
277277
Ok(hashed.format_for_version(bcrypt::Version::TwoB))
278278
}
279279

@@ -369,7 +369,7 @@ mod tests {
369369
let pepper = b"a-secret-pepper";
370370
let pepper2 = b"the-wrong-pepper";
371371

372-
let alg = Algorithm::Bcrypt { cost: 10 };
372+
let alg = Algorithm::Bcrypt { cost: Some(10) };
373373
// Hash with a pepper
374374
let hash = alg
375375
.hash_blocking(&mut rng, password, Some(pepper))
@@ -454,6 +454,7 @@ mod tests {
454454
assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
455455
}
456456

457+
#[allow(clippy::too_many_lines)]
457458
#[tokio::test]
458459
async fn hash_verify_and_upgrade() {
459460
// Tests the whole password manager, by hashing a password and upgrading it
@@ -465,7 +466,10 @@ mod tests {
465466

466467
let manager = PasswordManager::new([
467468
// Start with one hashing scheme: the one used by synapse, bcrypt + pepper
468-
(1, Hasher::bcrypt(10, Some(b"a-secret-pepper".to_vec()))),
469+
(
470+
1,
471+
Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
472+
),
469473
])
470474
.unwrap();
471475

@@ -511,7 +515,10 @@ mod tests {
511515

512516
let manager = PasswordManager::new([
513517
(2, Hasher::argon2id(None)),
514-
(1, Hasher::bcrypt(10, Some(b"a-secret-pepper".to_vec()))),
518+
(
519+
1,
520+
Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
521+
),
515522
])
516523
.unwrap();
517524

@@ -562,7 +569,10 @@ mod tests {
562569
let manager = PasswordManager::new([
563570
(3, Hasher::argon2id(Some(b"a-secret-pepper".to_vec()))),
564571
(2, Hasher::argon2id(None)),
565-
(1, Hasher::bcrypt(10, Some(b"a-secret-pepper".to_vec()))),
572+
(
573+
1,
574+
Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
575+
),
566576
])
567577
.unwrap();
568578

docs/config.schema.json

Lines changed: 43 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,73 +1532,61 @@
15321532
}
15331533
},
15341534
"HashingScheme": {
1535-
"description": "A hashing algorithm",
15361535
"type": "object",
1537-
"oneOf": [
1538-
{
1539-
"description": "bcrypt",
1540-
"type": "object",
1541-
"required": [
1542-
"algorithm"
1543-
],
1544-
"properties": {
1545-
"algorithm": {
1546-
"type": "string",
1547-
"enum": [
1548-
"bcrypt"
1549-
]
1550-
},
1551-
"cost": {
1552-
"description": "Hashing cost",
1553-
"default": 12,
1554-
"type": "integer",
1555-
"format": "uint32",
1556-
"minimum": 0.0
1557-
}
1558-
}
1559-
},
1560-
{
1561-
"description": "argon2id",
1562-
"type": "object",
1563-
"required": [
1564-
"algorithm"
1565-
],
1566-
"properties": {
1567-
"algorithm": {
1568-
"type": "string",
1569-
"enum": [
1570-
"argon2id"
1571-
]
1572-
}
1573-
}
1574-
},
1575-
{
1576-
"description": "PBKDF2",
1577-
"type": "object",
1578-
"required": [
1579-
"algorithm"
1580-
],
1581-
"properties": {
1582-
"algorithm": {
1583-
"type": "string",
1584-
"enum": [
1585-
"pbkdf2"
1586-
]
1587-
}
1588-
}
1589-
}
1590-
],
15911536
"required": [
1537+
"algorithm",
15921538
"version"
15931539
],
15941540
"properties": {
15951541
"version": {
15961542
"type": "integer",
15971543
"format": "uint16",
15981544
"minimum": 0.0
1545+
},
1546+
"algorithm": {
1547+
"$ref": "#/definitions/Algorithm"
1548+
},
1549+
"cost": {
1550+
"description": "Cost for the bcrypt algorithm",
1551+
"default": 12,
1552+
"type": "integer",
1553+
"format": "uint32",
1554+
"minimum": 0.0
1555+
},
1556+
"secret": {
1557+
"type": "string"
1558+
},
1559+
"secret_file": {
1560+
"type": "string"
15991561
}
16001562
}
16011563
},
1564+
"Algorithm": {
1565+
"description": "A hashing algorithm",
1566+
"oneOf": [
1567+
{
1568+
"description": "bcrypt",
1569+
"type": "string",
1570+
"enum": [
1571+
"bcrypt"
1572+
]
1573+
},
1574+
{
1575+
"description": "argon2id",
1576+
"type": "string",
1577+
"enum": [
1578+
"argon2id"
1579+
]
1580+
},
1581+
{
1582+
"description": "PBKDF2",
1583+
"type": "string",
1584+
"enum": [
1585+
"pbkdf2"
1586+
]
1587+
}
1588+
]
1589+
},
16021590
"MatrixConfig": {
16031591
"description": "Configuration related to the Matrix homeserver",
16041592
"type": "object",

0 commit comments

Comments
 (0)