oath_authenticator/
command.rs

1use core::convert::{TryFrom, TryInto};
2
3use iso7816::{Data, Status};
4
5use crate::oath;
6
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum Command<'l> {
10    /// Select the application
11    Select(Select<'l>),
12    /// Calculate the authentication data for a credential given by label.
13    Calculate(Calculate<'l>),
14    /// Calculate the authentication data for all credentials.
15    CalculateAll(CalculateAll<'l>),
16    /// Clear the password.
17    ClearPassword,
18    /// Delete a credential.
19    Delete(Delete<'l>),
20    /// List all credentials.
21    ListCredentials,
22    /// Register a new credential.
23    Register(Register<'l>),
24    /// Delete all credentials and rotate the salt.
25    Reset,
26    /// Set a password.
27    SetPassword(SetPassword<'l>),
28    /// Validate the password (both ways).
29    Validate(Validate<'l>),
30}
31
32/// TODO: change into enum
33#[derive(Clone, Copy, Eq, PartialEq)]
34pub struct Select<'l> {
35    pub aid: &'l [u8],
36}
37
38impl core::fmt::Debug for Select<'_> {
39    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> {
40        fmt.debug_struct("Select")
41            .field("aid", &hex_str!(&self.aid, 5))
42            .finish()
43    }
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct SetPassword<'l> {
48    pub kind: oath::Kind,
49    pub algorithm: oath::Algorithm,
50    pub key: &'l [u8],
51    pub challenge: &'l [u8],
52    pub response: &'l [u8],
53}
54
55impl<'l, const C: usize> TryFrom<&'l Data<C>> for SetPassword<'l> {
56    type Error = Status;
57    fn try_from(data: &'l Data<C>) -> Result<Self, Self::Error> {
58        // key = self.derive_key(password)
59        // keydata = bytearray([OATH_TYPE.TOTP | ALGO.SHA1]) + key
60        // challenge = os.urandom(8)
61        // h = hmac.HMAC(key, hashes.SHA1(), default_backend())  # nosec
62        // h.update(challenge)
63        // response = h.finalize()
64        // data = Tlv(TAG.KEY, keydata) + Tlv(TAG.CHALLENGE, challenge) + Tlv(
65        //     TAG.RESPONSE, response)
66        // self.send_apdu(INS.SET_CODE, 0, 0, data)
67        // return key
68
69        use flexiber::TaggedSlice;
70        let mut decoder = flexiber::Decoder::new(data);
71        let slice: TaggedSlice = decoder.decode().unwrap();
72        assert!(slice.tag() == (oath::Tag::Key as u8).try_into().unwrap());
73        let (key_header, key) = slice.as_bytes().split_at(1);
74
75        let kind: oath::Kind = key_header[0].try_into()?;
76        // assert!(kind == oath::Kind::Totp);
77        let algorithm: oath::Algorithm = key_header[0].try_into()?;
78        // assert!(algorithm == oath::Algorithm::Sha1);
79
80        let slice: TaggedSlice = decoder.decode().unwrap();
81        assert!(slice.tag() == (oath::Tag::Challenge as u8).try_into().unwrap());
82        let challenge = slice.as_bytes();
83        // assert_eq!(challenge.len(), 8);
84
85        let slice: TaggedSlice = decoder.decode().unwrap();
86        assert!(slice.tag() == (oath::Tag::Response as u8).try_into().unwrap());
87        let response = slice.as_bytes();
88        // assert_eq!(response.len(), 20);
89
90        Ok(SetPassword {
91            kind,
92            algorithm,
93            key,
94            challenge,
95            response,
96        })
97    }
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub struct Validate<'l> {
102    pub response: &'l [u8],
103    pub challenge: &'l [u8],
104}
105
106impl<'l, const C: usize> TryFrom<&'l Data<C>> for Validate<'l> {
107    type Error = Status;
108    fn try_from(data: &'l Data<C>) -> Result<Self, Self::Error> {
109        use flexiber::TaggedSlice;
110        let mut decoder = flexiber::Decoder::new(data);
111
112        let slice: TaggedSlice = decoder.decode().unwrap();
113        assert!(slice.tag() == (oath::Tag::Response as u8).try_into().unwrap());
114        let response = slice.as_bytes();
115
116        let slice: TaggedSlice = decoder.decode().unwrap();
117        assert!(slice.tag() == (oath::Tag::Challenge as u8).try_into().unwrap());
118        let challenge = slice.as_bytes();
119
120        Ok(Validate { challenge, response })
121    }
122}
123
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
125pub struct Calculate<'l> {
126    pub label: &'l [u8],
127    pub challenge: &'l [u8],
128}
129
130impl<'l, const C: usize> TryFrom<&'l Data<C>> for Calculate<'l> {
131    type Error = Status;
132    fn try_from(data: &'l Data<C>) -> Result<Self, Self::Error> {
133        use flexiber::TaggedSlice;
134        let mut decoder = flexiber::Decoder::new(data);
135
136        let first: TaggedSlice = decoder.decode().unwrap();
137        assert!(first.tag() == (oath::Tag::Name as u8).try_into().unwrap());
138        let label = first.as_bytes();
139
140        let second: TaggedSlice = decoder.decode().unwrap();
141        assert!(second.tag() == (oath::Tag::Challenge as u8).try_into().unwrap());
142        let challenge = second.as_bytes();
143
144        Ok(Calculate { label, challenge })
145    }
146}
147
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
149pub struct CalculateAll<'l> {
150    pub challenge: &'l [u8],
151}
152
153impl<'l, const C: usize> TryFrom<&'l Data<C>> for CalculateAll<'l> {
154    type Error = Status;
155    fn try_from(data: &'l Data<C>) -> Result<Self, Self::Error> {
156        use flexiber::TaggedSlice;
157        let mut decoder = flexiber::Decoder::new(data);
158
159        let first: TaggedSlice = decoder.decode().unwrap();
160        assert!(first.tag() == (oath::Tag::Challenge as u8).try_into().unwrap());
161        let challenge = first.as_bytes();
162
163        Ok(CalculateAll { challenge })
164    }
165}
166
167
168#[derive(Clone, Copy, Eq, PartialEq)]
169pub struct Delete<'l> {
170    pub label: &'l [u8],
171}
172
173impl core::fmt::Debug for Delete<'_> {
174    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> {
175        fmt.debug_struct("Credential")
176            .field("label", &core::str::from_utf8(self.label).unwrap_or(&"invalid UTF8 label"))
177            .finish()
178    }
179}
180
181
182impl<'l, const C: usize> TryFrom<&'l Data<C>> for Delete<'l> {
183    type Error = iso7816::Status;
184    fn try_from(data: &'l Data<C>) -> Result<Self, Self::Error> {
185        use flexiber::TaggedSlice;
186        let mut decoder = flexiber::Decoder::new(data);
187
188        let first: TaggedSlice = decoder.decode().unwrap();
189        assert!(first.tag() == (oath::Tag::Name as u8).try_into().unwrap());
190        let label = first.as_bytes();
191
192        Ok(Delete { label })
193    }
194}
195
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
197pub struct Register<'l> {
198    pub credential: Credential<'l>,
199}
200
201#[derive(Clone, Copy, Eq, PartialEq)]
202pub struct Credential<'l> {
203    pub label: &'l [u8],
204    pub kind: oath::Kind,
205    pub algorithm: oath::Algorithm,
206    pub digits: u8,
207    /// What we get here (inspecting the client app) may not be the raw K, but K' in HMAC lingo,
208    /// i.e., If secret.len() < block size (64B for Sha1/Sha256, 128B for Sha512),
209    /// then it's the hash of the secret.  Otherwise, it's the secret, padded to length
210    /// at least 14B with null bytes. This is of no concern to us, as is it does not
211    /// change the MAC.
212    ///
213    /// The 14 is a bit strange: RFC 4226, section 4 says:
214    /// "The algorithm MUST use a strong shared secret.  The length of the shared secret MUST be
215    /// at least 128 bits.  This document RECOMMENDs a shared secret length of 160 bits."
216    ///
217    /// Meanwhile, the client app just pads up to 14B :)
218
219    pub secret: &'l [u8],
220    pub touch_required: bool,
221    pub counter: Option<u32>,
222}
223
224impl core::fmt::Debug for Credential<'_> {
225    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> {
226        fmt.debug_struct("Credential")
227            .field("label", &core::str::from_utf8(self.label).unwrap_or(&"invalid UTF8 label")) //(format!("{}", &hex_str!(&self.label))))
228            .field("kind", &self.kind)
229            .field("alg", &self.algorithm)
230            .field("digits", &self.digits)
231            .field("secret", &hex_str!(&self.secret, 4))
232            .field("touch", &self.touch_required)
233            .field("counter", &self.counter)
234            .finish()
235    }
236}
237
238// This is totally broken at the moment in flexiber
239//
240// #[derive(Decodable)]
241// pub struct SerializedPut<'l> {
242//     // #[tlv(simple="oath::Tag::Name as u8")]
243//     #[tlv(simple="0x71")]
244//     pub label: &'l [u8],
245// }
246
247#[derive(Clone, Copy, Debug, Eq, PartialEq)]
248struct Properties(u8);
249
250impl Properties {
251    fn touch_required(&self) -> bool {
252        self.0 & (oath::Properties::RequireTouch as u8) != 0
253    }
254}
255impl<'a> flexiber::Decodable<'a> for Properties {
256    fn decode(decoder: &mut flexiber::Decoder<'a>) -> flexiber::Result<Properties> {
257        let two_bytes: [u8; 2] = decoder.decode()?;
258        let [tag, properties] = two_bytes;
259        use flexiber::Tagged;
260        assert_eq!(flexiber::Tag::try_from(tag).unwrap(), Self::tag());
261        Ok(Properties(properties))
262    }
263}
264impl flexiber::Tagged for Properties {
265    fn tag() -> flexiber::Tag {
266        let ret = flexiber::Tag::try_from(oath::Tag::Property as u8).unwrap();
267        ret
268
269    }
270}
271
272impl<'l, const C: usize> TryFrom<&'l Data<C>> for Register<'l> {
273    type Error = iso7816::Status;
274    fn try_from(data: &'l Data<C>) -> Result<Self, Self::Error> {
275        use flexiber::{Decodable, TagLike};
276        type TaggedSlice<'a> = flexiber::TaggedSlice<'a, flexiber::SimpleTag>;
277        let mut decoder = flexiber::Decoder::new(data);
278
279        // first comes the label of the credential, with Tag::Name
280        let first: TaggedSlice = decoder.decode().unwrap();
281        assert!(first.tag() == (oath::Tag::Name as u8).try_into().unwrap());
282        let label = first.as_bytes();
283
284        // then come (kind,algorithm,digits) and the actual secret (somewhat massaged)
285        let second: TaggedSlice = decoder.decode().unwrap();
286        second.tag().assert_eq((oath::Tag::Key as u8).try_into().unwrap()).unwrap();
287        let (secret_header, secret) = second.as_bytes().split_at(2);
288
289        let kind: oath::Kind = secret_header[0].try_into()?;
290        let algorithm: oath::Algorithm = secret_header[0].try_into()?;
291        let digits = secret_header[1];
292
293        let maybe_properties: Option<Properties> = decoder.decode().unwrap();
294        // info_now!("maybe_properties: {:?}", &maybe_properties);
295
296        let touch_required = maybe_properties
297            .map(|properties| {
298                info_now!("unraveling {:?}", &properties);
299                properties.touch_required()
300            })
301            .unwrap_or(false);
302
303        let mut counter = None;
304        // kind::Hotp and valid u32 starting counter should be more tightly tied together on a
305        // type level
306        if kind == oath::Kind::Hotp {
307            // when the counter is not specified or set to zero, ykman does not send it
308            counter = Some(0);
309            if let Ok(last) = TaggedSlice::decode(&mut decoder) {
310                if last.tag() == (oath::Tag::InitialMovingFactor as u8).try_into().unwrap() {
311                    let bytes = last.as_bytes();
312                    if bytes.len() == 4 {
313                        counter = Some(u32::from_be_bytes(bytes.try_into().unwrap()));
314                    }
315                }
316            }
317            debug_now!("counter set to {:?}", &counter);
318        }
319
320        let credential = Credential {
321            label,
322            kind,
323            algorithm,
324            digits,
325            secret,
326            touch_required,
327            counter,
328        };
329
330        Ok(Register { credential })
331    }
332}
333
334impl<'l, const C: usize> TryFrom<&'l iso7816::Command<C>> for Command<'l> {
335    type Error = Status;
336    /// The first layer of unraveling the iso7816::Command onion.
337    ///
338    /// The responsibility here is to check (cla, ins, p1, p2) are valid as defined
339    /// in the "Command Syntax" boxes of NIST SP 800-73-4, and return early errors.
340    ///
341    /// The individual piv::Command TryFroms then further interpret these validated parameters.
342    fn try_from(command: &'l iso7816::Command<C>) -> Result<Self, Self::Error> {
343        let (class, instruction, p1, p2) = (command.class(), command.instruction(), command.p1, command.p2);
344        let data = command.data();
345
346        if !class.secure_messaging().none() {
347            return Err(Status::SecureMessagingNotSupported);
348        }
349
350        if class.channel() != Some(0) {
351            return Err(Status::LogicalChannelNotSupported);
352        }
353
354        // TODO: should we check `command.expected() == 0`, where specified?
355
356        if (0x00, iso7816::Instruction::Select, 0x04, 0x00) == (class.into_inner(), instruction, p1, p2) {
357            Ok(Self::Select(Select::try_from(data)?))
358        } else {
359            let instruction_byte: u8 = instruction.into();
360            let instruction: oath::Instruction = instruction_byte.try_into()?;
361            Ok(match (class.into_inner(), instruction, p1, p2) {
362                         // also 0xa4
363                (0x00, oath::Instruction::Calculate, 0x00, 0x01) => Self::Calculate(Calculate::try_from(data)?),
364                (0x00, oath::Instruction::CalculateAll, 0x00, 0x01) => Self::CalculateAll(CalculateAll::try_from(data)?),
365                (0x00, oath::Instruction::Delete, 0x00, 0x00) => Self::Delete(Delete::try_from(data)?),
366                (0x00, oath::Instruction::List, 0x00, 0x00) => Self::ListCredentials,
367                (0x00, oath::Instruction::Put, 0x00, 0x00) => Self::Register(Register::try_from(data)?),
368                (0x00, oath::Instruction::Reset, 0xde, 0xad) => Self::Reset,
369                (0x00, oath::Instruction::SetCode, 0x00, 0x00) => {
370                    // should check this is a TLV(SetPassword, b'')
371                    if data.len() == 2 {
372                        Self::ClearPassword
373                    } else {
374                        Self::SetPassword(SetPassword::try_from(data)?)
375                    }
376                }
377                (0x00, oath::Instruction::Validate, 0x00, 0x00) => Self::Validate(Validate::try_from(data)?),
378                _ => return Err(Status::InstructionNotSupportedOrInvalid),
379            })
380        }
381    }
382}
383
384impl<'l, const C: usize> TryFrom<&'l Data<C>> for Select<'l> {
385    type Error = Status;
386    fn try_from(data: &'l Data<C>) -> Result<Self, Self::Error> {
387        // info_now!("comparing {} against {}", hex_str!(data.as_slice()), hex_str!(crate::YUBICO_OATH_AID));
388        Ok(match data.as_slice() {
389            crate::YUBICO_OATH_AID => Self { aid: data },
390            _ => return Err(Status::NotFound),
391        })
392    }
393}
394