Updating common annotated security key generation and validation in Rust (#57)
* adding an end-to-end test * adding constants for long form and standard * adding validation * fixing failing test * adding long-form support * fixing generation * adding support for long-form, normalizing rust and c# common key generation and validation * fixes and cleanups * adding common key generator test * adding validation test * adding test * adding test * adding test
This commit is contained in:
Родитель
c4ab2fcf34
Коммит
e92d236f5d
|
@ -83,12 +83,6 @@ public static class IdentifiableSecrets
|
|||
|
||||
bool longForm = key.Length == LongFormCommonAnnotatedKeySize;
|
||||
|
||||
string signature = key.Substring(CommonAnnotatedKey.ProviderFixedSignatureOffset, CommonAnnotatedKey.ProviderFixedSignatureLength);
|
||||
|
||||
string alternate = char.IsUpper(signature[0])
|
||||
? signature.ToLowerInvariant()
|
||||
: signature.ToUpperInvariant();
|
||||
|
||||
ulong checksumSeed = VersionTwoChecksumSeed;
|
||||
|
||||
string componentToChecksum = key.Substring(0, CommonAnnotatedKey.ChecksumOffset);
|
||||
|
@ -238,12 +232,17 @@ public static class IdentifiableSecrets
|
|||
throw new InvalidOperationException($"The key length is less than 86 characters: {key}");
|
||||
}
|
||||
|
||||
key = key.Substring(0, 86);
|
||||
key = $"{key}==";
|
||||
key = key.Substring(0, 85);
|
||||
|
||||
// We use Q== as the suffix to keep the key format consistent with the usage in Rust.
|
||||
// In C#, the base64 decoder can handle illegal base64 strings, but not in Rust.
|
||||
key = $"{key}Q==";
|
||||
}
|
||||
else
|
||||
{
|
||||
key = $"{new string(testChar!.Value, 86)}==";
|
||||
// We use Q== as the suffix to keep the key format consistent with the usage in Rust.
|
||||
// In C#, the base64 decoder can handle illegal base64 strings, but not in Rust.
|
||||
key = $"{new string(testChar!.Value, 85)}Q==";
|
||||
}
|
||||
|
||||
try
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'microsoft_security_utilities'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=microsoft_security_utilities"
|
||||
],
|
||||
"filter": {
|
||||
"name": "microsoft_security_utilities",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
#[cfg(test)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generate_and_detect_common_annotated_key_test() {
|
||||
let options = microsoft_security_utilities_core::identifiable_scans::ScanOptions::default();
|
||||
let mut scan = microsoft_security_utilities_core::identifiable_scans::Scan::new(options);
|
||||
|
||||
let input = microsoft_security_utilities_core::identifiable_secrets::generate_common_annotated_test_key(
|
||||
microsoft_security_utilities_core::identifiable_secrets::VERSION_TWO_CHECKSUM_SEED.clone(),
|
||||
"TEST",
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
Some('a')
|
||||
);
|
||||
|
||||
let generated_input = input.clone().unwrap();
|
||||
let input_as_bytes = generated_input.as_bytes();
|
||||
scan.parse_bytes(input_as_bytes);
|
||||
|
||||
let check = scan.possible_matches().first().unwrap();
|
||||
let scan_match = check.matches_bytes(input_as_bytes, true);
|
||||
assert!(scan_match.is_some(), "identifiable_scan: at least one match found");
|
||||
|
||||
let scan_match = scan_match.unwrap();
|
||||
assert_eq!(generated_input, scan_match.text(), "identifiable_scan: matched string equals generated input");
|
||||
|
||||
let validation_result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(
|
||||
&generated_input,
|
||||
"TEST"
|
||||
);
|
||||
|
||||
assert_eq!(validation_result, true, "checksum validation of generated input passes");
|
||||
}
|
|
@ -3,14 +3,165 @@
|
|||
|
||||
#[cfg(test)]
|
||||
use super::*;
|
||||
use std::{collections::HashSet, hash::Hash};
|
||||
use base64::alphabet;
|
||||
use std::{collections::HashSet};
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use uuid::Uuid;
|
||||
|
||||
static S_BASE62_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
||||
|
||||
#[test]
|
||||
fn identifiable_secrets_try_validate_common_annotated_key_generate_common_annotated_key_long_form() {
|
||||
for &long_form in &[true, false] {
|
||||
let valid_signature = "ABCD";
|
||||
let valid_key = microsoft_security_utilities_core::identifiable_secrets::
|
||||
generate_common_annotated_key(
|
||||
valid_signature,
|
||||
true,
|
||||
Some(&vec![0; 9]),
|
||||
Some(&vec![0; 3]),
|
||||
long_form.clone(),
|
||||
None
|
||||
);
|
||||
|
||||
let valid_key = valid_key.unwrap().clone();
|
||||
let valid_key_len = valid_key.len();
|
||||
|
||||
let result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(
|
||||
&valid_key,
|
||||
valid_signature,
|
||||
);
|
||||
assert!(result, "a generated key should validate");
|
||||
|
||||
let expected_length = if long_form {
|
||||
microsoft_security_utilities_core::identifiable_secrets::LONG_FORM_COMMON_ANNOTATED_KEY_SIZE
|
||||
} else {
|
||||
microsoft_security_utilities_core::identifiable_secrets::STANDARD_COMMON_ANNOTATED_KEY_SIZE
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
valid_key_len,
|
||||
expected_length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifiable_secrets_try_validate_common_annotated_key_reject_null_empty_and_whitespace_arguments() {
|
||||
let valid_signature = "ABCD";
|
||||
let valid_key = microsoft_security_utilities_core::identifiable_secrets::
|
||||
generate_common_annotated_key(
|
||||
valid_signature,
|
||||
true,
|
||||
Some(&vec![0; 9]),
|
||||
Some(&vec![0; 3]),
|
||||
true,
|
||||
None
|
||||
);
|
||||
|
||||
let valid_key = valid_key.unwrap().clone();
|
||||
|
||||
let result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(&valid_key, valid_signature);
|
||||
assert!(result, "a generated key should validate");
|
||||
|
||||
let args = vec![String::new(), String::from(" ")];
|
||||
for arg in args {
|
||||
let result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(&arg, valid_signature);
|
||||
assert!(!result, "{}", format!("the key {} is not a valid argument", arg));
|
||||
|
||||
let result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(&valid_key, &arg);
|
||||
assert!(!result, "{}", format!("the signature '{}' is not a valid argument", arg.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifiable_secrets_try_validate_common_annotated_key_reject_invalid_signatures() {
|
||||
let valid_signature = "ABCD";
|
||||
let valid_key = microsoft_security_utilities_core::identifiable_secrets::
|
||||
generate_common_annotated_key(
|
||||
valid_signature,
|
||||
true,
|
||||
Some(&vec![0; 9]),
|
||||
Some(&vec![0; 3]),
|
||||
true,
|
||||
None
|
||||
);
|
||||
|
||||
let valid_key = valid_key.unwrap().clone();
|
||||
|
||||
let result = microsoft_security_utilities_core::identifiable_secrets::
|
||||
try_validate_common_annotated_key(&valid_key, valid_signature);
|
||||
assert!(result, "a generated key should validate");
|
||||
|
||||
let signatures = vec!["Z", "YY", "XXX", "WWWWW", "1AAA"];
|
||||
|
||||
for signature in signatures {
|
||||
for &long_form in &[true, false] {
|
||||
let action =
|
||||
microsoft_security_utilities_core::identifiable_secrets::
|
||||
generate_common_annotated_key(
|
||||
signature,
|
||||
true,
|
||||
Some(&vec![0; 9]),
|
||||
Some(&vec![0; 3]),
|
||||
long_form,
|
||||
None
|
||||
);
|
||||
|
||||
|
||||
assert!(action.is_err(), "{}", format!("the signature '{}' is not valid", signature));
|
||||
|
||||
let result = microsoft_security_utilities_core::identifiable_secrets::
|
||||
try_validate_common_annotated_key(&valid_key, signature);
|
||||
assert!(!result, "{}", format!("'{}' is not a valid signature argument", signature));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifiable_secrets_try_validate_common_annotated_key_reject_invalid_key() {
|
||||
let valid_signature = "Z123";
|
||||
let valid_key = microsoft_security_utilities_core::identifiable_secrets::
|
||||
generate_common_annotated_key(
|
||||
valid_signature,
|
||||
true,
|
||||
Some(&vec![0; 9]),
|
||||
Some(&vec![0; 3]),
|
||||
true,
|
||||
None
|
||||
);
|
||||
|
||||
let valid_key = valid_key.unwrap().clone();
|
||||
|
||||
let result = microsoft_security_utilities_core::identifiable_secrets::
|
||||
try_validate_common_annotated_key(&valid_key, valid_signature);
|
||||
assert!(result, "a generated key should validate");
|
||||
|
||||
let result = microsoft_security_utilities_core::identifiable_secrets::
|
||||
try_validate_common_annotated_key(
|
||||
&format!("{}a", valid_key),
|
||||
valid_signature,
|
||||
);
|
||||
assert!(!result, "a key with an invalid length should not validate");
|
||||
|
||||
let result = microsoft_security_utilities_core::identifiable_secrets::
|
||||
try_validate_common_annotated_key(
|
||||
&valid_key[1..],
|
||||
valid_signature,
|
||||
);
|
||||
assert!(!result, "a key with an invalid length should not validate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifiable_secrets_validate_common_annotated_key_signature() {
|
||||
for invalid_signature in ["AbAA", "aaaB", "1AAA"] {
|
||||
assert!(matches!(
|
||||
microsoft_security_utilities_core::identifiable_secrets::validate_common_annotated_key_signature(invalid_signature),
|
||||
Err(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identifiable_secrets_compute_checksum_seed_enforces_length_requirement()
|
||||
{
|
||||
|
@ -42,7 +193,7 @@ fn identifiable_secrets_platform_annotated_security_keys() {
|
|||
{
|
||||
for k in 0..iterations
|
||||
{
|
||||
let mut signature = format!("{:?}", Uuid::new_v4().simple()).chars().skip(1).take(4).collect::<String>();
|
||||
let mut signature: String = format!("{:?}", Uuid::new_v4().simple()).chars().skip(1).take(4).collect::<String>();
|
||||
|
||||
signature = format!("{}{}", alphabet.chars().nth(((keys_generated as i32) % (alphabet.len() as i32)) as usize).unwrap().to_string(), &signature[1..]);
|
||||
|
||||
|
@ -86,15 +237,24 @@ fn identifiable_secrets_platform_annotated_security_keys() {
|
|||
let provider_reserved_vec = provider_reserved.to_vec();
|
||||
|
||||
for &customer_managed in &[true, false] {
|
||||
let key = microsoft_security_utilities_core::identifiable_secrets::generate_common_annotated_key(&signature, customer_managed, Some(&platform_reserved_vec), Some(&provider_reserved_vec), None).unwrap();
|
||||
for &long_form in &[true, false] {
|
||||
let mut cased_signature = signature.clone();
|
||||
if customer_managed {
|
||||
cased_signature = cased_signature.to_uppercase();
|
||||
} else {
|
||||
cased_signature = cased_signature.to_lowercase();
|
||||
}
|
||||
|
||||
let mut result = microsoft_security_utilities_core::identifiable_secrets::COMMON_ANNOTATED_KEY_REGEX.is_match(key.as_str());
|
||||
assert!(result, "the key '{}' should match the common annotated key regex", key);
|
||||
let key = microsoft_security_utilities_core::identifiable_secrets::generate_common_annotated_key(&cased_signature, customer_managed, Some(&platform_reserved_vec), Some(&provider_reserved_vec), long_form, None).unwrap();
|
||||
|
||||
result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(key.as_str(), &signature);
|
||||
assert!(result, "the key '{}' should comprise an HIS v2-conformant pattern", key);
|
||||
let mut result = microsoft_security_utilities_core::identifiable_secrets::COMMON_ANNOTATED_KEY_REGEX.is_match(key.as_str());
|
||||
assert!(result, "the key '{}' should match the common annotated key regex", key);
|
||||
|
||||
keys_generated += 1;
|
||||
result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(key.as_str(), &cased_signature);
|
||||
assert!(result, "the key '{}' should comprise an HIS v2-conformant pattern", key);
|
||||
|
||||
keys_generated += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
pub mod microsoft_security_utilities_core;
|
||||
|
||||
mod end_to_end_tests;
|
||||
mod marvin_tests;
|
||||
mod identifiable_secrets_tests;
|
||||
mod cross_company_correlating_id_tests;
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use lazy_static::lazy_static;
|
||||
use sha2::{Sha256, Digest};
|
||||
use std::fmt;
|
||||
use std::cell::RefCell;
|
||||
|
|
|
@ -9,6 +9,7 @@ use lazy_static::lazy_static;
|
|||
use std::{mem};
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
use rand::RngCore;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use regex::Regex;
|
||||
use substring::Substring;
|
||||
|
@ -25,17 +26,54 @@ lazy_static! {
|
|||
|
||||
pub static MAXIMUM_GENERATED_KEY_SIZE: u32 = 4096;
|
||||
pub static MINIMUM_GENERATED_KEY_SIZE: u32 = 24;
|
||||
pub static STANDARD_COMMON_ANNOTATED_KEY_SIZE: usize = 84;
|
||||
pub static LONG_FORM_COMMON_ANNOTATED_KEY_SIZE: usize = 88;
|
||||
pub static COMMON_ANNOTATED_KEY_SIGNATURE: &str = "JQQJ99";
|
||||
pub static COMMON_ANNOTATED_DERIVED_KEY_SIGNATURE: &str = "JQQJ9D";
|
||||
static BITS_IN_BYTES: i32 = 8;
|
||||
static BITS_IN_BASE64_CHARACTER: i32 = 6;
|
||||
static SIZE_OF_CHECKSUM_IN_BYTES: i32 = mem::size_of::<u32>() as i32;
|
||||
|
||||
static COMMON_ANNOTATED_KEY_SIZE_IN_BYTES: usize = 63;
|
||||
|
||||
pub fn is_base62_encoding_char(ch: char) -> bool
|
||||
{
|
||||
return (ch >= 'a' && ch <= 'z') ||
|
||||
(ch >= 'A' && ch <= 'Z') ||
|
||||
(ch >= '0' && ch <= '9');
|
||||
/// The offset to the encoded standard fixed signature ('JQQJ99' or 'JQQJ9D').
|
||||
pub static STANDARD_FIXED_SIGNATURE_OFFSET: usize = 52;
|
||||
|
||||
/// The encoded length of the standard fixed signature ('JQQJ99' or 'JQQJ9D').
|
||||
pub static STANDARD_FIXED_SIGNATURE_LENGTH: usize = 6;
|
||||
|
||||
/// The offset to the encoded character that denotes a derived ('D')
|
||||
/// or standard ('9') common annotated security key.
|
||||
pub static DERIVED_KEY_CHARACTER_OFFSET: usize = STANDARD_FIXED_SIGNATURE_OFFSET + STANDARD_FIXED_SIGNATURE_LENGTH - 1;
|
||||
|
||||
/// The offset to the two-character encoded key creation date.
|
||||
pub static DATE_OFFSET: usize = STANDARD_FIXED_SIGNATURE_OFFSET + STANDARD_FIXED_SIGNATURE_LENGTH;
|
||||
|
||||
/// The encoded length of the creation date (a value such as 'AE').
|
||||
pub static DATE_LENGTH: usize = 2;
|
||||
|
||||
/// The offset to the 12-character encoded platform-reserved data.
|
||||
pub static PLATFORM_RESERVED_OFFSET: usize = DATE_OFFSET + DATE_LENGTH;
|
||||
|
||||
/// The encoded length of the platform-reserved bytes.
|
||||
pub static PLATFORM_RESERVED_LENGTH: usize = 12;
|
||||
|
||||
/// The offset to the 4-character encoded provider-reserved data.
|
||||
pub static PROVIDER_RESERVED_OFFSET: usize = PLATFORM_RESERVED_OFFSET + PLATFORM_RESERVED_LENGTH;
|
||||
|
||||
/// The encoded length of the provider-reserved bytes.
|
||||
pub static PROVIDER_RESERVED_LENGTH: usize = 4;
|
||||
|
||||
/// The offset to the 4-character encoded provider fixed signature.
|
||||
pub static PROVIDER_FIXED_SIGNATURE_OFFSET: usize = PROVIDER_RESERVED_OFFSET + PROVIDER_RESERVED_LENGTH;
|
||||
|
||||
/// The encoded length of the provider fixed signature, e.g., 'AZEG'.
|
||||
pub static PROVIDER_FIXED_SIGNATURE_LENGTH: usize = 4;
|
||||
|
||||
pub static CHECKSUM_OFFSET: usize = PROVIDER_FIXED_SIGNATURE_OFFSET + PROVIDER_FIXED_SIGNATURE_LENGTH;
|
||||
|
||||
pub fn is_base62_encoding_char(ch: char) -> bool {
|
||||
ch.is_ascii_alphanumeric()
|
||||
}
|
||||
|
||||
pub fn is_base64_encoding_char(ch: char) -> bool
|
||||
|
@ -52,6 +90,46 @@ pub fn is_base64_url_encoding_char(ch: char) -> bool
|
|||
ch == '_';
|
||||
}
|
||||
|
||||
pub fn try_validate_common_annotated_key(key: &str, base64_encoded_signature: &str) -> bool {
|
||||
if key.is_empty() || key.trim().is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match validate_common_annotated_key_signature(base64_encoded_signature) {
|
||||
Ok(_) => (),
|
||||
Err(s) => {
|
||||
println!("{}", s);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if key.len() != STANDARD_COMMON_ANNOTATED_KEY_SIZE && key.len() != LONG_FORM_COMMON_ANNOTATED_KEY_SIZE {
|
||||
return false;
|
||||
}
|
||||
|
||||
let long_form = key.len() == LONG_FORM_COMMON_ANNOTATED_KEY_SIZE;
|
||||
|
||||
let checksum_seed = VERSION_TWO_CHECKSUM_SEED.clone();
|
||||
|
||||
let component_to_checksum = &key[..CHECKSUM_OFFSET];
|
||||
let checksum_text = &key[CHECKSUM_OFFSET..];
|
||||
|
||||
let key_bytes = general_purpose::STANDARD.decode(component_to_checksum).unwrap();
|
||||
|
||||
let checksum = marvin::compute_hash32(&key_bytes, checksum_seed, 0, key_bytes.len() as i32);
|
||||
|
||||
let checksum_bytes = checksum.to_ne_bytes();
|
||||
|
||||
// A long-form has a full 4-byte checksum, while a standard form has only 3.
|
||||
let encoded = general_purpose::STANDARD.encode(if long_form {
|
||||
&checksum_bytes[..4]
|
||||
} else {
|
||||
&checksum_bytes[..3]
|
||||
});
|
||||
|
||||
encoded == checksum_text
|
||||
}
|
||||
|
||||
/// Generate a u64 an HIS v1 compliant checksum seed from a string literal
|
||||
/// that is 8 characters long and ends with at least one digit, e.g., 'ReadKey0', 'RWSeed00',
|
||||
/// etc. The checksum seed is used to initialize the Marvin32 algorithm to watermark a
|
||||
|
@ -80,37 +158,18 @@ pub fn compute_his_v1_checksum_seed(versioned_key_kind: &str) -> u64 {
|
|||
result
|
||||
}
|
||||
|
||||
pub fn try_validate_common_annotated_key(key: &str, base64_encoded_signature: &str) -> bool {
|
||||
let checksum_seed: u64 = VERSION_TWO_CHECKSUM_SEED.clone();
|
||||
|
||||
let key_bytes = general_purpose::STANDARD.decode(&key).unwrap();
|
||||
|
||||
assert_eq!(key_bytes.len(), COMMON_ANNOTATED_KEY_SIZE_IN_BYTES);
|
||||
|
||||
let key_bytes_length = key_bytes.len();
|
||||
|
||||
let bytes_for_checksum = &key_bytes[..key_bytes_length - 3];
|
||||
let actual_checksum_bytes = &key_bytes[key_bytes_length - 3..key_bytes_length];
|
||||
|
||||
let computed_marvin = marvin::compute_hash32(bytes_for_checksum, checksum_seed, 0, bytes_for_checksum.len() as i32);
|
||||
let computed_marvin_bytes = computed_marvin.to_ne_bytes();
|
||||
|
||||
// The HIS v2 standard requires a match for the first 3-bytes (24 bits) of the Marvin checksum.
|
||||
actual_checksum_bytes[0] == computed_marvin_bytes[0]
|
||||
&& actual_checksum_bytes[1] == computed_marvin_bytes[1]
|
||||
&& actual_checksum_bytes[2] == computed_marvin_bytes[2]
|
||||
}
|
||||
|
||||
pub fn generate_common_annotated_key(base64_encoded_signature: &str,
|
||||
customer_managed_key: bool,
|
||||
platform_reserved: Option<&[u8]>,
|
||||
provider_reserved: Option<&[u8]>,
|
||||
long_form: bool,
|
||||
test_char: Option<char>) -> Result<String, String> {
|
||||
generate_common_annotated_test_key(VERSION_TWO_CHECKSUM_SEED.clone(),
|
||||
base64_encoded_signature,
|
||||
customer_managed_key,
|
||||
platform_reserved,
|
||||
provider_reserved,
|
||||
long_form,
|
||||
test_char)
|
||||
}
|
||||
|
||||
|
@ -120,11 +179,17 @@ pub fn generate_common_annotated_test_key(
|
|||
customer_managed_key: bool,
|
||||
platform_reserved: Option<&[u8]>,
|
||||
provider_reserved: Option<&[u8]>,
|
||||
long_form: bool,
|
||||
test_char: Option<char>,
|
||||
) -> Result<String, String> {
|
||||
const PLATFORM_RESERVED_LENGTH: usize = 9;
|
||||
const PROVIDER_RESERVED_LENGTH: usize = 3;
|
||||
|
||||
match validate_common_annotated_key_signature(base64_encoded_signature) {
|
||||
Ok(_) => base64_encoded_signature,
|
||||
Err(s) => return Err(format!("Common Annotated Key generation failed due to: {}", s)),
|
||||
};
|
||||
|
||||
let platform_reserved = match platform_reserved {
|
||||
Some(reserved) if reserved.len() != PLATFORM_RESERVED_LENGTH => {
|
||||
return Err(format!(
|
||||
|
@ -153,21 +218,33 @@ pub fn generate_common_annotated_test_key(
|
|||
base64_encoded_signature.to_lowercase()
|
||||
};
|
||||
|
||||
let mut key = String::new();
|
||||
let mut key: String;
|
||||
|
||||
loop {
|
||||
let key_length_in_bytes = 66;
|
||||
let mut key_bytes = vec![0; key_length_in_bytes];
|
||||
|
||||
if let Some(test_char) = test_char {
|
||||
key = format!("{:86}==", test_char.to_string().repeat(86));
|
||||
} else {
|
||||
key = format!("{:85}Q==", test_char.to_string().repeat(85));
|
||||
}
|
||||
else {
|
||||
let mut rng = rand::thread_rng();
|
||||
rng.fill_bytes(&mut key_bytes);
|
||||
rng.try_fill_bytes(&mut key_bytes).expect("Failed to generate random bytes.");
|
||||
|
||||
key = base_62::encode(&key_bytes);
|
||||
|
||||
if key.len() < 86 {
|
||||
return Err(format!("The key length is less than 86 characters: {}", key));
|
||||
}
|
||||
|
||||
key = key.substring(0, 85).to_string();
|
||||
key = format!("{}Q==", key);
|
||||
}
|
||||
|
||||
let key_string = general_purpose::STANDARD.encode(&key_bytes);
|
||||
key_bytes = general_purpose::STANDARD.decode(&key_string).unwrap();
|
||||
key_bytes = match general_purpose::STANDARD.decode(&key) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => return Err(format!("Key could not be decoded: {}.", key)),
|
||||
};
|
||||
|
||||
let j_bits = b'J' - b'A';
|
||||
let q_bits = b'Q' - b'A';
|
||||
|
@ -177,61 +254,66 @@ pub fn generate_common_annotated_test_key(
|
|||
|
||||
let key_bytes_length = key_bytes.len();
|
||||
|
||||
key_bytes[key_bytes_length - 27] = reserved_bytes[2];
|
||||
key_bytes[key_bytes_length - 26] = reserved_bytes[1];
|
||||
key_bytes[key_bytes_length - 25] = reserved_bytes[0];
|
||||
key_bytes[key_bytes_length - 25] = reserved_bytes[2];
|
||||
key_bytes[key_bytes_length - 24] = reserved_bytes[1];
|
||||
key_bytes[key_bytes_length - 23] = reserved_bytes[0];
|
||||
|
||||
// Simplistic timestamp computation.
|
||||
let years_since_2024 = (chrono::Utc::now().year() - 2024) as u8;
|
||||
let zero_indexed_month = (chrono::Utc::now().month() - 1) as u8;
|
||||
|
||||
let metadata: i32 = (61 << 18) | (61 << 12) | (years_since_2024 << 6) as i32 | zero_indexed_month as i32;
|
||||
let metadata_bytes = metadata.to_ne_bytes();
|
||||
|
||||
key_bytes[key_bytes_length - 24] = metadata_bytes[2];
|
||||
key_bytes[key_bytes_length - 23] = metadata_bytes[1];
|
||||
key_bytes[key_bytes_length - 22] = metadata_bytes[0];
|
||||
key_bytes[key_bytes_length - 22] = metadata_bytes[2];
|
||||
key_bytes[key_bytes_length - 21] = metadata_bytes[1];
|
||||
key_bytes[key_bytes_length - 20] = metadata_bytes[0];
|
||||
|
||||
key_bytes[key_bytes_length - 21] = platform_reserved[0];
|
||||
key_bytes[key_bytes_length - 20] = platform_reserved[1];
|
||||
key_bytes[key_bytes_length - 19] = platform_reserved[2];
|
||||
key_bytes[key_bytes_length - 18] = platform_reserved[3];
|
||||
key_bytes[key_bytes_length - 17] = platform_reserved[4];
|
||||
key_bytes[key_bytes_length - 16] = platform_reserved[5];
|
||||
key_bytes[key_bytes_length - 15] = platform_reserved[6];
|
||||
key_bytes[key_bytes_length - 14] = platform_reserved[7];
|
||||
key_bytes[key_bytes_length - 13] = platform_reserved[8];
|
||||
key_bytes[key_bytes_length - 19] = platform_reserved[0];
|
||||
key_bytes[key_bytes_length - 18] = platform_reserved[1];
|
||||
key_bytes[key_bytes_length - 17] = platform_reserved[2];
|
||||
key_bytes[key_bytes_length - 16] = platform_reserved[3];
|
||||
key_bytes[key_bytes_length - 15] = platform_reserved[4];
|
||||
key_bytes[key_bytes_length - 14] = platform_reserved[5];
|
||||
key_bytes[key_bytes_length - 13] = platform_reserved[6];
|
||||
key_bytes[key_bytes_length - 12] = platform_reserved[7];
|
||||
key_bytes[key_bytes_length - 11] = platform_reserved[8];
|
||||
|
||||
key_bytes[key_bytes_length - 12] = provider_reserved[0];
|
||||
key_bytes[key_bytes_length - 11] = provider_reserved[1];
|
||||
key_bytes[key_bytes_length - 10] = provider_reserved[2];
|
||||
key_bytes[key_bytes_length - 10] = provider_reserved[0];
|
||||
key_bytes[key_bytes_length - 9] = provider_reserved[1];
|
||||
key_bytes[key_bytes_length - 8] = provider_reserved[2];
|
||||
|
||||
let signature_offset = key_bytes_length - 9;
|
||||
let signature_offset = key_bytes_length - 7;
|
||||
let sig_bytes = general_purpose::STANDARD.decode(&base64_encoded_signature).unwrap();
|
||||
|
||||
for i in 0..sig_bytes.len() {
|
||||
key_bytes[signature_offset + i] = sig_bytes[i];
|
||||
}
|
||||
|
||||
let checksum = marvin::compute_hash32(&key_bytes, checksum_seed, 0, (key_bytes_length - 6) as i32);
|
||||
let checksum = marvin::compute_hash32(&key_bytes, checksum_seed, 0, (key_bytes_length - 4) as i32);
|
||||
|
||||
let checksum_bytes = checksum.to_ne_bytes();
|
||||
|
||||
key_bytes[key_bytes_length - 6] = checksum_bytes[0];
|
||||
key_bytes[key_bytes_length - 5] = checksum_bytes[1];
|
||||
key_bytes[key_bytes_length - 4] = checksum_bytes[2];
|
||||
key_bytes[key_bytes_length - 3] = checksum_bytes[3];
|
||||
key_bytes[key_bytes_length - 4] = checksum_bytes[0];
|
||||
key_bytes[key_bytes_length - 3] = checksum_bytes[1];
|
||||
key_bytes[key_bytes_length - 2] = checksum_bytes[2];
|
||||
key_bytes[key_bytes_length - 1] = checksum_bytes[3];
|
||||
|
||||
key = general_purpose::STANDARD.encode(&key_bytes[..COMMON_ANNOTATED_KEY_SIZE_IN_BYTES]);
|
||||
key = general_purpose::STANDARD.encode(&key_bytes);
|
||||
|
||||
// The HIS v2 standard requires that there be no special characters in the generated key.
|
||||
if !key.contains('+') && !key.contains('/') {
|
||||
break;
|
||||
if !long_form {
|
||||
key = key.substring(0, key.len() - 4).to_string();
|
||||
}
|
||||
return Ok(key);
|
||||
} else if test_char.is_some() {
|
||||
// We could not produce a valid test key given the current signature,
|
||||
// checksum seed, reserved bits and specified test character.
|
||||
key = String::new();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
|
@ -474,6 +556,40 @@ fn generate_base64_key_helper(checksum_seed: u64,
|
|||
encode_for_url);
|
||||
}
|
||||
|
||||
pub fn validate_common_annotated_key_signature(base64_encoded_signature: &str) -> Result<String, String> {
|
||||
const REQUIRED_ENCODED_SIGNATURE_LENGTH: usize = 4;
|
||||
|
||||
if base64_encoded_signature.len() != REQUIRED_ENCODED_SIGNATURE_LENGTH {
|
||||
return Err(format!("Base64-encoded signature {} must be 4 characters long.",
|
||||
base64_encoded_signature));
|
||||
}
|
||||
|
||||
if base64_encoded_signature.chars().next().unwrap().is_digit(10) {
|
||||
return Err(format!("The first character of the signature {} must not be a digit.",
|
||||
base64_encoded_signature));
|
||||
}
|
||||
|
||||
for ch in base64_encoded_signature.chars() {
|
||||
if !is_base62_encoding_char(ch) {
|
||||
return Err(format!("Signature {} can only contain alphabetic or numeric values.",
|
||||
base64_encoded_signature));
|
||||
}
|
||||
}
|
||||
|
||||
let all_upper = base64_encoded_signature.to_uppercase();
|
||||
if base64_encoded_signature == all_upper {
|
||||
return Ok(format!("Valid signature {}", base64_encoded_signature));
|
||||
}
|
||||
|
||||
let all_lower = base64_encoded_signature.to_lowercase();
|
||||
if base64_encoded_signature == all_lower {
|
||||
return Ok(format!("Valid signature {}", base64_encoded_signature));
|
||||
}
|
||||
|
||||
return Err(format!("Signature {} characters must all upper- or all lower-case.",
|
||||
base64_encoded_signature));
|
||||
}
|
||||
|
||||
fn validate_base64_encoded_signature(base64_encoded_signature: &String, encode_for_url: bool)
|
||||
{
|
||||
let required_encoded_signature_length = 4;
|
||||
|
@ -514,7 +630,7 @@ fn get_csrandom_bytes(key_length_in_bytes: u32) -> Vec<u8>
|
|||
let mut rng = ChaCha20Rng::from_entropy();
|
||||
let mut random_bytes: Vec<u8> = Vec::new();
|
||||
|
||||
for i in 0..key_length_in_bytes
|
||||
for _i in 0..key_length_in_bytes
|
||||
{
|
||||
random_bytes.push(rng.gen::<u8>());
|
||||
}
|
||||
|
|
|
@ -68,13 +68,13 @@ pub fn compute_hash(data: &[u8], seed: u64, offset: i32, mut length: i32) -> i64
|
|||
1 => p0 = p0.wrapping_add(0x8000 | (data[remaining_data_offset as usize] as u32)),
|
||||
2 => {
|
||||
let d1 = (data[(remaining_data_offset as usize) + 1] as u32) << 8;
|
||||
let d0: u32 = data[(remaining_data_offset as usize)] as u32;
|
||||
let d0: u32 = data[remaining_data_offset as usize] as u32;
|
||||
p0 = p0.wrapping_add(0x800000 | d1 | d0);
|
||||
},
|
||||
3 => {
|
||||
let d2 = (data[(remaining_data_offset as usize) + 2] as u32) << 16;
|
||||
let d1 = (data[(remaining_data_offset as usize) + 1] as u32) << 8;
|
||||
let d0: u32 = data[(remaining_data_offset as usize)] as u32;
|
||||
let d0: u32 = data[remaining_data_offset as usize] as u32;
|
||||
p0 = p0.wrapping_add(0x80000000 | d2 | d1 | d0);
|
||||
},
|
||||
_ => panic!("Hash computation reached an invalid state")
|
||||
|
|
Загрузка…
Ссылка в новой задаче