diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 42df30e..fb65467 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -14,18 +14,13 @@ jobs: steps: - name: run mysql server run: | - # TODO: use password eventually - # docker run --name some-mysql --env MYSQL_ROOT_PASSWORD=password -p 3306:3306 -d mysql - docker run --name some-mysql --env MYSQL_ALLOW_EMPTY_PASSWORD=1 -p 3306:3306 -d mysql + docker run --name some-mysql --env MYSQL_ROOT_PASSWORD=password -p 3306:3306 -d mysql - uses: actions/checkout@v3 - name: install zig run: | - # curl -L https://ziglang.org/download/ > page.xml - # ZIG_VERSION=$(cat page.xml | tidy -html 2> /dev/null | grep zig-linux-x86_64 | head -n 1 | cut -d '-' -f 4,5 | cut -d '.' -f 1,2,3,4) - ZIG_VERSION=0.12.0-dev.1642+5f8641401 - echo "zig version: $ZIG_VERSION" + ZIG_VERSION=0.12.0-dev.1647+325e0f5f0 wget https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz tar xf zig-linux-x86_64-$ZIG_VERSION.tar.xz mv zig-linux-x86_64-$ZIG_VERSION $HOME/zig-build diff --git a/README.md b/README.md index be1e32c..e3d6396 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,22 @@ - mysql client in zig ## Status -- Not ready +- MVP ## Features -- [x] Connection with no password - [x] Ping - [x] Query Text Protocol - [x] Prepared Statement +- [x] Password Authentication -## Example +## Examples - Coming soon! -## Task Pool -- [ ] `caching_sha2_password` full authentication -- [ ] TLS +## Tasks +- [ ] TLS support - [ ] Query Text Protocol Input Parameters -- [ ] Execute Result: Support Data Type -- [ ] Prepared Statement Parameters +- [ ] Prepared Statement Input Parameters +- [ ] Execute Result: Support More Data Type ## Unit Tests - `zig test src/myzql.zig` diff --git a/integration_tests/config.zig b/integration_tests/config.zig index e5d1223..54acae5 100644 --- a/integration_tests/config.zig +++ b/integration_tests/config.zig @@ -2,5 +2,5 @@ const Config = @import("../src/config.zig").Config; // TODO: use a config with password pub const test_config: Config = .{ - // .password = "password", + .password = "password", }; diff --git a/src/auth.zig b/src/auth.zig index 230dfb5..1535351 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -1,5 +1,7 @@ const std = @import("std"); const FixedBytes = @import("./utils.zig").FixedBytes; +const PublicKey = std.crypto.Certificate.rsa.PublicKey; +const Sha1 = std.crypto.hash.Sha1; const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n"); @@ -170,3 +172,95 @@ test "scrambleSHA256Password" { try std.testing.expectEqual(t.expected, actual); } } + +// https://mariadb.com/kb/en/sha256_password-plugin/#rsa-encrypted-password +// RSA encrypted value of XOR(password, seed) using server public key (RSA_PKCS1_OAEP_PADDING). +pub fn encryptPassword(allocator: std.mem.Allocator, password: []const u8, auth_data: *const [20]u8, pk: *const PublicKey) ![]const u8 { + var plain = blk: { + var plain = try allocator.alloc(u8, password.len + 1); + @memcpy(plain.ptr, password); + plain[plain.len - 1] = 0; + break :blk plain; + }; + defer allocator.free(plain); + + for (plain, 0..) |*c, i| { + c.* ^= auth_data[i % 20]; + } + + return rsaEncryptOAEP(allocator, plain, pk); +} + +fn rsaEncryptOAEP(allocator: std.mem.Allocator, msg: []const u8, pk: *const PublicKey) ![]const u8 { + const init_hash = Sha1.init(.{}); + + const lHash = blk: { + var hash = init_hash; + hash.update(&.{}); + break :blk hash.finalResult(); + }; + const digest_len = lHash.len; + + const k = (pk.n.bits() + 7) / 8; // modulus size in bytes + + var em = try allocator.alloc(u8, k); + defer allocator.free(em); + @memset(em, 0); + var seed = em[1 .. 1 + digest_len]; + var db = em[1 + digest_len ..]; + + @memcpy(db[0..lHash.len], &lHash); + db[db.len - msg.len - 1] = 1; + @memcpy(db[db.len - msg.len ..], msg); + std.crypto.random.bytes(seed); + + mgf1XOR(db, &init_hash, seed); + mgf1XOR(seed, &init_hash, db); + + return encryptMsg(allocator, em, pk); +} + +fn encryptMsg(allocator: std.mem.Allocator, msg: []const u8, pk: *const PublicKey) ![]const u8 { + // can remove this if it's publicly exposed in std.crypto.Certificate.rsa + // for now, just copy it from std.crypto.ff + const max_modulus_bits = 4096; + const Modulus = std.crypto.ff.Modulus(max_modulus_bits); + const Fe = Modulus.Fe; + + const m = try Fe.fromBytes(pk.*.n, msg, .big); + const e = try pk.n.powPublic(m, pk.e); + + var res = try allocator.alloc(u8, msg.len); + try e.toBytes(res, .big); + return res; +} + +// mgf1XOR XORs the bytes in out with a mask generated using the MGF1 function +// specified in PKCS #1 v2.1. +fn mgf1XOR(dest: []u8, init_hash: *const Sha1, seed: []const u8) void { + var counter: [4]u8 = .{ 0, 0, 0, 0 }; + var digest: [Sha1.digest_length]u8 = undefined; + + var done: usize = 0; + while (done < dest.len) : (incCounter(&counter)) { + var hash = init_hash.*; + hash.update(seed); + hash.update(counter[0..4]); + digest = hash.finalResult(); + + for (&digest) |*d| { + if (done >= dest.len) break; + dest[done] ^= d.*; + done += 1; + } + } +} + +// incCounter increments a four byte, big-endian counter. +fn incCounter(c: *[4]u8) void { + inline for (&.{ 3, 2, 1, 0 }) |i| { + const res = @addWithOverflow(c[i], 1); + c[i] = res[0]; + if (res[1] == 0) return; // no overflow, so we're done + } +} diff --git a/src/conn.zig b/src/conn.zig index e0e01cc..9cfa34f 100644 --- a/src/conn.zig +++ b/src/conn.zig @@ -201,7 +201,7 @@ pub const Conn = struct { switch (auth_plugin) { .caching_sha2_password => { switch (more_data[0]) { - auth.caching_sha2_password_fast_auth_success => return, // success (no more action needed) + auth.caching_sha2_password_fast_auth_success => {}, // success (do nothing, wait for next packet) auth.caching_sha2_password_full_authentication_start => { // Full Authentication start @@ -215,22 +215,13 @@ pub const Conn = struct { defer pk_packet.deinit(allocator); // Decode public key - const pub_key = try auth.decodePublicKey(pk_packet.payload, allocator); - defer pub_key.deinit(allocator); + const decoded_pk = try auth.decodePublicKey(pk_packet.payload, allocator); + defer decoded_pk.deinit(allocator); - // Encrypt password with public key - // TODO - const auth_resp = try generate_auth_response(.sha256_password, &auth_data, config.password); - try conn.sendBytesAsPacket(auth_resp.get()); - - const resp_packet = try conn.readPacket(allocator); - defer resp_packet.deinit(allocator); - - switch (resp_packet.payload[0]) { - constants.OK => _ = OkPacket.initFromPacket(&resp_packet, conn.client_capabilities), - constants.ERR => return ErrorPacket.initFromPacket(false, &resp_packet, conn.client_capabilities).asError(), - else => return resp_packet.asError(conn.client_capabilities), - } + // Encrypt password with public key and send it to server + const encrypted_pw = try auth.encryptPassword(allocator, config.password, &auth_data, &decoded_pk.value); + defer allocator.free(encrypted_pw); + try conn.sendBytesAsPacket(encrypted_pw); }, else => return error.UnsupportedCachingSha2PasswordMoreData, }