├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src ├── main.zig └── test.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | *~ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Frank Denis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # charm 2 | 3 | A tiny, self-contained cryptography library, implementing authenticated encryption and keyed hashing. 4 | 5 | Charm was especially designed for memory-constrained devices, but can also be used to add encryption support to WebAssembly modules with minimal overhead. 6 | 7 | Any number of hashing and authenticated encryption operations can be freely chained using a single rolling state. 8 | In this mode, each authentication tag authenticates the whole transcript since the beginning of the session. 9 | 10 | The [original implementation](https://github.com/jedisct1/charm) was written in C and is used by the [dsvpn](https://github.com/jedisct1/dsvpn) VPN software. 11 | 12 | This is a port to the [Zig](https://ziglang.org) language. It is fully compatible with the C version. 13 | 14 | ## Usage 15 | 16 | ### Setting up a session 17 | 18 | Charm requires a 256-bit key, and, if the key is reused for different sessions, a unique session identifier (`nonce`): 19 | 20 | ```zig 21 | var key: [Charm.key_length]u8 = undefined; 22 | std.crypto.random.bytes(&key); 23 | 24 | var charm = Charm.new(key, null); 25 | ``` 26 | 27 | ### Hashing 28 | 29 | ```zig 30 | const h = charm.hash("data"); 31 | ``` 32 | 33 | ### Authenticated encryption 34 | 35 | #### Encryption 36 | 37 | ```zig 38 | const tag = charm.encrypt(msg[0..]); 39 | ``` 40 | 41 | Encrypts `msg` in-place and returns a 128-bit authentication tag. 42 | 43 | #### Decryption 44 | 45 | Starting from the same state as the one used for encryption: 46 | 47 | ```zig 48 | try charm.decrypt(msg[0..], tag); 49 | ``` 50 | 51 | Returns `error.AuthenticationFailed` if the authentication tag is invalid for the given message and the previous transcript. 52 | 53 | ## Security guarantees 54 | 55 | 128-bit security, no practical limits on the size and length of messages. 56 | 57 | ## Other implementations: 58 | 59 | - [charm](https://github.com/jedisct1/charm) original implementation in C. 60 | - [charm.js](https://github.com/jedisct1/charm.js) a JavaScript (TypeScript) implementation. 61 | - [go-charm](https://github.com/x13a/go-charm) a Go implementation. 62 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | 6 | const optimize = b.standardOptimizeOption(.{}); 7 | 8 | _ = b.addModule("charm", .{ 9 | .root_source_file = b.path("src/main.zig"), 10 | }); 11 | 12 | const lib = b.addStaticLibrary(.{ 13 | .name = "charm", 14 | .root_source_file = b.path("src/main.zig"), 15 | .target = target, 16 | .optimize = optimize, 17 | }); 18 | 19 | b.installArtifact(lib); 20 | 21 | const main_tests = b.addTest(.{ 22 | .root_source_file = b.path("src/main.zig"), 23 | .target = target, 24 | .optimize = optimize, 25 | }); 26 | 27 | const run_main_tests = b.addRunArtifact(main_tests); 28 | 29 | const test_step = b.step("test", "Run library tests"); 30 | test_step.dependOn(&run_main_tests.step); 31 | } 32 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .charm, 3 | .version = "1.0.0", 4 | .fingerprint = 0x813d82eac977a7f9, 5 | .minimum_zig_version = "0.14.0", 6 | .dependencies = .{}, 7 | .paths = .{ 8 | "build.zig", 9 | "build.zig.zon", 10 | "src", 11 | "LICENSE", 12 | "README.md", 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const math = std.math; 4 | const mem = std.mem; 5 | 6 | const Xoodoo = struct { 7 | const rcs = [12]u32{ 0x058, 0x038, 0x3c0, 0x0d0, 0x120, 0x014, 0x060, 0x02c, 0x380, 0x0f0, 0x1a0, 0x012 }; 8 | const Lane = @Vector(4, u32); 9 | state: [3]Lane, 10 | 11 | inline fn asWords(self: *Xoodoo) *[12]u32 { 12 | return @as(*[12]u32, @ptrCast(&self.state)); 13 | } 14 | 15 | inline fn asBytes(self: *Xoodoo) *[48]u8 { 16 | return mem.asBytes(&self.state); 17 | } 18 | 19 | fn permute(self: *Xoodoo) void { 20 | const rot8x32 = comptime if (builtin.target.cpu.arch.endian() == .big) 21 | [_]i32{ 9, 10, 11, 8, 13, 14, 15, 12, 1, 2, 3, 0, 5, 6, 7, 4 } 22 | else 23 | [_]i32{ 11, 8, 9, 10, 15, 12, 13, 14, 3, 0, 1, 2, 7, 4, 5, 6 }; 24 | 25 | var a = self.state[0]; 26 | var b = self.state[1]; 27 | var c = self.state[2]; 28 | inline for (rcs) |rc| { 29 | var p = @shuffle(u32, a ^ b ^ c, undefined, [_]i32{ 3, 0, 1, 2 }); 30 | var e = math.rotl(Lane, p, 5); 31 | p = math.rotl(Lane, p, 14); 32 | e ^= p; 33 | a ^= e; 34 | b ^= e; 35 | c ^= e; 36 | b = @shuffle(u32, b, undefined, [_]i32{ 3, 0, 1, 2 }); 37 | c = math.rotl(Lane, c, 11); 38 | a[0] ^= rc; 39 | a ^= ~b & c; 40 | b ^= ~c & a; 41 | c ^= ~a & b; 42 | b = math.rotl(Lane, b, 1); 43 | c = @as(Lane, @bitCast(@shuffle(u8, @as(@Vector(16, u8), @bitCast(c)), undefined, rot8x32))); 44 | } 45 | self.state[0] = a; 46 | self.state[1] = b; 47 | self.state[2] = c; 48 | } 49 | 50 | inline fn endianSwapRate(self: *Xoodoo) void { 51 | for (self.asWords()[0..4]) |*w| { 52 | w.* = mem.littleToNative(u32, w.*); 53 | } 54 | } 55 | 56 | inline fn endianSwapAll(self: *Xoodoo) void { 57 | for (self.asWords()) |*w| { 58 | w.* = mem.littleToNative(u32, w.*); 59 | } 60 | } 61 | 62 | fn squeezePermute(self: *Xoodoo) [16]u8 { 63 | self.endianSwapRate(); 64 | const rate = self.asBytes()[0..16].*; 65 | self.endianSwapRate(); 66 | self.permute(); 67 | return rate; 68 | } 69 | }; 70 | 71 | pub const Charm = struct { 72 | x: Xoodoo, 73 | 74 | pub const tag_length = 16; 75 | pub const key_length = 32; 76 | pub const nonce_length = 16; 77 | pub const hash_length = 32; 78 | 79 | pub fn new(key: [key_length]u8, nonce: ?[nonce_length]u8) Charm { 80 | var x = Xoodoo{ .state = undefined }; 81 | var bytes = x.asBytes(); 82 | if (nonce) |n| { 83 | @memcpy(bytes[0..16], n[0..]); 84 | } else { 85 | @memset(bytes[0..16], 0); 86 | } 87 | @memcpy(bytes[16..][0..32], key[0..]); 88 | x.endianSwapAll(); 89 | x.permute(); 90 | return Charm{ .x = x }; 91 | } 92 | 93 | fn xor128(out: *[16]u8, in: *const [16]u8) void { 94 | for (out, 0..) |*x, i| { 95 | x.* ^= in[i]; 96 | } 97 | } 98 | 99 | fn equal128(a: [16]u8, b: [16]u8) bool { 100 | var d: u8 = 0; 101 | for (a, 0..) |x, i| { 102 | d |= x ^ b[i]; 103 | } 104 | mem.doNotOptimizeAway(d); 105 | return d == 0; 106 | } 107 | 108 | pub fn nonceIncrement(nonce: *[nonce_length]u8, endian: builtin.Endian) void { 109 | const next = mem.readInt(u128, nonce, endian) +% 1; 110 | mem.writeInt(u128, nonce, next, endian); 111 | } 112 | 113 | pub fn encrypt(charm: *Charm, msg: []u8) [tag_length]u8 { 114 | var squeezed: [16]u8 = undefined; 115 | var bytes = charm.x.asBytes(); 116 | var off: usize = 0; 117 | while (off + 16 < msg.len) : (off += 16) { 118 | charm.x.endianSwapRate(); 119 | @memcpy(squeezed[0..], bytes[0..16]); 120 | xor128(bytes[0..16], msg[off..][0..16]); 121 | charm.x.endianSwapRate(); 122 | xor128(msg[off..][0..16], squeezed[0..]); 123 | charm.x.permute(); 124 | } 125 | const leftover = msg.len - off; 126 | var padded = [_]u8{0} ** (16 + 1); 127 | @memcpy(padded[0..leftover], msg[off..][0..leftover]); 128 | padded[leftover] = 0x80; 129 | charm.x.endianSwapRate(); 130 | @memcpy(squeezed[0..], bytes[0..16]); 131 | xor128(bytes[0..16], padded[0..16]); 132 | charm.x.endianSwapRate(); 133 | charm.x.asWords()[11] ^= (@as(u32, 1) << 24 | @as(u32, @intCast(leftover)) >> 4 << 25 | @as(u32, 1) << 26); 134 | xor128(padded[0..16], squeezed[0..]); 135 | @memcpy(msg[off..][0..leftover], padded[0..leftover]); 136 | charm.x.permute(); 137 | return charm.x.squeezePermute(); 138 | } 139 | 140 | pub fn decrypt(charm: *Charm, msg: []u8, expected_tag: [tag_length]u8) !void { 141 | var squeezed: [16]u8 = undefined; 142 | var bytes = charm.x.asBytes(); 143 | var off: usize = 0; 144 | while (off + 16 < msg.len) : (off += 16) { 145 | charm.x.endianSwapRate(); 146 | @memcpy(squeezed[0..], bytes[0..16]); 147 | xor128(msg[off..][0..16], squeezed[0..]); 148 | xor128(bytes[0..16], msg[off..][0..16]); 149 | charm.x.endianSwapRate(); 150 | charm.x.permute(); 151 | } 152 | const leftover = msg.len - off; 153 | var padded = [_]u8{0} ** (16 + 1); 154 | @memcpy(padded[0..leftover], msg[off..][0..leftover]); 155 | charm.x.endianSwapRate(); 156 | @memset(squeezed[0..], 0); 157 | @memcpy(squeezed[0..leftover], bytes[0..leftover]); 158 | xor128(padded[0..16], squeezed[0..]); 159 | padded[leftover] = 0x80; 160 | xor128(bytes[0..16], padded[0..16]); 161 | charm.x.endianSwapRate(); 162 | charm.x.asWords()[11] ^= (@as(u32, 1) << 24 | @as(u32, @intCast(leftover)) >> 4 << 25 | @as(u32, 1) << 26); 163 | @memcpy(msg[off..][0..leftover], padded[0..leftover]); 164 | charm.x.permute(); 165 | const tag = charm.x.squeezePermute(); 166 | if (!equal128(expected_tag, tag)) { 167 | @memset(msg, 0); 168 | return error.AuthenticationFailed; 169 | } 170 | } 171 | 172 | pub fn hash(charm: *Charm, msg: []const u8) [hash_length]u8 { 173 | var bytes = charm.x.asBytes(); 174 | var off: usize = 0; 175 | while (off + 16 < msg.len) : (off += 16) { 176 | charm.x.endianSwapRate(); 177 | xor128(bytes[0..16], msg[off..][0..16]); 178 | charm.x.endianSwapRate(); 179 | charm.x.permute(); 180 | } 181 | const leftover = msg.len - off; 182 | var padded = [_]u8{0} ** (16 + 1); 183 | @memcpy(padded[0..leftover], msg[off..][0..leftover]); 184 | padded[leftover] = 0x80; 185 | charm.x.endianSwapRate(); 186 | xor128(bytes[0..16], padded[0..16]); 187 | charm.x.endianSwapRate(); 188 | charm.x.asWords()[11] ^= (@as(u32, 1) << 24 | @as(u32, @intCast(leftover)) >> 4 << 25); 189 | charm.x.permute(); 190 | var h: [hash_length]u8 = undefined; 191 | @memcpy(h[0..16], charm.x.squeezePermute()[0..]); 192 | @memcpy(h[16..32], charm.x.squeezePermute()[0..]); 193 | return h; 194 | } 195 | }; 196 | 197 | test "charm" { 198 | _ = @import("test.zig"); 199 | } 200 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const mem = std.mem; 4 | const random = std.crypto.random; 5 | const Charm = @import("main.zig").Charm; 6 | 7 | test "encrypt and hash in a session" { 8 | var key: [Charm.key_length]u8 = undefined; 9 | var nonce: [Charm.nonce_length]u8 = undefined; 10 | 11 | random.bytes(&key); 12 | random.bytes(&nonce); 13 | 14 | const msg1_0 = "message 1"; 15 | const msg2_0 = "message 2"; 16 | var msg1 = msg1_0.*; 17 | var msg2 = msg2_0.*; 18 | 19 | var charm = Charm.new(key, nonce); 20 | const tag1 = charm.encrypt(msg1[0..]); 21 | const tag2 = charm.encrypt(msg2[0..]); 22 | const h = charm.hash(msg1_0); 23 | 24 | charm = Charm.new(key, nonce); 25 | try charm.decrypt(msg1[0..], tag1); 26 | try charm.decrypt(msg2[0..], tag2); 27 | const hx = charm.hash(msg1_0); 28 | 29 | debug.assert(mem.eql(u8, msg1[0..], msg1_0[0..])); 30 | debug.assert(mem.eql(u8, msg2[0..], msg2_0[0..])); 31 | debug.assert(mem.eql(u8, h[0..], hx[0..])); 32 | } 33 | --------------------------------------------------------------------------------