keyhog_scanner/checksum/
github.rs1use super::{ChecksumResult, ChecksumValidator};
2
3const BASE62_DIGITS: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
4
5pub(super) fn crc32(data: &[u8]) -> u32 {
7 const TABLE: [u32; 256] = {
8 let mut table = [0u32; 256];
9 let mut i = 0;
10 while i < 256 {
11 let mut crc = i as u32;
12 let mut j = 0;
13 while j < 8 {
14 if crc & 1 != 0 {
15 crc = 0xEDB88320 ^ (crc >> 1);
16 } else {
17 crc >>= 1;
18 }
19 j += 1;
20 }
21 table[i] = crc;
22 i += 1;
23 }
24 table
25 };
26
27 let mut crc: u32 = 0xFFFF_FFFF;
28 for &byte in data {
29 crc = TABLE[((crc ^ (byte as u32)) & 0xFF) as usize] ^ (crc >> 8);
30 }
31 crc ^ 0xFFFF_FFFF
32}
33
34pub(super) fn base62_encode_u32(mut value: u32, width: usize) -> String {
36 if value == 0 {
37 return "0".repeat(width);
38 }
39 let mut rev = Vec::with_capacity(width.max(6));
40 while value > 0 {
41 rev.push(BASE62_DIGITS[(value % 62) as usize] as char);
42 value /= 62;
43 }
44 while rev.len() < width {
45 rev.push('0');
46 }
47 rev.reverse();
48 rev.into_iter().collect()
49}
50
51#[allow(dead_code)]
53pub(super) fn base62_decode(s: &str) -> Option<u32> {
54 let mut value: u32 = 0;
55 for ch in s.chars() {
56 let digit = BASE62_DIGITS.iter().position(|&d| d == ch as u8)? as u32;
57 value = value.checked_mul(62)?.checked_add(digit)?;
58 }
59 Some(value)
60}
61
62pub struct GithubClassicPatValidator;
67
68impl ChecksumValidator for GithubClassicPatValidator {
69 fn validator_id(&self) -> &str {
70 "github-classic-pat"
71 }
72
73 fn validate(&self, credential: &str) -> ChecksumResult {
74 let payload = match credential.strip_prefix("ghp_") {
75 Some(p) => p,
76 None => return ChecksumResult::NotApplicable,
77 };
78 if payload.len() != 36 {
79 return ChecksumResult::NotApplicable;
80 }
81 if !payload.chars().all(|c| c.is_ascii_alphanumeric()) {
82 return ChecksumResult::Invalid;
83 }
84 let entropy = &payload[..30];
85 let checksum_str = &payload[30..];
86 let expected = base62_encode_u32(crc32(entropy.as_bytes()), 6);
87 if expected == checksum_str {
88 ChecksumResult::Valid
89 } else {
90 ChecksumResult::Invalid
91 }
92 }
93}
94
95pub struct GithubFineGrainedPatValidator;
99
100impl GithubFineGrainedPatValidator {
101 fn try_payload(payload: &str) -> ChecksumResult {
102 if payload.len() < 7 {
103 return ChecksumResult::Invalid;
104 }
105 let entropy = &payload[..payload.len() - 6];
106 let checksum_str = &payload[payload.len() - 6..];
107 let expected = base62_encode_u32(crc32(entropy.as_bytes()), 6);
108 if expected == checksum_str {
109 ChecksumResult::Valid
110 } else {
111 ChecksumResult::Invalid
112 }
113 }
114}
115
116impl ChecksumValidator for GithubFineGrainedPatValidator {
117 fn validator_id(&self) -> &str {
118 "github-fine-grained-pat"
119 }
120
121 fn validate(&self, credential: &str) -> ChecksumResult {
122 let Some(payload) = credential.strip_prefix("github_pat_") else {
123 return ChecksumResult::NotApplicable;
124 };
125 let parts: Vec<&str> = payload.split('_').collect();
126 if parts.len() != 2 {
127 return ChecksumResult::Invalid;
128 }
129 let (left, right) = (parts[0], parts[1]);
130 if left.len() != 22 || right.len() != 59 {
131 return ChecksumResult::Invalid;
132 }
133 if !left.chars().all(|c| c.is_ascii_alphanumeric())
134 || !right.chars().all(|c| c.is_ascii_alphanumeric())
135 {
136 return ChecksumResult::Invalid;
137 }
138
139 if Self::try_payload(payload) == ChecksumResult::Valid {
140 return ChecksumResult::Valid;
141 }
142 if Self::try_payload(right) == ChecksumResult::Valid {
143 return ChecksumResult::Valid;
144 }
145 ChecksumResult::Invalid
146 }
147}