From db1896444d529f22fe362ae43dbe4d09550895d1 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Sat, 20 Mar 2021 00:50:27 -0700 Subject: [PATCH] feat(postgres): add support for clear and md5 passwords --- Cargo.lock | 19 ++++ sqlx-postgres/Cargo.toml | 2 + sqlx-postgres/src/connection/connect.rs | 15 +++- sqlx-postgres/src/protocol/frontend.rs | 2 + .../src/protocol/frontend/password.rs | 90 +++++++++++++++++++ 5 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 sqlx-postgres/src/protocol/frontend/password.rs diff --git a/Cargo.lock b/Cargo.lock index 2771a0fc..1395cf89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -555,6 +555,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "idna" version = "0.2.2" @@ -639,6 +645,17 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + [[package]] name = "memchr" version = "2.3.4" @@ -1132,7 +1149,9 @@ dependencies = [ "futures-executor", "futures-io", "futures-util", + "hex", "log", + "md-5", "memchr", "percent-encoding", "sqlx-core", diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index dee0d72b..f20534df 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -33,12 +33,14 @@ log = "0.4.11" either = "1.6.1" bytestring = "1.0.0" url = "2.2.0" +hex = "0.4.3" percent-encoding = "2.1.0" futures-io = { version = "0.3", optional = true } bytes = "1.0" memchr = "2.3" bitflags = "1.2" base64 = "0.13.0" +md-5 = "0.9.1" [dev-dependencies] sqlx-core = { version = "0.6.0-pre", path = "../sqlx-core", features = ["_mock"] } diff --git a/sqlx-postgres/src/connection/connect.rs b/sqlx-postgres/src/connection/connect.rs index 3b1b3810..6f817c0e 100644 --- a/sqlx-postgres/src/connection/connect.rs +++ b/sqlx-postgres/src/connection/connect.rs @@ -16,7 +16,7 @@ use sqlx_core::net::Stream as NetStream; use sqlx_core::{Error, Result, Runtime}; use crate::protocol::backend::{Authentication, BackendMessage, BackendMessageType}; -use crate::protocol::frontend::Startup; +use crate::protocol::frontend::{Password, PasswordMd5, Startup}; use crate::{PgClientError, PgConnectOptions, PgConnection}; impl PgConnection { @@ -50,12 +50,17 @@ impl PgConnection { return Ok(true); } - Authentication::Md5Password(_) => { - todo!("md5") + Authentication::Md5Password(data) => { + self.stream.write_message(&PasswordMd5 { + password: options.get_password().unwrap_or_default(), + username: options.get_username().unwrap_or_default(), + salt: data.salt, + })?; } Authentication::CleartextPassword => { - todo!("cleartext") + self.stream + .write_message(&Password(options.get_password().unwrap_or_default()))?; } Authentication::Sasl(_) => todo!("sasl"), @@ -70,6 +75,8 @@ impl PgConnection { })); } } + + Ok(false) } } diff --git a/sqlx-postgres/src/protocol/frontend.rs b/sqlx-postgres/src/protocol/frontend.rs index 96712d95..dd0cdbf1 100644 --- a/sqlx-postgres/src/protocol/frontend.rs +++ b/sqlx-postgres/src/protocol/frontend.rs @@ -1,5 +1,7 @@ +mod password; mod startup; mod terminate; +pub(crate) use password::{Password, PasswordMd5}; pub(crate) use startup::Startup; pub(crate) use terminate::Terminate; diff --git a/sqlx-postgres/src/protocol/frontend/password.rs b/sqlx-postgres/src/protocol/frontend/password.rs new file mode 100644 index 00000000..0efc847a --- /dev/null +++ b/sqlx-postgres/src/protocol/frontend/password.rs @@ -0,0 +1,90 @@ +use crate::io::PgWriteExt; +use md5::{Digest, Md5}; +use sqlx_core::io::Serialize; +use sqlx_core::Result; + +#[derive(Debug)] +pub(crate) struct Password<'a>(pub(crate) &'a str); + +#[derive(Debug)] +pub(crate) struct PasswordMd5<'a> { + pub(crate) password: &'a str, + pub(crate) username: &'a str, + pub(crate) salt: [u8; 4], +} + +impl Serialize<'_> for Password<'_> { + fn serialize_with(&self, buf: &mut Vec, _: ()) -> Result<()> { + buf.reserve(1 + 4 + self.0.len() + 1); + buf.push(b'p'); + + buf.write_len_prefixed(|buf| { + buf.extend(self.0.as_bytes()); + buf.push(b'\0'); + + Ok(()) + }) + } +} + +impl Serialize<'_> for PasswordMd5<'_> { + fn serialize_with(&self, buf: &mut Vec, _: ()) -> Result<()> { + buf.reserve(1 + 4 + 3 + 32 + 1); + buf.push(b'p'); + + buf.write_len_prefixed(|buf| { + // the actual `PasswordMessage` can be computed in SQL as: + // concat('md5', md5(concat(md5(concat(password, username)), random-salt))) + + // keep in mind the md5() function returns its result as a hex string + + let mut hasher = Md5::new(); + + hasher.update(self.password); + hasher.update(self.username); + + let offset = buf.len(); + buf.resize(offset + 32 + 3, 0); + + let hash = hasher.finalize_reset(); + let _ = hex::encode_to_slice(hash.as_slice(), &mut buf[offset..offset + 32]); + + hasher.update(&buf[offset..offset + 32]); + hasher.update(self.salt); + + buf[offset..offset + 3].copy_from_slice(&b"md5"[..]); + + let hash = hasher.finalize(); + let _ = hex::encode_to_slice(hash.as_slice(), &mut buf[offset + 3..offset + 32 + 3]); + + buf.push(b'\0'); + + Ok(()) + }) + } +} + +#[cfg(test)] +mod tests { + use super::{Password, PasswordMd5, Serialize}; + + #[test] + fn should_serialize() { + let mut buf = Vec::new(); + let m = Password("password"); + + m.serialize(&mut buf).unwrap(); + + assert_eq!(buf, b"p\0\0\0\rpassword\0"); + } + + #[test] + fn should_serialize_md5() { + let mut buf = Vec::new(); + let m = PasswordMd5 { password: "password", username: "root", salt: [147, 24, 57, 152] }; + + m.serialize(&mut buf).unwrap(); + + assert_eq!(buf, b"p\0\0\0(md53e2c9d99d49b201ef867a36f3f9ed62c\0"); + } +}