Skip to content

Commit 6037dd5

Browse files
Add Citrix CTX1 (#76)
* Add Citrix CTX1 Adds a Citrix CTX1 decoder. * Fix tests Add simple sanity check.
1 parent e07ee50 commit 6037dd5

3 files changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
use crate::checkers::CheckerTypes;
2+
use crate::decoders::interface::check_string_success;
3+
4+
use super::crack_results::CrackResult;
5+
use super::interface::Crack;
6+
use super::interface::Decoder;
7+
8+
use log::{debug, info, trace};
9+
10+
///! Citrix CTX1 Decoder
11+
pub struct CitrixCTX1Decoder;
12+
13+
impl Crack for Decoder<CitrixCTX1Decoder> {
14+
fn new() -> Decoder<CitrixCTX1Decoder> {
15+
Decoder {
16+
name: "citrix_ctx1",
17+
description: "Citrix CTX1 is a very old encoding that was used for encoding Citrix passwords.",
18+
link: "https://www.remkoweijnen.nl/blog/2012/05/13/encoding-and-decoding-citrix-passwords/",
19+
tags: vec!["citrix_ctx1", "citrix", "passwords", "decoder"],
20+
expected_runtime: 0.01,
21+
expected_success: 1.0,
22+
failure_runtime: 0.01,
23+
normalised_entropy: vec![1.0, 10.0],
24+
popularity: 0.1,
25+
phantom: std::marker::PhantomData,
26+
}
27+
}
28+
29+
/// This function does the actual decoding
30+
/// It returns an Option<string> if it was successful
31+
/// Else the Option returns nothing and the error is logged in Trace
32+
fn crack(&self, text: &str, checker: &CheckerTypes) -> CrackResult {
33+
trace!("Trying citrix_ctx1 with text {:?}", text);
34+
let decoded_text: Option<String> = decode_citrix_ctx1(text);
35+
36+
trace!("Decoded text for citrix_ctx1: {:?}", decoded_text);
37+
let mut results = CrackResult::new(self, text.to_string());
38+
39+
if decoded_text.is_none() {
40+
debug!("Failed to decode citrix_ctx1 because the length is not a multiple of 4");
41+
return results;
42+
}
43+
44+
let decoded_text = decoded_text.unwrap();
45+
if !check_string_success(&decoded_text, text) {
46+
info!(
47+
"Failed to decode citrix_ctx1 because check_string_success returned false on string {}",
48+
decoded_text
49+
);
50+
return results;
51+
}
52+
53+
let checker_result = checker.check(&decoded_text);
54+
results.unencrypted_text = Some(decoded_text);
55+
56+
results.update_checker(&checker_result);
57+
58+
results
59+
}
60+
}
61+
62+
/// Decodes Citrix CTX1
63+
fn decode_citrix_ctx1(text: &str) -> Option<String> {
64+
if text.len() % 4 != 0 {
65+
return None;
66+
}
67+
68+
if text.to_uppercase() != text || !text.chars().all(|c| c.is_ascii()) {
69+
return None;
70+
}
71+
72+
let mut rev = text.as_bytes().to_vec();
73+
rev.reverse();
74+
let mut result = Vec::new();
75+
let mut temp;
76+
77+
for i in (0..rev.len()).step_by(2) {
78+
if i + 2 >= rev.len() {
79+
temp = 0;
80+
} else {
81+
temp = ((rev[i + 2] - 0x41) & 0xF) ^ (((rev[i + 3] - 0x41) << 4) & 0xF0);
82+
}
83+
temp ^= (((rev[i] - 0x41) & 0xF) ^ (((rev[i + 1] - 0x41) << 4) & 0xF0)) ^ 0xA5;
84+
result.push(temp);
85+
}
86+
87+
result.retain(|&x| x != 0);
88+
result.reverse();
89+
90+
String::from_utf8(result).ok()
91+
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use super::CitrixCTX1Decoder;
96+
use crate::{
97+
checkers::{
98+
athena::Athena,
99+
checker_type::{Check, Checker},
100+
CheckerTypes,
101+
},
102+
decoders::interface::{Crack, Decoder},
103+
};
104+
105+
// helper for tests
106+
fn get_athena_checker() -> CheckerTypes {
107+
let athena_checker = Checker::<Athena>::new();
108+
CheckerTypes::CheckAthena(athena_checker)
109+
}
110+
111+
#[test]
112+
fn test_citrix_ctx1() {
113+
let decoder = Decoder::<CitrixCTX1Decoder>::new();
114+
let result = decoder.crack(
115+
"MNGIKIANMEGBKIANMHGCOHECJADFPPFKINCIOBEEIFCA",
116+
&get_athena_checker(),
117+
);
118+
assert_eq!(result.unencrypted_text.unwrap(), "hello world");
119+
}
120+
#[test]
121+
fn citrix_ctx1_decode_empty_string() {
122+
// Citrix_ctx1 returns an empty string, this is a valid citrix_ctx1 string
123+
// but returns False on check_string_success
124+
let citrix_ctx1_decoder = Decoder::<CitrixCTX1Decoder>::new();
125+
let result = citrix_ctx1_decoder
126+
.crack("", &get_athena_checker())
127+
.unencrypted_text;
128+
assert!(result.is_none());
129+
}
130+
131+
#[test]
132+
fn citrix_ctx1_decode_handles_panics() {
133+
let citrix_ctx1_decoder = Decoder::<CitrixCTX1Decoder>::new();
134+
let result = citrix_ctx1_decoder
135+
.crack(
136+
"hello my name is panicky mc panic face!",
137+
&get_athena_checker(),
138+
)
139+
.unencrypted_text;
140+
if result.is_some() {
141+
panic!("Decode_citrix_ctx1 did not return an option with Some<t>.")
142+
} else {
143+
// If we get here, the test passed
144+
// Because the citrix_ctx1_decoder.crack function returned None
145+
// as it should do for the input
146+
assert_eq!(true, true);
147+
}
148+
}
149+
150+
#[test]
151+
fn citrix_ctx1_handle_panic_if_empty_string() {
152+
let citrix_ctx1_decoder = Decoder::<CitrixCTX1Decoder>::new();
153+
let result = citrix_ctx1_decoder
154+
.crack("", &get_athena_checker())
155+
.unencrypted_text;
156+
if result.is_some() {
157+
assert_eq!(true, true);
158+
}
159+
}
160+
161+
#[test]
162+
fn citrix_ctx1_work_if_string_not_citrix_ctx1() {
163+
// You can citrix_ctx1 decode a string that is not citrix_ctx1
164+
// This string decodes to:
165+
// ```.ée¢
166+
// (uÖ²```
167+
// https://gchq.github.io/CyberChef/#recipe=From_Base58('A-Za-z0-9%2B/%3D',true)&input=aGVsbG8gZ29vZCBkYXkh
168+
let citrix_ctx1_decoder = Decoder::<CitrixCTX1Decoder>::new();
169+
let result = citrix_ctx1_decoder
170+
.crack("hello good day!", &get_athena_checker())
171+
.unencrypted_text;
172+
if result.is_some() {
173+
assert_eq!(true, true);
174+
}
175+
}
176+
177+
#[test]
178+
fn citrix_ctx1_handle_panic_if_emoji() {
179+
let citrix_ctx1_decoder = Decoder::<CitrixCTX1Decoder>::new();
180+
let result = citrix_ctx1_decoder
181+
.crack("😂", &get_athena_checker())
182+
.unencrypted_text;
183+
if result.is_some() {
184+
assert_eq!(true, true);
185+
}
186+
}
187+
}

src/decoders/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub mod base58_flickr_decoder;
2121
/// The base64_decoder module decodes base64
2222
/// It is public as we use it in some tests.
2323
pub mod base64_decoder;
24+
/// The citrix_ctx1_decoder module decodes citrix ctx1
25+
pub mod citrix_ctx1_decoder;
2426
/// The crack_results module defines the CrackResult
2527
/// Each and every decoder return same CrackResult
2628
pub mod crack_results;

src/filtration_system/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::decoders::base58_ripple_decoder::Base58RippleDecoder;
1212
///! Given a filter object, return an array of decoders/crackers which have been filtered
1313
///
1414
use crate::decoders::base64_decoder::Base64Decoder;
15+
use crate::decoders::citrix_ctx1_decoder::CitrixCTX1Decoder;
1516
use crate::decoders::crack_results::CrackResult;
1617
use crate::decoders::interface::{Crack, Decoder};
1718
use crate::decoders::morse_code::MorseCodeDecoder;
@@ -100,6 +101,7 @@ pub fn filter_and_get_decoders() -> Decoders {
100101
let base58_ripple = Decoder::<Base58RippleDecoder>::new();
101102
let base58_flickr = Decoder::<Base58FlickrDecoder>::new();
102103
let base64 = Decoder::<Base64Decoder>::new();
104+
let citrix_ctx1 = Decoder::<CitrixCTX1Decoder>::new();
103105
let base32 = Decoder::<Base32Decoder>::new();
104106
let reversedecoder = Decoder::<ReverseDecoder>::new();
105107
let morsecodedecoder = Decoder::<MorseCodeDecoder>::new();
@@ -110,6 +112,7 @@ pub fn filter_and_get_decoders() -> Decoders {
110112
Box::new(base58_ripple),
111113
Box::new(base58_flickr),
112114
Box::new(base64),
115+
Box::new(citrix_ctx1),
113116
Box::new(base32),
114117
Box::new(reversedecoder),
115118
Box::new(morsecodedecoder),

0 commit comments

Comments
 (0)