Single-byte XOR cipher
This commit is contained in:
parent
067f29127a
commit
19036e4dec
4 changed files with 172 additions and 0 deletions
71
src/letter_frequencies.zig
Normal file
71
src/letter_frequencies.zig
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const Freq = struct { letter: u8, frequency: f32 };
|
||||||
|
|
||||||
|
// relative frequencies a-z, with lowest scaled to 1.0
|
||||||
|
// from some random website
|
||||||
|
const en = [_]f32{
|
||||||
|
43.3,
|
||||||
|
10.5,
|
||||||
|
23.1,
|
||||||
|
17.2,
|
||||||
|
56.8,
|
||||||
|
9.2,
|
||||||
|
12.5,
|
||||||
|
15.3,
|
||||||
|
38.4,
|
||||||
|
1.0,
|
||||||
|
5.6,
|
||||||
|
27.9,
|
||||||
|
15.3,
|
||||||
|
33.9,
|
||||||
|
36.5,
|
||||||
|
16.1,
|
||||||
|
1.0,
|
||||||
|
38.6,
|
||||||
|
29.2,
|
||||||
|
35.4,
|
||||||
|
18.5,
|
||||||
|
5.1,
|
||||||
|
6.5,
|
||||||
|
1.4,
|
||||||
|
9.0,
|
||||||
|
1.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn score_en(ascii: u8) f32 {
|
||||||
|
const ascii_lower = std.ascii.toLower(ascii);
|
||||||
|
const index: i32 = @as(i32, ascii_lower) - @as(i32, 'a');
|
||||||
|
|
||||||
|
if (index >= 0 and index < en.len) {
|
||||||
|
if (ascii_lower != ascii) {
|
||||||
|
// don't score uppercase chars as they shouldn't be that
|
||||||
|
// frequent, except someone screams. Retrospectively
|
||||||
|
// pulled that one out of my ass but works.
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return en[@intCast(index)] / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.ascii.isPrint(ascii)) {
|
||||||
|
// we don't know that char, but it is still printable ascii,
|
||||||
|
// so give essentially a score of 0. Pulled that one out of my
|
||||||
|
// ass too.
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.ascii.isWhitespace(ascii)) {
|
||||||
|
// we don't know that char, but it is some printable
|
||||||
|
// whitespace, so give essentially a score of 0. Same, I have
|
||||||
|
// a very roomy ass.
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -penalty(); // unscoreable
|
||||||
|
}
|
||||||
|
|
||||||
|
fn penalty() comptime_float {
|
||||||
|
var sum: comptime_float = 0.0;
|
||||||
|
inline for (en) |f| sum += f;
|
||||||
|
|
||||||
|
return sum / @as(f32, @floatFromInt(en.len));
|
||||||
|
}
|
24
src/main.zig
24
src/main.zig
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||||
const b64 = @import("base64.zig");
|
const b64 = @import("base64.zig");
|
||||||
const hex = @import("hex.zig");
|
const hex = @import("hex.zig");
|
||||||
const xor = @import("xor.zig");
|
const xor = @import("xor.zig");
|
||||||
|
const xor_crack = @import("xor_crack.zig");
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
@ -48,4 +49,27 @@ pub fn main() !void {
|
||||||
|
|
||||||
try stdout.print("{s}", .{out_hex});
|
try stdout.print("{s}", .{out_hex});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, args[1], "crack-xor")) {
|
||||||
|
const in_a = args[2];
|
||||||
|
|
||||||
|
const buf_a = try hex.decode(allocator, in_a);
|
||||||
|
defer allocator.free(buf_a);
|
||||||
|
|
||||||
|
const oracle = xor_crack.Oracle{
|
||||||
|
.decrypt = xor.xor_byte_noalloc,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = try xor_crack.single(allocator, buf_a, oracle) orelse {
|
||||||
|
try stdout.print("Did not find a solution", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
try stdout.print("Found solution key={c}, score={d}\n", .{ res[0], res[1] });
|
||||||
|
|
||||||
|
const out = try xor.xor_byte(allocator, buf_a, res[0]);
|
||||||
|
defer allocator.free(out);
|
||||||
|
|
||||||
|
try stdout.print("{s}\n", .{out});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
19
src/xor.zig
19
src/xor.zig
|
@ -12,3 +12,22 @@ pub fn xor_buffers(allocator: std.mem.Allocator, buf_a: []u8, buf_b: []u8) ![]u8
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// buf_a ^ byte_b. Caller must free result.
|
||||||
|
pub fn xor_byte(allocator: std.mem.Allocator, buf_a: []u8, byte_b: u8) ![]u8 {
|
||||||
|
var out: []u8 = try allocator.alloc(u8, buf_a.len);
|
||||||
|
|
||||||
|
for (0..buf_a.len) |i| {
|
||||||
|
out[i] = buf_a[i] ^ byte_b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// buf_a ^ byte_b. Caller must provide out buffer long enough to hold
|
||||||
|
/// result (buf_a.len == out.len).
|
||||||
|
pub fn xor_byte_noalloc(buf_a: []const u8, byte_b: u8, out: []u8) void {
|
||||||
|
for (0..buf_a.len) |i| {
|
||||||
|
out[i] = buf_a[i] ^ byte_b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
58
src/xor_crack.zig
Normal file
58
src/xor_crack.zig
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const score_en = @import("letter_frequencies.zig").score_en;
|
||||||
|
const penalty = @import("letter_frequencies.zig").penalty();
|
||||||
|
|
||||||
|
pub const Oracle = struct {
|
||||||
|
decrypt: fn (in_buf: []u8, key: u8, out_buf: []u8) void,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Crack a buffer encrypted with single-byte XOR
|
||||||
|
pub fn single(allocator: std.mem.Allocator, cipher: []u8, oracle: Oracle) !?struct { u8, f32 } {
|
||||||
|
var key_scores = std.AutoHashMap(u8, f32).init(allocator);
|
||||||
|
defer key_scores.deinit();
|
||||||
|
|
||||||
|
const out = try allocator.alloc(u8, cipher.len);
|
||||||
|
defer allocator.free(out);
|
||||||
|
|
||||||
|
for (32..127) |c| {
|
||||||
|
oracle.decrypt(cipher, @intCast(c), out);
|
||||||
|
|
||||||
|
var buf_score: f32 = 0.0;
|
||||||
|
for (out) |o| buf_score += score_en(o);
|
||||||
|
|
||||||
|
try key_scores.put(@intCast(c), buf_score);
|
||||||
|
}
|
||||||
|
|
||||||
|
var init_search_it = key_scores.iterator();
|
||||||
|
var max_key: u8 = undefined;
|
||||||
|
var max_score: f32 = undefined;
|
||||||
|
if (init_search_it.next()) |entry| {
|
||||||
|
max_key = entry.key_ptr.*;
|
||||||
|
max_score = entry.value_ptr.*;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var it = key_scores.iterator();
|
||||||
|
var is_init = true;
|
||||||
|
while (it.next()) |entry| {
|
||||||
|
if (entry.value_ptr.* == max_score and !is_init) {
|
||||||
|
std.log.warn("Found equal max scores ({c}, {d}), ({c}, {d})", .{
|
||||||
|
max_key,
|
||||||
|
max_score,
|
||||||
|
entry.key_ptr.*,
|
||||||
|
entry.value_ptr.*,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.value_ptr.* > max_score) {
|
||||||
|
max_key = entry.key_ptr.*;
|
||||||
|
max_score = entry.value_ptr.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_init = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ max_key, max_score };
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue