Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sha2 auth #3

Merged
merged 7 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions .github/workflows/integrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion integration_tests/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
94 changes: 94 additions & 0 deletions src/auth.zig
Original file line number Diff line number Diff line change
@@ -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");

Expand Down Expand Up @@ -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
}
}
23 changes: 7 additions & 16 deletions src/conn.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
}
Expand Down