Skip to main content

oracledb_protocol/thin/
auth.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4
5pub fn append_auth_phase_one(
6    out: &mut Vec<u8>,
7    user: &str,
8    program: &str,
9    machine: &str,
10    osuser: &str,
11    terminal: &str,
12    pid: u32,
13) -> Result<()> {
14    let mut writer = TtcWriter::new();
15    writer.write_function_code(TNS_FUNC_AUTH_PHASE_ONE);
16    write_auth_header(&mut writer, user, TNS_AUTH_MODE_LOGON, 5)?;
17    write_key_value(&mut writer, "AUTH_TERMINAL", terminal, 0)?;
18    write_key_value(&mut writer, "AUTH_PROGRAM_NM", program, 0)?;
19    write_key_value(&mut writer, "AUTH_MACHINE", machine, 0)?;
20    write_key_value(&mut writer, "AUTH_PID", &pid.to_string(), 0)?;
21    write_key_value(&mut writer, "AUTH_SID", osuser, 0)?;
22    out.extend_from_slice(&writer.into_bytes());
23    Ok(())
24}
25
26/// Appends the auth message for **token authentication** (OCI IAM database
27/// token / OAuth2) to the fast-auth bundle. Unlike password auth there is no
28/// verifier challenge: the reference sends auth phase TWO directly, carrying the
29/// token in `AUTH_TOKEN` with no `AUTH_SESSKEY`/`AUTH_PASSWORD` and auth mode
30/// `LOGON` (no `WITH_PASSWORD`); it never resends (messages/auth.pyx
31/// `_set_params`/`_write_message`, messages/fast_auth.pyx). Because this message
32/// lives inside the fast-auth bundle (ttc field version 19.1), the function code
33/// carries no `ub8` token-num — exactly like [`append_auth_phase_one`].
34pub fn append_auth_phase_two_token(
35    out: &mut Vec<u8>,
36    user: &str,
37    token: &str,
38    driver_name: &str,
39    version_num: u32,
40    connect_string: &str,
41) -> Result<()> {
42    let mut writer = TtcWriter::new();
43    writer.write_function_code(TNS_FUNC_AUTH_PHASE_TWO);
44    // AUTH_TOKEN + the four mandatory session pairs, plus AUTH_CONNECT_STRING.
45    let mut num_pairs = 5u32;
46    if !connect_string.is_empty() {
47        num_pairs += 1;
48    }
49    write_auth_header(&mut writer, user, TNS_AUTH_MODE_LOGON, num_pairs)?;
50    write_key_value(&mut writer, "AUTH_TOKEN", token, 0)?;
51    write_key_value(&mut writer, "SESSION_CLIENT_CHARSET", "873", 0)?;
52    write_key_value(&mut writer, "SESSION_CLIENT_DRIVER_NAME", driver_name, 0)?;
53    write_key_value(
54        &mut writer,
55        "SESSION_CLIENT_VERSION",
56        &version_num.to_string(),
57        0,
58    )?;
59    write_key_value(
60        &mut writer,
61        "AUTH_ALTER_SESSION",
62        "ALTER SESSION SET TIME_ZONE='+00:00'\0",
63        1,
64    )?;
65    if !connect_string.is_empty() {
66        write_key_value(&mut writer, "AUTH_CONNECT_STRING", connect_string, 0)?;
67    }
68    out.extend_from_slice(&writer.into_bytes());
69    Ok(())
70}
71
72pub fn build_auth_phase_two_payload(
73    user: &str,
74    encrypted: &crate::crypto::EncryptedPassword,
75    driver_name: &str,
76    version_num: u32,
77    connect_string: &str,
78) -> Result<Vec<u8>> {
79    build_auth_phase_two_payload_with_seq(
80        user,
81        encrypted,
82        driver_name,
83        version_num,
84        connect_string,
85        1,
86    )
87}
88
89pub fn build_auth_phase_two_payload_with_seq(
90    user: &str,
91    encrypted: &crate::crypto::EncryptedPassword,
92    driver_name: &str,
93    version_num: u32,
94    connect_string: &str,
95    seq_num: u8,
96) -> Result<Vec<u8>> {
97    build_auth_phase_two_payload_with_context_with_seq(
98        user,
99        encrypted,
100        driver_name,
101        version_num,
102        connect_string,
103        seq_num,
104        &[],
105    )
106}
107
108pub fn build_auth_phase_two_payload_with_context_with_seq(
109    user: &str,
110    encrypted: &crate::crypto::EncryptedPassword,
111    driver_name: &str,
112    version_num: u32,
113    connect_string: &str,
114    seq_num: u8,
115    app_context: &[(String, String, String)],
116) -> Result<Vec<u8>> {
117    build_auth_phase_two_payload_with_proxy_with_seq(
118        user,
119        encrypted,
120        driver_name,
121        version_num,
122        connect_string,
123        seq_num,
124        app_context,
125        None,
126        None,
127    )
128}
129
130/// Phase-two auth payload with optional proxy authentication: the reference
131/// writes `PROXY_CLIENT_NAME` as the first key/value pair when the connect
132/// user is of the form `user[proxy_user]` (messages/auth.pyx).
133#[allow(clippy::too_many_arguments)]
134pub fn build_auth_phase_two_payload_with_proxy_with_seq(
135    user: &str,
136    encrypted: &crate::crypto::EncryptedPassword,
137    driver_name: &str,
138    version_num: u32,
139    connect_string: &str,
140    seq_num: u8,
141    app_context: &[(String, String, String)],
142    proxy_user: Option<&str>,
143    edition: Option<&str>,
144) -> Result<Vec<u8>> {
145    let mut writer = TtcWriter::new();
146    writer.write_function_code_with_seq(TNS_FUNC_AUTH_PHASE_TWO, seq_num);
147    writer.write_ub8(0);
148    let mut num_pairs = 6u32;
149    if encrypted.speedy_key.is_some() {
150        num_pairs += 1;
151    }
152    if proxy_user.is_some() {
153        num_pairs += 1;
154    }
155    if !connect_string.is_empty() {
156        num_pairs += 1;
157    }
158    if edition.is_some() {
159        num_pairs += 1;
160    }
161    let app_context_pairs =
162        app_context
163            .len()
164            .checked_mul(3)
165            .ok_or(ProtocolError::InvalidPacketLength {
166                length: app_context.len(),
167                minimum: 0,
168            })?;
169    num_pairs +=
170        u32::try_from(app_context_pairs).map_err(|_| ProtocolError::InvalidPacketLength {
171            length: app_context.len(),
172            minimum: 0,
173        })?;
174    write_auth_header(
175        &mut writer,
176        user,
177        TNS_AUTH_MODE_LOGON | TNS_AUTH_MODE_WITH_PASSWORD,
178        num_pairs,
179    )?;
180    if let Some(proxy_user) = proxy_user {
181        write_key_value(&mut writer, "PROXY_CLIENT_NAME", proxy_user, 0)?;
182    }
183    write_key_value(&mut writer, "AUTH_SESSKEY", &encrypted.session_key, 1)?;
184    if let Some(speedy_key) = &encrypted.speedy_key {
185        write_key_value(&mut writer, "AUTH_PBKDF2_SPEEDY_KEY", speedy_key, 0)?;
186    }
187    write_key_value(&mut writer, "AUTH_PASSWORD", &encrypted.password, 0)?;
188    write_key_value(&mut writer, "SESSION_CLIENT_CHARSET", "873", 0)?;
189    write_key_value(&mut writer, "SESSION_CLIENT_DRIVER_NAME", driver_name, 0)?;
190    write_key_value(
191        &mut writer,
192        "SESSION_CLIENT_VERSION",
193        &version_num.to_string(),
194        0,
195    )?;
196    write_key_value(
197        &mut writer,
198        "AUTH_ALTER_SESSION",
199        "ALTER SESSION SET TIME_ZONE='+00:00'\0",
200        1,
201    )?;
202    // Edition-Based Redefinition: select the session edition during auth, exactly
203    // as the reference does (messages/auth.pyx writes `AUTH_ORA_EDITION` when
204    // `params.edition is not None`). Applied before any user SQL.
205    if let Some(edition) = edition {
206        write_key_value(&mut writer, "AUTH_ORA_EDITION", edition, 0)?;
207    }
208    for (namespace, name, value) in app_context {
209        write_key_value(&mut writer, "AUTH_APPCTX_NSPACE\0", namespace, 0)?;
210        write_key_value(&mut writer, "AUTH_APPCTX_ATTR\0", name, 0)?;
211        write_key_value(&mut writer, "AUTH_APPCTX_VALUE\0", value, 0)?;
212    }
213    if !connect_string.is_empty() {
214        write_key_value(&mut writer, "AUTH_CONNECT_STRING", connect_string, 0)?;
215    }
216    Ok(writer.into_bytes())
217}
218
219/// Change-password payload: an AUTH_PHASE_TWO message carrying only the
220/// combo-key-encrypted old/new passwords (reference
221/// connection.pyx `_create_change_password_message` + messages/auth.pyx
222/// `_write_message`: auth mode WITH_PASSWORD|CHANGE_PASSWORD, two pairs).
223pub fn build_change_password_payload_with_seq(
224    user: &str,
225    encoded_password: &str,
226    encoded_newpassword: &str,
227    seq_num: u8,
228) -> Result<Vec<u8>> {
229    let mut writer = TtcWriter::new();
230    writer.write_function_code_with_seq(TNS_FUNC_AUTH_PHASE_TWO, seq_num);
231    writer.write_ub8(0);
232    write_auth_header(
233        &mut writer,
234        user,
235        TNS_AUTH_MODE_WITH_PASSWORD | TNS_AUTH_MODE_CHANGE_PASSWORD,
236        2,
237    )?;
238    write_key_value(&mut writer, "AUTH_PASSWORD", encoded_password, 0)?;
239    write_key_value(&mut writer, "AUTH_NEWPASSWORD", encoded_newpassword, 0)?;
240    Ok(writer.into_bytes())
241}
242
243pub fn parse_auth_response(payload: &[u8]) -> Result<AuthResponse> {
244    let mut reader = TtcReader::new(payload);
245    let mut response = AuthResponse::default();
246    while reader.remaining() > 0 {
247        let message_type = reader.read_u8()?;
248        match message_type {
249            TNS_MSG_TYPE_PROTOCOL => {
250                if let Some(capabilities) = skip_protocol_message(&mut reader)? {
251                    response.capabilities = Some(capabilities);
252                }
253            }
254            TNS_MSG_TYPE_DATA_TYPES => skip_data_types_response(&mut reader)?,
255            TNS_MSG_TYPE_PARAMETER => {
256                let mut parsed = parse_return_parameters(&mut reader)?;
257                response.session_data.append(&mut parsed.session_data);
258                if parsed.verifier_type.is_some() {
259                    response.verifier_type = parsed.verifier_type;
260                }
261            }
262            TNS_MSG_TYPE_STATUS => {
263                let _call_status = reader.read_ub4()?;
264                let _seq = reader.read_ub2()?;
265            }
266            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
267                let _ = skip_server_side_piggyback(&mut reader)?;
268            }
269            TNS_MSG_TYPE_END_OF_RESPONSE => break,
270            TNS_MSG_TYPE_ERROR => {
271                if let Some(message) = parse_server_error(&mut reader, 13)? {
272                    return Err(ProtocolError::ServerError(message));
273                }
274            }
275            _ => {
276                return Err(ProtocolError::UnknownMessageType {
277                    message_type,
278                    position: reader.position().saturating_sub(1),
279                })
280            }
281        }
282    }
283    Ok(response)
284}
285
286pub(crate) fn write_auth_header(
287    writer: &mut TtcWriter,
288    user: &str,
289    auth_mode: u32,
290    num_pairs: u32,
291) -> Result<()> {
292    let user_bytes = user.as_bytes();
293    writer.write_u8(u8::from(!user_bytes.is_empty()));
294    writer.write_ub4(u32::try_from(user_bytes.len()).map_err(|_| {
295        ProtocolError::InvalidPacketLength {
296            length: user_bytes.len(),
297            minimum: 0,
298        }
299    })?);
300    writer.write_ub4(auth_mode);
301    writer.write_u8(1);
302    writer.write_ub4(num_pairs);
303    writer.write_u8(1);
304    writer.write_u8(1);
305    if !user_bytes.is_empty() {
306        writer.write_bytes_with_length(user_bytes)?;
307    }
308    Ok(())
309}
310
311pub(crate) fn write_key_value(
312    writer: &mut TtcWriter,
313    key: &str,
314    value: &str,
315    flags: u32,
316) -> Result<()> {
317    writer.write_str_two_lengths(key)?;
318    writer.write_str_two_lengths(value)?;
319    writer.write_ub4(flags);
320    Ok(())
321}
322
323pub(crate) fn parse_return_parameters(reader: &mut TtcReader<'_>) -> Result<AuthResponse> {
324    let num_params = reader.read_ub2()?;
325    let mut response = AuthResponse::default();
326    for _ in 0..num_params {
327        let key = reader
328            .read_string_with_length()?
329            .ok_or(ProtocolError::TtcDecode("missing auth response key"))?;
330        let value = reader.read_string_with_length()?.unwrap_or_default();
331        if key == "AUTH_VFR_DATA" {
332            response.verifier_type = Some(reader.read_ub4()?);
333        } else {
334            let _flags = reader.read_ub4()?;
335        }
336        response.session_data.insert(key, value);
337    }
338    Ok(response)
339}
340
341#[cfg(test)]
342mod token_auth_tests {
343    use super::*;
344
345    /// Decode an Oracle `ub4` at `*pos`, advancing it (see `WriteBuffer::write_ub4`).
346    fn read_ub4(bytes: &[u8], pos: &mut usize) -> u32 {
347        let len = bytes[*pos] as usize;
348        *pos += 1;
349        let mut value = 0u32;
350        for _ in 0..len {
351            value = (value << 8) | u32::from(bytes[*pos]);
352            *pos += 1;
353        }
354        value
355    }
356
357    fn contains(haystack: &[u8], needle: &[u8]) -> bool {
358        haystack.windows(needle.len()).any(|w| w == needle)
359    }
360
361    /// The token auth message must encode the token as `AUTH_TOKEN`, in auth mode
362    /// `LOGON` (never `WITH_PASSWORD`), with no `AUTH_SESSKEY`/`AUTH_PASSWORD` and
363    /// the correct key/value-pair count. This is the deterministic "cassette" that
364    /// pins the wire format against the reference (messages/auth.pyx).
365    #[test]
366    fn token_message_carries_auth_token_not_password() {
367        let mut out = Vec::new();
368        append_auth_phase_two_token(
369            &mut out,
370            "scott",
371            "HEADER.PAYLOAD.SIG",
372            "drv",
373            300_000_000,
374            "cs",
375        )
376        .unwrap();
377
378        // Function header: TTC function message, phase two, then the auth header.
379        assert_eq!(out[0], TNS_MSG_TYPE_FUNCTION);
380        assert_eq!(out[1], TNS_FUNC_AUTH_PHASE_TWO);
381        // out[2] is the sequence byte; out[3] is the has_user flag.
382        assert_eq!(out[3], 1, "user is present");
383        let mut pos = 4;
384        assert_eq!(read_ub4(&out, &mut pos), 5, "user length = len(\"scott\")");
385        assert_eq!(
386            read_ub4(&out, &mut pos),
387            TNS_AUTH_MODE_LOGON,
388            "token auth uses LOGON only — never the WITH_PASSWORD bit"
389        );
390        assert_eq!(out[pos], 1); // authivl pointer
391        pos += 1;
392        assert_eq!(
393            read_ub4(&out, &mut pos),
394            6,
395            "AUTH_TOKEN + 4 session pairs + AUTH_CONNECT_STRING"
396        );
397
398        assert!(contains(&out, b"AUTH_TOKEN"));
399        assert!(
400            contains(&out, b"HEADER.PAYLOAD.SIG"),
401            "the token value is sent"
402        );
403        assert!(contains(&out, b"AUTH_CONNECT_STRING"));
404        assert!(
405            !contains(&out, b"AUTH_PASSWORD") && !contains(&out, b"AUTH_SESSKEY"),
406            "token auth must not send any password material"
407        );
408    }
409
410    /// Without a connect string the pair count drops to exactly the token + the
411    /// four mandatory session pairs.
412    #[test]
413    fn token_message_pair_count_without_connect_string() {
414        let mut out = Vec::new();
415        append_auth_phase_two_token(&mut out, "u", "tok", "drv", 1, "").unwrap();
416        let mut pos = 4;
417        let _user_len = read_ub4(&out, &mut pos);
418        let _auth_mode = read_ub4(&out, &mut pos);
419        pos += 1; // authivl pointer
420        assert_eq!(read_ub4(&out, &mut pos), 5, "AUTH_TOKEN + 4 session pairs");
421        assert!(!contains(&out, b"AUTH_CONNECT_STRING"));
422    }
423}