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
//! A Rust library to interact with [RFC 8959](https://tools.ietf.org/html/rfc8959) secret-token URIs.
//!
//! See the RFC text for motivation and details.
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, NON_ALPHANUMERIC};

/// The URI scheme used.
pub const SCHEME: &'static str = "secret-token";

/// The URI scheme with colon, for convenience.
pub const PREFIX: &'static str = "secret-token:";

// The list is shamelessly borrowed from
// https://github.com/Lexicality/secret-token/blob/d3cf01d7cc5b6c44d461e0ff71f7652b3edcb574/secret_token.py
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC
    .remove(b'-')
    .remove(b'.')
    .remove(b'_')
    .remove(b'~')
    .remove(b'!')
    .remove(b'$')
    .remove(b'&')
    .remove(b'\'')
    .remove(b'(')
    .remove(b')')
    .remove(b'*')
    .remove(b'+')
    .remove(b',')
    .remove(b';')
    .remove(b'=')
    .remove(b':')
    .remove(b'@');

/// Encodes the secret into the secret-token URI.
///
/// Non-ascii characters are UTF-8-encoded, disallowed characters then are percent-encoded,
/// finally the [PREFIX](const.PREFIX) is prependend.
pub fn encode(secret: &str) -> String {
    format!(
        "{}{}",
        PREFIX,
        percent_encode(secret.as_bytes(), DISALLOWED_CHARACTERS)
    )
}

/// Decodes the secret-token URI into a secret.
///
/// This function returns `None` when `uri`:
///
/// * Does not start with the [PREFIX](const.PREFIX)
/// * Has no token
/// * Has token that contains invalid percent-encoded UTF-8
pub fn decode(uri: &str) -> Option<String> {
    match uri.strip_prefix(PREFIX) {
        Some("") => None,
        Some(rest) => match percent_decode_str(rest).decode_utf8() {
            Ok(decoded) => Some(decoded.into_owned()),
            Err(_) => None,
        },
        None => None,
    }
}

/// Returns true if the URI is valid (this means it can be decoded), false otherwise.
pub fn is_valid(uri: &str) -> bool {
    decode(uri).is_some()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn valid_pairs() -> Vec<(&'static str, &'static str)> {
        vec![
            ("secret-token:s", "s"),
            ("secret-token:hello", "hello"),
            (
                "secret-token:E92FB7EB-D882-47A4-A265-A0B6135DC842%20foo",
                "E92FB7EB-D882-47A4-A265-A0B6135DC842 foo",
            ),
            ("secret-token:%C5%81%C3%B3d%C5%BA", "Łódź"),
        ]
    }

    fn invalid_uris() -> Vec<&'static str> {
        vec![
            "",
            "s",
            "hello",
            "Łódź",
            "%C5%81%C3%B3d%C5%BA",
            "secret-token",
            //"secret-token:",
            //"secret-token:secret-token:",
            //"secret-token:secret-token:hello",
            //"secret-token:secret-token:secret-token:secret-token:",
            //"secret-token:secret-token:secret-token:secret-token:hello",
            "SECRET-TOKEN:",
            "SECRET-TOKEN:hello",
            ":secret-token",
            ":secret-token:",
            ":secret-token:hello",
            "secret-token:%a1",
        ]
    }

    #[test]
    fn test_decode_and_is_valid_work_with_valid_uris() {
        for (input, output) in valid_pairs() {
            println!("Testing {}", input);
            assert_eq!(decode(input).unwrap(), output);
            assert!(is_valid(input));
        }
    }

    #[test]
    fn test_decode_and_is_valid_with_invalid_uris() {
        for input in invalid_uris() {
            println!("Testing {}", input);
            assert!(decode(input).is_none());
            assert!(!is_valid(input));
        }
    }

    #[test]
    fn test_encode() {
        for (input, output) in valid_pairs() {
            println!("Testing {}", input);
            assert_eq!(encode(output), input);
        }
    }
}