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
51pub struct GithubClassicPatValidator;
56
57impl ChecksumValidator for GithubClassicPatValidator {
58 fn validator_id(&self) -> &str {
59 "github-classic-pat"
60 }
61
62 fn validate(&self, credential: &str) -> ChecksumResult {
63 let payload = match credential.strip_prefix("ghp_") {
64 Some(p) => p,
65 None => return ChecksumResult::NotApplicable,
66 };
67 if payload.len() != 36 {
68 return ChecksumResult::NotApplicable;
69 }
70 if !payload.chars().all(|c| c.is_ascii_alphanumeric()) {
71 return ChecksumResult::Invalid;
72 }
73 let entropy = &payload[..30];
74 let checksum_str = &payload[30..];
75 let expected = base62_encode_u32(crc32(entropy.as_bytes()), 6);
76 if expected == checksum_str {
77 ChecksumResult::Valid
78 } else {
79 ChecksumResult::Invalid
87 }
88 }
89}
90
91pub struct GithubFineGrainedPatValidator;
95
96impl GithubFineGrainedPatValidator {
97 fn try_payload(payload: &str) -> ChecksumResult {
98 if payload.len() < 7 {
99 return ChecksumResult::Invalid;
100 }
101 let entropy = &payload[..payload.len() - 6];
102 let checksum_str = &payload[payload.len() - 6..];
103 let expected = base62_encode_u32(crc32(entropy.as_bytes()), 6);
104 if expected == checksum_str {
105 ChecksumResult::Valid
106 } else {
107 ChecksumResult::Invalid
108 }
109 }
110}
111
112impl ChecksumValidator for GithubFineGrainedPatValidator {
113 fn validator_id(&self) -> &str {
114 "github-fine-grained-pat"
115 }
116
117 fn validate(&self, credential: &str) -> ChecksumResult {
118 let Some(payload) = credential.strip_prefix("github_pat_") else {
119 return ChecksumResult::NotApplicable;
120 };
121 let parts: Vec<&str> = payload.split('_').collect();
122 if parts.len() != 2 {
123 return ChecksumResult::Invalid;
124 }
125 let (left, right) = (parts[0], parts[1]);
126 if left.len() != 22 || right.len() != 59 {
127 return ChecksumResult::Invalid;
128 }
129 if !left.chars().all(|c| c.is_ascii_alphanumeric())
130 || !right.chars().all(|c| c.is_ascii_alphanumeric())
131 {
132 return ChecksumResult::Invalid;
133 }
134
135 if Self::try_payload(payload) == ChecksumResult::Valid {
136 return ChecksumResult::Valid;
137 }
138 if Self::try_payload(right) == ChecksumResult::Valid {
139 return ChecksumResult::Valid;
140 }
141 ChecksumResult::Invalid
142 }
143}