pam_u2f_mapping/
lib.rs

1// Copyright © Amanda Graven 2022
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License,
4// v. 2.0. If a copy of the MPL was not distributed with this file, You can
5// obtain one at https://mozilla.org/MPL/2.0/.
6
7//! Dead simple parser and formatter for mapping files generated by
8//! `pamu2fcfg(1)`.
9
10#![warn(missing_docs)]
11
12use std::str::FromStr;
13
14/// Represents the contents of a mapping file.
15#[derive(Clone, Debug)]
16pub struct MappingFile {
17	/// The list of mapping entries in the file
18	pub mappings: Vec<Mapping>,
19}
20
21/// The list of keys associated with a given username. Corresponds to one line in the mapping file
22#[derive(Clone, Debug)]
23pub struct Mapping {
24	/// The username the mapping applies to
25	pub user: String,
26	/// The list of keys associated with the user
27	pub keys: Vec<Key>,
28}
29
30/// A key entry in a mapping file. Corresponds to one colon (:) separated entry in a mapping line.
31#[derive(Clone, Debug)]
32pub struct Key {
33	/// The key handle
34	pub handle: String,
35	/// The public key data
36	pub public: String,
37	/// The key algorithm
38	pub kind: String,
39	/// Flags for the key
40	pub flags: Vec<String>,
41}
42
43impl FromStr for MappingFile {
44	type Err = Error;
45
46	fn from_str(s: &str) -> Result<Self, Self::Err> {
47		let mappings = s
48			.lines()
49			.map(Mapping::from_str)
50			.collect::<Result<Vec<Mapping>, Error>>()?;
51		Ok(MappingFile { mappings })
52	}
53}
54
55impl std::str::FromStr for Mapping {
56	type Err = Error;
57
58	fn from_str(s: &str) -> Result<Self, Self::Err> {
59		let mut fields = s.split(':');
60		let user = fields.next().ok_or(Error::UserMissing)?;
61		let mut keys = Vec::new();
62		for field in fields {
63			let mut subfields = field.split(',');
64			// split will always yield at least one item
65			let public = subfields.next().unwrap().to_owned();
66			let handle = subfields.next().ok_or(Error::HandleMissing)?.to_owned();
67			let kind = subfields.next().ok_or(Error::KindMissing)?.to_owned();
68			let flags = subfields.next().ok_or(Error::FlagsMissing)?.to_owned();
69			let mut flags = flags.split('+');
70			if flags.next() != Some("") {
71				return Err(Error::BadFlags);
72			}
73			let flags = flags.map(|s| s.to_owned()).collect::<Vec<_>>();
74			keys.push(Key {
75				public,
76				handle,
77				kind,
78				flags,
79			})
80		}
81		Ok(Mapping {
82			user: user.to_owned(),
83			keys,
84		})
85	}
86}
87
88impl std::fmt::Display for Mapping {
89	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90		write!(f, "{}", self.user)?;
91		for key in &self.keys {
92			write!(f, ":{},{},{},", &key.handle, &key.public, &key.kind)?;
93			for flag in &key.flags {
94				write!(f, "+{flag}")?;
95			}
96		}
97		Ok(())
98	}
99}
100
101/// The key contained invalid data and failed to parse
102#[derive(Debug, Clone, Copy)]
103pub enum Error {
104	/// User field was missing from a mapping
105	UserMissing,
106	/// Key handle was missing
107	HandleMissing,
108	/// Key kind was missing
109	KindMissing,
110	/// Key flags were missing
111	FlagsMissing,
112	/// Key flags were malformed
113	BadFlags,
114}
115
116impl std::fmt::Display for Error {
117	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118		use Error::*;
119		let s = match *self {
120			UserMissing => "Entry has no username",
121			HandleMissing => "Missing second half of key data",
122			KindMissing => "Entry has no key type",
123			FlagsMissing => "Entry has no flags",
124			BadFlags => "Entry has ill-formed flags",
125		};
126		f.write_str(s)
127	}
128}
129
130impl std::error::Error for Error {}
131
132#[cfg(test)]
133mod tests {
134	use super::Mapping;
135	type BoxError = Box<dyn std::error::Error>;
136	const TEST_MAPPING: &str = "alice:\
137		owBYtYMabYlexEG10ildyDLNqwkpeIZyc4YwqP6yUnqlQ3DCxNMjPXoGcQOPiNXu2kFuGKs\
138		LsN6am/UCjgUpwvr9G54GyY85i0zt/vHRsU+OayYoalSjsVjBvyRqFai3fZUdGEHVLdpw9Y\
139		Z3MZeJiSWWEumF59CBdFWNLtq0Xi5M1katPXKIqOUSHLePlq1UfaGkh7R5y+Cv8jXtrhtak\
140		ROcMXjrAfo+5Wq0hNe0JiQwxFPufHUJ8IMBTFw4Qv3TnPGcVFTXZgJQU1FguzVlQ6pU7FS6\
141		37Dhdg==,\
142		IiFyv2O8qSG517c2ghvHEbMb6xs5ToPaoOXdgGkkorH2ta/iYWtOhMB7wxaiS3BhOHSxcJU\
143		JJkMLmfUWl8Uivw==,\
144		es256,+presence";
145	#[test]
146	fn parse() -> Result<(), BoxError> {
147		let mapping: Mapping = TEST_MAPPING.parse()?;
148		assert_eq!(mapping.user, "alice", "user mismatch");
149		let key = &mapping.keys[0];
150		assert_eq!(
151			key.handle,
152			"owBYtYMabYlexEG10ildyDLNqwkpeIZyc4YwqP6yUnqlQ3DCxNMjPXoGcQOPiNXu2k\
153			FuGKsLsN6am/UCjgUpwvr9G54GyY85i0zt/vHRsU+OayYoalSjsVjBvyRqFai3fZUdG\
154			EHVLdpw9YZ3MZeJiSWWEumF59CBdFWNLtq0Xi5M1katPXKIqOUSHLePlq1UfaGkh7R5\
155			y+Cv8jXtrhtakROcMXjrAfo+5Wq0hNe0JiQwxFPufHUJ8IMBTFw4Qv3TnPGcVFTXZgJ\
156			QU1FguzVlQ6pU7FS637Dhdg==",
157			"key handle mismatch"
158		);
159		assert_eq!(
160			key.public,
161			"IiFyv2O8qSG517c2ghvHEbMb6xs5ToPaoOXdgGkkorH2ta/iYWtOhMB7wxaiS3BhOHSxcJUJJkMLmfUWl8Uivw==",
162			"public key mismatch"
163		);
164		assert_eq!(key.kind, "es256");
165		assert_eq!(key.flags, &["presence"]);
166		Ok(())
167	}
168	/// Asserts that a file gets parsed and formatted to the same data
169	#[test]
170	fn non_destructive() -> Result<(), BoxError> {
171		assert_eq!(TEST_MAPPING.parse::<Mapping>()?.to_string(), TEST_MAPPING);
172		Ok(())
173	}
174}