prs_lib/crypto/
util.rs

1//! Common crypto utilities.
2
3use anyhow::Result;
4
5use super::{Config, Key, prelude::*};
6
7/// Minimum hexadecimal length for a fingerprint to be considered valid
8///
9/// GnuPG requires at least 8 hexadecimal characters. GnuPG 2.2+ still allows 8 characters, but
10/// emits a warning at least 16 characters are recommended.
11const FINGERPRINT_MIN_LEN: usize = 8;
12
13/// Format fingerprint in consistent format.
14///
15/// Trims and uppercases.
16pub fn format_fingerprint<S: AsRef<str>>(fingerprint: S) -> String {
17    normalize_fingerprint(fingerprint)
18}
19
20/// Normalize a fingerprint to some consistent format
21///
22/// Does the following in order:
23/// - removes `0x` or `0X` prefix if present
24/// - removes comment suffix if present
25/// - trims whitespace
26/// - uppercases
27///
28/// Does not normalize the length of the fingerprint
29pub fn normalize_fingerprint<S: AsRef<str>>(fingerprint: S) -> String {
30    let fp = fingerprint.as_ref().trim_start();
31
32    // Remove 0x prefix
33    let fp = if fp.starts_with("0x") || fp.starts_with("0X") {
34        &fp[2..]
35    } else {
36        fp
37    };
38
39    // Remove comment suffix
40    let fp = fp.split('#').next().unwrap();
41
42    // Trim whitespace and uppercase
43    fp.trim_end().to_uppercase()
44}
45
46/// Check whether two fingerprints match
47///
48/// Normalizes both fingerprints using [`normalize_fingerprint`].
49///
50/// Returns true if `b` is a substring of `a`.
51/// Requires at least [`FINGERPRINT_MIN_LEN`] hexadecimal characters in both.
52pub fn fingerprints_equal<S: AsRef<str>, T: AsRef<str>>(a: S, b: T) -> bool {
53    // Normalize both fingerprints
54    let a = normalize_fingerprint(a);
55    let b = normalize_fingerprint(b);
56
57    // Require at least 8 characters
58    if a.len() < FINGERPRINT_MIN_LEN || b.len() < FINGERPRINT_MIN_LEN {
59        return false;
60    }
61
62    a.contains(&b)
63}
64
65/// Check whether a list of keys contains the given fingerprint.
66pub fn keys_contain_fingerprint<S: AsRef<str>>(keys: &[Key], fingerprint: S) -> bool {
67    keys.iter()
68        .any(|key| fingerprints_equal(key.fingerprint(false), fingerprint.as_ref()))
69}
70
71/// Check whether the user has any private/secret key in their keychain.
72pub fn has_private_key(config: &Config) -> Result<bool> {
73    Ok(!super::context(config)?.keys_private()?.is_empty())
74}
75
76#[cfg(test)]
77mod tests {
78    #[rustfmt::skip]
79    const FPS_NORMALIZE: &[(&str, &str)] = &[
80        ("E2D8DE4D35CE386F",                                        "E2D8DE4D35CE386F"),
81        ("364119B9",                                                "364119B9"        ),
82        ("0xae78a4de7A738B54  ",                                    "AE78A4DE7A738B54"),
83        ("0xB079912385023787 # username <user@example.com>",        "B079912385023787"),
84        ("  0X47e3b6f9970b175f   # some comment # something else ", "47E3B6F9970B175F"),
85    ];
86    #[rustfmt::skip]
87    const FPS_EQUAL: &[(&str, &str)] = &[
88        // 8 characters is minimum
89        ("AAAAAAAA",                                                "AAAAAAAA"),
90        // Different casing
91        ("e2d8de4d35CE386f",                                        "E2D8DE4d35CE386f"),
92        // 0x prefixes
93        ("0x364119B9",                                              "0X364119B9"),
94        ("364119B9",                                                "0x364119B9"),
95        // Comments
96        ("364119B9 # comment 1",                                    "364119B9 # comment 2"),
97        // Substrings
98        ("AE78A4DE7A738B54",                                        "7A738B54"),
99        ("AE78A4DE7A738B54",                                        "AE78A4DE"),
100        ("   0xAE78A4DE7A738B54   # username <user@example.com>",   "a4de7a73"),
101    ];
102    #[rustfmt::skip]
103    const FPS_NOT_EQUAL: &[(&str, &str)] = &[
104        // Empty or too short fingerprints are never equal
105        ("",                    ""),
106        ("AAAAAAA",             "AAAAAAA"),
107        ("AAAAAAAA",            "AAAAAAA"),
108        ("AAAAAAA",             "AAAAAAAA"),
109        // First is smaller than second
110        ("364119B9",            "0364119B9"),
111        ("0xae78a4de7A738B54",  "AE78A4DE7A738B540"),
112    ];
113
114    #[test]
115    fn test_normalize_fingerprint() {
116        for &(a, b) in FPS_NORMALIZE {
117            assert_eq!(
118                super::normalize_fingerprint(a),
119                b,
120                "{a:?} should normalize to {b:?}"
121            );
122        }
123    }
124
125    #[test]
126    fn test_fingerprints_equal() {
127        for &(a, b) in FPS_NORMALIZE {
128            assert!(
129                super::fingerprints_equal(a, b),
130                "{a:?} and {b:?} should be equal",
131            );
132        }
133        for &(a, b) in FPS_EQUAL {
134            assert!(
135                super::fingerprints_equal(a, b),
136                "{a:?} and {b:?} should be equal",
137            );
138        }
139        for &(a, b) in FPS_NOT_EQUAL {
140            assert!(
141                !super::fingerprints_equal(a, b),
142                "{a:?} and {b:?} should not be equal",
143            );
144        }
145    }
146}