ssh_key/fingerprint/
randomart.rs

1//! Support for the "drunken bishop" fingerprint algorithm, a.k.a. "randomart".
2//!
3//! The algorithm is described in the paper:
4//!
5//! "The drunken bishop: An analysis of the OpenSSH fingerprint visualization algorithm"
6//!
7//! <http://www.dirk-loss.de/sshvis/drunken_bishop.pdf>
8
9use super::Fingerprint;
10use core::fmt;
11
12const WIDTH: usize = 17;
13const HEIGHT: usize = 9;
14const VALUES: &[u8; 17] = b" .o+=*BOX@%&#/^SE";
15const NVALUES: u8 = VALUES.len() as u8 - 1;
16
17type Field = [[u8; WIDTH]; HEIGHT];
18
19/// "randomart" renderer.
20pub(super) struct Randomart<'a> {
21    header: &'a str,
22    field: Field,
23    footer: &'static str,
24}
25
26impl<'a> Randomart<'a> {
27    /// Create new "randomart" from the given fingerprint.
28    // TODO: Remove this when the pipeline toolchain is updated beyond 1.69
29    #[allow(clippy::arithmetic_side_effects)]
30    pub(super) fn new(header: &'a str, fingerprint: Fingerprint) -> Self {
31        let mut field = Field::default();
32        let mut x = WIDTH / 2;
33        let mut y = HEIGHT / 2;
34
35        for mut byte in fingerprint.as_bytes().iter().copied() {
36            for _ in 0..4 {
37                if byte & 0x1 == 0 {
38                    x = x.saturating_sub(1);
39                } else {
40                    x = x.saturating_add(1);
41                }
42
43                if byte & 0x2 == 0 {
44                    y = y.saturating_sub(1);
45                } else {
46                    y = y.saturating_add(1);
47                }
48
49                x = x.min(WIDTH.saturating_sub(1));
50                y = y.min(HEIGHT.saturating_sub(1));
51
52                if field[y][x] < NVALUES - 2 {
53                    field[y][x] = field[y][x].saturating_add(1);
54                }
55
56                byte >>= 2;
57            }
58        }
59
60        field[HEIGHT / 2][WIDTH / 2] = NVALUES - 1;
61        field[y][x] = NVALUES;
62
63        Self {
64            header,
65            field,
66            footer: fingerprint.footer(),
67        }
68    }
69}
70
71impl fmt::Display for Randomart<'_> {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        writeln!(f, "+{:-^width$}+", self.header, width = WIDTH)?;
74
75        for row in self.field {
76            write!(f, "|")?;
77
78            for c in row {
79                write!(f, "{}", VALUES[c as usize] as char)?;
80            }
81
82            writeln!(f, "|")?;
83        }
84
85        write!(f, "+{:-^width$}+", self.footer, width = WIDTH)
86    }
87}
88
89#[cfg(all(test, feature = "alloc"))]
90mod tests {
91    use super::Fingerprint;
92
93    const EXAMPLE_FINGERPRINT: &str = "SHA256:UCUiLr7Pjs9wFFJMDByLgc3NrtdU344OgUM45wZPcIQ";
94    const EXAMPLE_RANDOMART: &str = "\
95+--[ED25519 256]--+
96|o+oO==+ o..      |
97|.o++Eo+o..       |
98|. +.oO.o . .     |
99| . o..B.. . .    |
100|  ...+ .S. o     |
101|  .o. . . . .    |
102|  o..    o       |
103|   B      .      |
104|  .o*            |
105+----[SHA256]-----+";
106
107    #[test]
108    fn generation() {
109        let fingerprint = EXAMPLE_FINGERPRINT.parse::<Fingerprint>().unwrap();
110        let randomart = fingerprint.to_randomart("[ED25519 256]");
111        assert_eq!(EXAMPLE_RANDOMART, randomart);
112    }
113}