solo2 0.2.2

Library and CLI for the SoloKeys Solo 2 security key
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! Solo 2 devices, which may be in regular or bootloader mode.
use core::sync::atomic::{AtomicBool, Ordering};
use std::collections::{BTreeMap, BTreeSet};

use anyhow::anyhow;
use lpc55::bootloader::{Bootloader as Lpc55, UuidSelectable};

use crate::{apps::Admin, Firmware, Result, Select as _, Uuid, Version};
use core::fmt;

pub mod ctap;
pub mod pcsc;

/// A [SoloKeys][solokeys] [Solo 2][solo2] device, in regular mode.
///
/// From an inventory perspective, the core identifier is a UUID (16 bytes / 128 bits).
///
/// From an interface perspective, either the CTAP or PCSC transport must be available.
/// Therefore, it is an invariant that at least one is interface, and the device itself
/// implements [Transport][crate::Transport].
///
/// [solokeys]: https://solokeys.com
/// [solo2]: https://solo2.dev
pub struct Solo2 {
    ctap: Option<ctap::Device>,
    pcsc: Option<pcsc::Device>,
    locked: Option<bool>,
    uuid: Uuid,
    version: Version,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TransportPreference {
    Ctap,
    Pcsc,
}

static PREFER_CTAP: AtomicBool = AtomicBool::new(false);

impl Solo2 {
    pub fn transport_preference() -> TransportPreference {
        if PREFER_CTAP.load(Ordering::SeqCst) {
            TransportPreference::Ctap
        } else {
            TransportPreference::Pcsc
        }
    }

    pub fn prefer_ctap() {
        PREFER_CTAP.store(true, Ordering::SeqCst);
    }

    pub fn prefer_pcsc() {
        PREFER_CTAP.store(false, Ordering::SeqCst);
    }

    /// NB: Requires user tap
    pub fn into_lpc55(self) -> Result<Lpc55> {
        let mut solo2 = self;
        let uuid = solo2.uuid;
        // AGAIN: This requires user tap!
        let now = std::time::Instant::now();
        Admin::select(&mut solo2)?.maintenance().ok();
        drop(solo2);

        std::thread::sleep(std::time::Duration::from_secs(1));
        let mut lpc55 = Lpc55::having(uuid);
        while lpc55.is_err() {
            if now.elapsed().as_secs() > 15 {
                return Err(anyhow!("User prompt to confirm maintenance timed out (or udev rules for LPC 55 mode missing)!"));
            }
            std::thread::sleep(std::time::Duration::from_secs(1));
            lpc55 = Lpc55::having(uuid);
        }

        lpc55
    }
}

impl fmt::Debug for Solo2 {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::result::Result<(), fmt::Error> {
        write!(
            f,
            "Solo 2 {:X} (CTAP: {:?}, PCSC: {:?}, Version: {} aka {})",
            &self.uuid.simple(),
            &self.ctap,
            &self.pcsc,
            &self.version.to_semver(),
            &self.version.to_calver(),
        )
    }
}

impl fmt::Display for Solo2 {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let transports = match (self.ctap.is_some(), self.pcsc.is_some()) {
            (true, true) => "CTAP+PCSC",
            (true, false) => "CTAP only",
            (false, true) => "PCSC only",
            _ => unreachable!(),
        };
        let lock_status = match self.locked {
            Some(true) => ", locked",
            Some(false) => ", unlocked",
            None => "",
        };
        write!(
            f,
            "Solo 2 {:X} ({}, firmware {}{})",
            &self.uuid.simple(),
            transports,
            &self.version().to_calver(),
            lock_status,
        )
    }
}

impl UuidSelectable for Solo2 {
    fn try_uuid(&mut self) -> Result<Uuid> {
        Ok(self.uuid)
    }

    fn list() -> Vec<Self> {
        // iterator/lifetime woes avoiding the explicit for loop
        let mut ctaps = BTreeMap::new();
        for mut device in ctap::list().into_iter() {
            if let Ok(uuid) = device.try_uuid() {
                ctaps.insert(uuid, device);
            }
        }
        // iterator/lifetime woes avoiding the explicit for loop
        let mut pcscs = BTreeMap::new();
        for mut device in pcsc::list().into_iter() {
            if let Ok(uuid) = device.try_uuid() {
                pcscs.insert(uuid, device);
            }
        }

        let uuids = BTreeSet::from_iter(ctaps.keys().chain(pcscs.keys()).copied());
        let mut devices = Vec::new();
        for uuid in uuids.iter() {
            // a bit roundabout, but hey, "it works".
            let mut device = Self {
                ctap: ctaps.remove(uuid),
                pcsc: pcscs.remove(uuid),
                locked: None,
                uuid: *uuid,
                version: Version {
                    major: 0,
                    minor: 0,
                    patch: 0,
                },
            };
            if let Ok(mut admin) = Admin::select(&mut device) {
                if let Ok(locked) = admin.locked() {
                    device.locked = Some(locked);
                }
            }
            if let Ok(mut admin) = Admin::select(&mut device) {
                if let Ok(version) = admin.version() {
                    device.version = version;
                    devices.push(device);
                }
            }
        }
        devices
    }
}

impl Solo2 {
    /// UUID of device.
    pub fn uuid(&self) -> Uuid {
        self.uuid
    }

    /// Firmware version on device.
    pub fn version(&self) -> Version {
        self.version
    }

    pub fn as_ctap(&self) -> Option<&ctap::Device> {
        self.ctap.as_ref()
    }

    pub fn as_ctap_mut(&mut self) -> Option<&mut ctap::Device> {
        self.ctap.as_mut()
    }

    pub fn as_pcsc(&self) -> Option<&pcsc::Device> {
        self.pcsc.as_ref()
    }

    pub fn as_pcsc_mut(&mut self) -> Option<&mut pcsc::Device> {
        self.pcsc.as_mut()
    }
}

impl TryFrom<ctap::Device> for Solo2 {
    type Error = crate::Error;
    fn try_from(device: ctap::Device) -> Result<Solo2> {
        let mut device = device;
        let locked = Admin::select(&mut device)?.locked().ok();
        let uuid = device.try_uuid()?;
        let version = Admin::select(&mut device)?.version()?;

        Ok(Solo2 {
            ctap: Some(device),
            pcsc: None,
            locked,
            uuid,
            version,
        })
    }
}

impl TryFrom<pcsc::Device> for Solo2 {
    type Error = crate::Error;
    fn try_from(device: pcsc::Device) -> Result<Solo2> {
        let mut device = device;
        let mut admin = Admin::select(&mut device)?;
        let uuid = admin.uuid()?;
        let version = admin.version()?;
        Ok(Solo2 {
            ctap: None,
            pcsc: Some(device),
            locked: None,
            uuid,
            version,
        })
    }
}

/// A SoloKeys Solo 2 device, which may be in regular ([Solo2]) or update ([Lpc55]) mode.
///
/// Not every [pcsc::Device] is a [Device]; currently if it reacts to the SoloKeys administrative
/// [App][crate::apps::admin::App] with a valid UUID, then we treat it as such.
// #[derive(Debug, Eq, PartialEq)]
pub enum Device {
    Lpc55(Lpc55),
    Solo2(Solo2),
}

impl fmt::Display for Device {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use Device::*;
        match self {
            Lpc55(lpc55) => write!(f, "LPC 55 {:X}", Uuid::from_u128(lpc55.uuid).simple()),
            Solo2(solo2) => solo2.fmt(f),
        }
    }
}

impl UuidSelectable for Device {
    fn try_uuid(&mut self) -> Result<Uuid> {
        Ok(self.uuid())
    }

    fn list() -> Vec<Self> {
        let lpc55s = Lpc55::list().into_iter().map(Device::from);
        let solo2s = Solo2::list().into_iter().map(Device::from);
        lpc55s.chain(solo2s).collect()
    }

    /// Fails is if zero or >1 devices have the given UUID.
    fn having(uuid: Uuid) -> Result<Self> {
        let mut candidates: Vec<Device> = Self::list()
            .into_iter()
            .filter(|card| card.uuid() == uuid)
            .collect();
        match candidates.len() {
            0 => Err(anyhow!("No usable device has UUID {:X}", uuid.simple())),
            1 => Ok(candidates.remove(0)),
            n => Err(anyhow!(
                "Multiple ({}) devices have UUID {:X}",
                n,
                uuid.simple()
            )),
        }
    }
}

impl Device {
    fn uuid(&self) -> Uuid {
        match self {
            Device::Lpc55(lpc55) => Uuid::from_u128(lpc55.uuid),
            Device::Solo2(solo2) => solo2.uuid(),
        }
    }

    /// NB: will hang if in bootloader mode and Solo 2 firmware does not
    /// come up cleanly.
    pub fn into_solo2(self) -> Result<Solo2> {
        match self {
            Device::Solo2(solo2) => Ok(solo2),
            Device::Lpc55(lpc55) => {
                let uuid = Uuid::from_u128(lpc55.uuid);
                lpc55.reboot();
                drop(lpc55);

                std::thread::sleep(std::time::Duration::from_secs(1));
                let mut solo2 = Solo2::having(uuid);
                while solo2.is_err() {
                    std::thread::sleep(std::time::Duration::from_secs(1));
                    solo2 = Solo2::having(uuid);
                }

                solo2
            }
        }
    }

    /// NB: Requires user tap if device is in Solo2 mode.
    pub fn into_lpc55(self) -> Result<Lpc55> {
        match self {
            Device::Lpc55(lpc55) => Ok(lpc55),
            Device::Solo2(solo2) => solo2.into_lpc55(),
        }
    }

    pub fn program(
        self,
        firmware: Firmware,
        skip_major_prompt: bool,
        progress: Option<&dyn Fn(usize)>,
    ) -> Result<()> {
        // If device is in Solo2 mode
        // - if firmware is major version bump, confirm with dialogue
        // - prompt user tap and get into bootloader
        // let device_version: Version = admin.version()?;
        // let new_version = firmware.version();

        let lpc55 = match self {
            Device::Lpc55(lpc55) => lpc55,
            Device::Solo2(solo2) => {
                // If device is in Solo2 mode
                // - if firmware is major version bump, confirm with dialogue
                // - prompt user tap and get into Lpc55 bootloader

                info!("device fw version: {}", solo2.version.to_calver());
                info!("new fw version: {}", firmware.version().to_calver());

                if solo2.version > firmware.version() {
                    println!("Firmware version on device higher than firmware version used.");
                    println!("This would be rejected by the device.");
                    return Err(anyhow!("Firmware rollback attempt"));
                }

                let fw_major = firmware.version().major;
                let major_version_bump = fw_major > solo2.version.major;
                if !skip_major_prompt && major_version_bump {
                    use dialoguer::{theme, Confirm};
                    println!("Warning: This is is major update and it could risk breaking any current credentials on your key.");
                    println!("Check latest release notes here to double check: https://github.com/solokeys/solo2/releases");
                    println!(
                        "If you haven't used your key for anything yet, you can ignore this.\n"
                    );

                    if Confirm::with_theme(&theme::ColorfulTheme::default())
                        .with_prompt("Continue?")
                        .wait_for_newline(true)
                        .interact()?
                    {
                        println!("Continuing");
                    } else {
                        return Err(anyhow!("User aborted."));
                    }
                }

                println!("Tap button on key to confirm, or replug to abort...");
                Self::Solo2(solo2).into_lpc55()
                    .map_err(|e| {
                        if std::env::consts::OS == "linux" {
                            println!("\nIf you touched the key and the LED is off, you are likely missing udev rules for LPC 55 mode.");
                            println!("Either run `sudo solo2 update`, or install <https://github.com/solokeys/solo2-cli/blob/main/70-solo2.rules>");
                            println!("Specifically, you need this line:");
                            // SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="b000", TAG+="uaccess"
                            println!(r#"SUBSYSTEM=="hidraw", ATTRS{{idVendor}}=="1209", ATTRS{{idProduct}}=="b000", TAG+="uaccess"#);
                            println!();
                        }
                        e
                    })?
            }
        };

        println!("LPC55 Bootloader detected. The LED should be off.");
        println!("Writing new firmware...");
        firmware.write_to(&lpc55, progress);

        println!("Done. Rebooting key. The LED should turn back on.");
        Self::Lpc55(lpc55).into_solo2().map(drop)
    }
}

impl From<Lpc55> for Device {
    fn from(lpc55: Lpc55) -> Device {
        Device::Lpc55(lpc55)
    }
}

impl From<Solo2> for Device {
    fn from(solo2: Solo2) -> Device {
        Device::Solo2(solo2)
    }
}