Skip to main content

oracledb_protocol/thin/
auth.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4use crate::wire::ProtocolLimits;
5
6pub fn append_auth_phase_one(
7    out: &mut Vec<u8>,
8    user: &str,
9    program: &str,
10    machine: &str,
11    osuser: &str,
12    terminal: &str,
13    pid: u32,
14) -> Result<()> {
15    let mut writer = TtcWriter::new();
16    writer.write_function_code(TNS_FUNC_AUTH_PHASE_ONE);
17    write_auth_header(&mut writer, user, TNS_AUTH_MODE_LOGON, 5)?;
18    write_key_value(&mut writer, "AUTH_TERMINAL", terminal, 0)?;
19    write_key_value(&mut writer, "AUTH_PROGRAM_NM", program, 0)?;
20    write_key_value(&mut writer, "AUTH_MACHINE", machine, 0)?;
21    write_key_value(&mut writer, "AUTH_PID", &pid.to_string(), 0)?;
22    write_key_value(&mut writer, "AUTH_SID", osuser, 0)?;
23    out.extend_from_slice(&writer.into_bytes());
24    Ok(())
25}
26
27/// Appends the auth message for **token authentication** (OCI IAM database
28/// token / OAuth2) to the fast-auth bundle. Unlike password auth there is no
29/// verifier challenge: the reference sends auth phase TWO directly, carrying the
30/// token in `AUTH_TOKEN` with no `AUTH_SESSKEY`/`AUTH_PASSWORD` and auth mode
31/// `LOGON` (no `WITH_PASSWORD`); it never resends (messages/auth.pyx
32/// `_set_params`/`_write_message`, messages/fast_auth.pyx). Because this message
33/// lives inside the fast-auth bundle (ttc field version 19.1), the function code
34/// carries no `ub8` token-num — exactly like [`append_auth_phase_one`].
35pub fn append_auth_phase_two_token(
36    out: &mut Vec<u8>,
37    user: &str,
38    token: &str,
39    driver_name: &str,
40    version_num: u32,
41    connect_string: &str,
42    edition: Option<&str>,
43) -> Result<()> {
44    let mut writer = TtcWriter::new();
45    writer.write_function_code(TNS_FUNC_AUTH_PHASE_TWO);
46    // AUTH_TOKEN + the four mandatory session pairs, plus the optional
47    // AUTH_ORA_EDITION and AUTH_CONNECT_STRING.
48    let mut num_pairs = 5u32;
49    if edition.is_some() {
50        num_pairs += 1;
51    }
52    if !connect_string.is_empty() {
53        num_pairs += 1;
54    }
55    write_auth_header(&mut writer, user, TNS_AUTH_MODE_LOGON, num_pairs)?;
56    write_key_value(&mut writer, "AUTH_TOKEN", token, 0)?;
57    write_key_value(&mut writer, "SESSION_CLIENT_CHARSET", "873", 0)?;
58    write_key_value(&mut writer, "SESSION_CLIENT_DRIVER_NAME", driver_name, 0)?;
59    write_key_value(
60        &mut writer,
61        "SESSION_CLIENT_VERSION",
62        &version_num.to_string(),
63        0,
64    )?;
65    write_key_value(
66        &mut writer,
67        "AUTH_ALTER_SESSION",
68        "ALTER SESSION SET TIME_ZONE='+00:00'\0",
69        1,
70    )?;
71    // Edition-Based Redefinition applies to token auth too — the reference writes
72    // AUTH_ORA_EDITION after AUTH_ALTER_SESSION on both auth paths (messages/auth.pyx
73    // `_write_message`); omitting it here silently ran token sessions under the
74    // default edition.
75    if let Some(edition) = edition {
76        write_key_value(&mut writer, "AUTH_ORA_EDITION", edition, 0)?;
77    }
78    if !connect_string.is_empty() {
79        write_key_value(&mut writer, "AUTH_CONNECT_STRING", connect_string, 0)?;
80    }
81    out.extend_from_slice(&writer.into_bytes());
82    Ok(())
83}
84
85pub fn build_auth_phase_two_payload(
86    user: &str,
87    encrypted: &crate::crypto::EncryptedPassword,
88    driver_name: &str,
89    version_num: u32,
90    connect_string: &str,
91) -> Result<Vec<u8>> {
92    build_auth_phase_two_payload_with_seq(
93        user,
94        encrypted,
95        driver_name,
96        version_num,
97        connect_string,
98        1,
99    )
100}
101
102pub fn build_auth_phase_two_payload_with_seq(
103    user: &str,
104    encrypted: &crate::crypto::EncryptedPassword,
105    driver_name: &str,
106    version_num: u32,
107    connect_string: &str,
108    seq_num: u8,
109) -> Result<Vec<u8>> {
110    build_auth_phase_two_payload_with_context_with_seq(
111        user,
112        encrypted,
113        driver_name,
114        version_num,
115        connect_string,
116        seq_num,
117        &[],
118    )
119}
120
121pub fn build_auth_phase_two_payload_with_context_with_seq(
122    user: &str,
123    encrypted: &crate::crypto::EncryptedPassword,
124    driver_name: &str,
125    version_num: u32,
126    connect_string: &str,
127    seq_num: u8,
128    app_context: &[(String, String, String)],
129) -> Result<Vec<u8>> {
130    build_auth_phase_two_payload_with_proxy_with_seq(
131        user,
132        encrypted,
133        driver_name,
134        version_num,
135        connect_string,
136        seq_num,
137        app_context,
138        None,
139        None,
140        ClientCapabilities::default().ttc_field_version,
141    )
142}
143
144/// Phase-two auth payload with optional proxy authentication: the reference
145/// writes `PROXY_CLIENT_NAME` as the first key/value pair when the connect
146/// user is of the form `user[proxy_user]` (messages/auth.pyx).
147///
148/// `ttc_field_version` is the field version negotiated with THIS server: the
149/// ub8 pipeline-token in the function header is a 23.1+ field (reference
150/// messages/base.pyx `_write_function_code`); a pre-23ai server parses the
151/// stray byte as part of the auth header and breaks the connection with a
152/// MARKER (observed live against Oracle XE 18c).
153#[allow(clippy::too_many_arguments)]
154pub fn build_auth_phase_two_payload_with_proxy_with_seq(
155    user: &str,
156    encrypted: &crate::crypto::EncryptedPassword,
157    driver_name: &str,
158    version_num: u32,
159    connect_string: &str,
160    seq_num: u8,
161    app_context: &[(String, String, String)],
162    proxy_user: Option<&str>,
163    edition: Option<&str>,
164    ttc_field_version: u8,
165) -> Result<Vec<u8>> {
166    let mut writer = TtcWriter::new();
167    writer.write_function_code_with_seq(TNS_FUNC_AUTH_PHASE_TWO, seq_num);
168    if version_gates::writes_pipeline_token(ttc_field_version) {
169        writer.write_ub8(0);
170    }
171    let mut num_pairs = 6u32;
172    if encrypted.speedy_key.is_some() {
173        num_pairs += 1;
174    }
175    if proxy_user.is_some() {
176        num_pairs += 1;
177    }
178    if !connect_string.is_empty() {
179        num_pairs += 1;
180    }
181    if edition.is_some() {
182        num_pairs += 1;
183    }
184    let app_context_pairs =
185        app_context
186            .len()
187            .checked_mul(3)
188            .ok_or(ProtocolError::InvalidPacketLength {
189                length: app_context.len(),
190                minimum: 0,
191            })?;
192    num_pairs +=
193        u32::try_from(app_context_pairs).map_err(|_| ProtocolError::InvalidPacketLength {
194            length: app_context.len(),
195            minimum: 0,
196        })?;
197    write_auth_header(
198        &mut writer,
199        user,
200        TNS_AUTH_MODE_LOGON | TNS_AUTH_MODE_WITH_PASSWORD,
201        num_pairs,
202    )?;
203    if let Some(proxy_user) = proxy_user {
204        write_key_value(&mut writer, "PROXY_CLIENT_NAME", proxy_user, 0)?;
205    }
206    write_key_value(&mut writer, "AUTH_SESSKEY", &encrypted.session_key, 1)?;
207    if let Some(speedy_key) = &encrypted.speedy_key {
208        write_key_value(&mut writer, "AUTH_PBKDF2_SPEEDY_KEY", speedy_key, 0)?;
209    }
210    write_key_value(&mut writer, "AUTH_PASSWORD", &encrypted.password, 0)?;
211    write_key_value(&mut writer, "SESSION_CLIENT_CHARSET", "873", 0)?;
212    write_key_value(&mut writer, "SESSION_CLIENT_DRIVER_NAME", driver_name, 0)?;
213    write_key_value(
214        &mut writer,
215        "SESSION_CLIENT_VERSION",
216        &version_num.to_string(),
217        0,
218    )?;
219    write_key_value(
220        &mut writer,
221        "AUTH_ALTER_SESSION",
222        "ALTER SESSION SET TIME_ZONE='+00:00'\0",
223        1,
224    )?;
225    // Edition-Based Redefinition: select the session edition during auth, exactly
226    // as the reference does (messages/auth.pyx writes `AUTH_ORA_EDITION` when
227    // `params.edition is not None`). Applied before any user SQL.
228    if let Some(edition) = edition {
229        write_key_value(&mut writer, "AUTH_ORA_EDITION", edition, 0)?;
230    }
231    for (namespace, name, value) in app_context {
232        write_key_value(&mut writer, "AUTH_APPCTX_NSPACE\0", namespace, 0)?;
233        write_key_value(&mut writer, "AUTH_APPCTX_ATTR\0", name, 0)?;
234        write_key_value(&mut writer, "AUTH_APPCTX_VALUE\0", value, 0)?;
235    }
236    if !connect_string.is_empty() {
237        write_key_value(&mut writer, "AUTH_CONNECT_STRING", connect_string, 0)?;
238    }
239    Ok(writer.into_bytes())
240}
241
242/// Change-password payload: an AUTH_PHASE_TWO message carrying only the
243/// combo-key-encrypted old/new passwords (reference
244/// connection.pyx `_create_change_password_message` + messages/auth.pyx
245/// `_write_message`: auth mode WITH_PASSWORD|CHANGE_PASSWORD, two pairs).
246pub fn build_change_password_payload_with_seq(
247    user: &str,
248    encoded_password: &str,
249    encoded_newpassword: &str,
250    seq_num: u8,
251    ttc_field_version: u8,
252) -> Result<Vec<u8>> {
253    let mut writer = TtcWriter::new();
254    writer.write_function_code_with_seq(TNS_FUNC_AUTH_PHASE_TWO, seq_num);
255    if version_gates::writes_pipeline_token(ttc_field_version) {
256        writer.write_ub8(0);
257    }
258    write_auth_header(
259        &mut writer,
260        user,
261        TNS_AUTH_MODE_WITH_PASSWORD | TNS_AUTH_MODE_CHANGE_PASSWORD,
262        2,
263    )?;
264    write_key_value(&mut writer, "AUTH_PASSWORD", encoded_password, 0)?;
265    write_key_value(&mut writer, "AUTH_NEWPASSWORD", encoded_newpassword, 0)?;
266    Ok(writer.into_bytes())
267}
268
269pub fn parse_auth_response(payload: &[u8]) -> Result<AuthResponse> {
270    parse_auth_response_with_limits(payload, ProtocolLimits::DEFAULT)
271}
272
273/// Whether an accumulated classic (pre-END_OF_RESPONSE) connect-phase response
274/// is complete.
275///
276/// Servers that did not negotiate END_OF_RESPONSE framing (protocol version
277/// below 319, i.e. everything before 23ai) never set the end-of-response DATA
278/// flag; the response instead ends when its *terminal message* has been read
279/// (reference messages/base.pyx `Message.process`: loop until
280/// `end_of_response`). The terminal messages for the connect-phase round trips
281/// are:
282///
283/// - protocol negotiation (msg 1): ends the response once processed
284///   (messages/protocol.pyx),
285/// - data types (msg 2): same (messages/data_types.pyx),
286/// - STATUS (msg 9): ends any response when END_OF_RESPONSE framing is off
287///   (messages/base.pyx),
288/// - ERROR (msg 4): carries the failure that the real parse will surface.
289///
290/// Returns `Ok(false)` when the payload runs out mid-message — the caller must
291/// read the next DATA packet and try again, exactly like the reference's
292/// `ReadBuffer` blocking for more packets mid-parse. Unknown message types
293/// propagate as errors.
294pub fn classic_connect_response_is_complete(
295    payload: &[u8],
296    limits: ProtocolLimits,
297) -> Result<bool> {
298    let Ok(mut reader) = TtcReader::with_limits(payload, limits) else {
299        return Ok(false);
300    };
301    while reader.remaining() > 0 {
302        let message_type = reader.read_u8()?;
303        let terminal = match message_type {
304            TNS_MSG_TYPE_PROTOCOL => match skip_protocol_message(&mut reader) {
305                Ok(_) => true,
306                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
307                Err(err) => return Err(err),
308            },
309            TNS_MSG_TYPE_DATA_TYPES => match skip_data_types_response(&mut reader) {
310                Ok(()) => true,
311                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
312                Err(err) => return Err(err),
313            },
314            TNS_MSG_TYPE_PARAMETER => match parse_return_parameters(&mut reader) {
315                Ok(_) => false,
316                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
317                Err(err) => return Err(err),
318            },
319            TNS_MSG_TYPE_STATUS => {
320                let complete = reader.read_ub4().and_then(|_| reader.read_ub2());
321                match complete {
322                    Ok(_) => true,
323                    Err(ProtocolError::TtcDecode(_)) => return Ok(false),
324                    Err(err) => return Err(err),
325                }
326            }
327            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => match skip_server_side_piggyback(&mut reader) {
328                Ok(_) => false,
329                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
330                Err(err) => return Err(err),
331            },
332            TNS_MSG_TYPE_END_OF_RESPONSE => true,
333            TNS_MSG_TYPE_ERROR => match parse_server_error(&mut reader, 13) {
334                Ok(_) | Err(ProtocolError::ServerError(_)) => true,
335                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
336                Err(err) => return Err(err),
337            },
338            _ => {
339                return Err(ProtocolError::UnknownMessageType {
340                    message_type,
341                    position: reader.position().saturating_sub(1),
342                })
343            }
344        };
345        if terminal {
346            return Ok(true);
347        }
348    }
349    Ok(false)
350}
351
352pub fn parse_auth_response_with_limits(
353    payload: &[u8],
354    limits: ProtocolLimits,
355) -> Result<AuthResponse> {
356    let mut reader = TtcReader::with_limits(payload, limits)?;
357    let mut response = AuthResponse::default();
358    while reader.remaining() > 0 {
359        let message_type = reader.read_u8()?;
360        match message_type {
361            TNS_MSG_TYPE_PROTOCOL => {
362                if let Some(capabilities) = skip_protocol_message(&mut reader)? {
363                    response.capabilities = Some(capabilities);
364                }
365            }
366            TNS_MSG_TYPE_DATA_TYPES => skip_data_types_response(&mut reader)?,
367            TNS_MSG_TYPE_PARAMETER => {
368                let mut parsed = parse_return_parameters(&mut reader)?;
369                response.session_data.append(&mut parsed.session_data);
370                if parsed.verifier_type.is_some() {
371                    response.verifier_type = parsed.verifier_type;
372                }
373            }
374            TNS_MSG_TYPE_STATUS => {
375                let _call_status = reader.read_ub4()?;
376                let _seq = reader.read_ub2()?;
377            }
378            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
379                let _ = skip_server_side_piggyback(&mut reader)?;
380            }
381            TNS_MSG_TYPE_END_OF_RESPONSE => break,
382            TNS_MSG_TYPE_ERROR => {
383                if let Some(message) = parse_server_error(&mut reader, 13)? {
384                    return Err(ProtocolError::ServerError(message));
385                }
386            }
387            _ => {
388                return Err(ProtocolError::UnknownMessageType {
389                    message_type,
390                    position: reader.position().saturating_sub(1),
391                })
392            }
393        }
394    }
395    Ok(response)
396}
397
398pub(crate) fn write_auth_header(
399    writer: &mut TtcWriter,
400    user: &str,
401    auth_mode: u32,
402    num_pairs: u32,
403) -> Result<()> {
404    let user_bytes = user.as_bytes();
405    writer.write_u8(u8::from(!user_bytes.is_empty()));
406    writer.write_ub4(u32::try_from(user_bytes.len()).map_err(|_| {
407        ProtocolError::InvalidPacketLength {
408            length: user_bytes.len(),
409            minimum: 0,
410        }
411    })?);
412    writer.write_ub4(auth_mode);
413    writer.write_u8(1);
414    writer.write_ub4(num_pairs);
415    writer.write_u8(1);
416    writer.write_u8(1);
417    if !user_bytes.is_empty() {
418        writer.write_bytes_with_length(user_bytes)?;
419    }
420    Ok(())
421}
422
423pub(crate) fn write_key_value(
424    writer: &mut TtcWriter,
425    key: &str,
426    value: &str,
427    flags: u32,
428) -> Result<()> {
429    writer.write_str_two_lengths(key)?;
430    writer.write_str_two_lengths(value)?;
431    writer.write_ub4(flags);
432    Ok(())
433}
434
435pub(crate) fn parse_return_parameters(reader: &mut TtcReader<'_>) -> Result<AuthResponse> {
436    let num_params = reader.read_ub2()?;
437    reader
438        .limits()
439        .check_length_prefixed_elements(usize::from(num_params))?;
440    let mut response = AuthResponse::default();
441    for _ in 0..num_params {
442        let key = reader
443            .read_string_with_length()?
444            .ok_or(ProtocolError::TtcDecode("missing auth response key"))?;
445        let value = reader.read_string_with_length()?.unwrap_or_default();
446        if key == "AUTH_VFR_DATA" {
447            response.verifier_type = Some(reader.read_ub4()?);
448        } else {
449            let _flags = reader.read_ub4()?;
450        }
451        response.session_data.insert(key, value);
452    }
453    Ok(response)
454}
455
456#[cfg(test)]
457mod token_auth_tests {
458    use super::*;
459
460    /// Decode an Oracle `ub4` at `*pos`, advancing it (see `WriteBuffer::write_ub4`).
461    fn read_ub4(bytes: &[u8], pos: &mut usize) -> u32 {
462        let len = bytes[*pos] as usize;
463        *pos += 1;
464        let mut value = 0u32;
465        for _ in 0..len {
466            value = (value << 8) | u32::from(bytes[*pos]);
467            *pos += 1;
468        }
469        value
470    }
471
472    fn contains(haystack: &[u8], needle: &[u8]) -> bool {
473        haystack.windows(needle.len()).any(|w| w == needle)
474    }
475
476    /// The token auth message must encode the token as `AUTH_TOKEN`, in auth mode
477    /// `LOGON` (never `WITH_PASSWORD`), with no `AUTH_SESSKEY`/`AUTH_PASSWORD` and
478    /// the correct key/value-pair count. This is the deterministic "cassette" that
479    /// pins the wire format against the reference (messages/auth.pyx).
480    #[test]
481    fn token_message_carries_auth_token_not_password() {
482        let mut out = Vec::new();
483        append_auth_phase_two_token(
484            &mut out,
485            "scott",
486            "HEADER.PAYLOAD.SIG",
487            "drv",
488            300_000_000,
489            "cs",
490            None,
491        )
492        .unwrap();
493
494        // Function header: TTC function message, phase two, then the auth header.
495        assert_eq!(out[0], TNS_MSG_TYPE_FUNCTION);
496        assert_eq!(out[1], TNS_FUNC_AUTH_PHASE_TWO);
497        // out[2] is the sequence byte; out[3] is the has_user flag.
498        assert_eq!(out[3], 1, "user is present");
499        let mut pos = 4;
500        assert_eq!(read_ub4(&out, &mut pos), 5, "user length = len(\"scott\")");
501        assert_eq!(
502            read_ub4(&out, &mut pos),
503            TNS_AUTH_MODE_LOGON,
504            "token auth uses LOGON only — never the WITH_PASSWORD bit"
505        );
506        assert_eq!(out[pos], 1); // authivl pointer
507        pos += 1;
508        assert_eq!(
509            read_ub4(&out, &mut pos),
510            6,
511            "AUTH_TOKEN + 4 session pairs + AUTH_CONNECT_STRING"
512        );
513
514        assert!(contains(&out, b"AUTH_TOKEN"));
515        assert!(
516            contains(&out, b"HEADER.PAYLOAD.SIG"),
517            "the token value is sent"
518        );
519        assert!(contains(&out, b"AUTH_CONNECT_STRING"));
520        assert!(
521            !contains(&out, b"AUTH_PASSWORD") && !contains(&out, b"AUTH_SESSKEY"),
522            "token auth must not send any password material"
523        );
524    }
525
526    /// Without a connect string the pair count drops to exactly the token + the
527    /// four mandatory session pairs.
528    #[test]
529    fn token_message_pair_count_without_connect_string() {
530        let mut out = Vec::new();
531        append_auth_phase_two_token(&mut out, "u", "tok", "drv", 1, "", None).unwrap();
532        let mut pos = 4;
533        let _user_len = read_ub4(&out, &mut pos);
534        let _auth_mode = read_ub4(&out, &mut pos);
535        pos += 1; // authivl pointer
536        assert_eq!(read_ub4(&out, &mut pos), 5, "AUTH_TOKEN + 4 session pairs");
537        assert!(!contains(&out, b"AUTH_CONNECT_STRING"));
538    }
539
540    /// Edition-Based Redefinition must reach the server on the token path too:
541    /// `AUTH_ORA_EDITION` is written and counted, exactly as on the password path
542    /// (regression guard for the 0.2.0 bug where token auth dropped the edition).
543    #[test]
544    fn token_message_carries_edition() {
545        let mut out = Vec::new();
546        append_auth_phase_two_token(&mut out, "u", "tok", "drv", 1, "", Some("E_TEST")).unwrap();
547        let mut pos = 4;
548        let _user_len = read_ub4(&out, &mut pos);
549        let _auth_mode = read_ub4(&out, &mut pos);
550        pos += 1; // authivl pointer
551        assert_eq!(
552            read_ub4(&out, &mut pos),
553            6,
554            "AUTH_TOKEN + 4 session pairs + AUTH_ORA_EDITION"
555        );
556        assert!(contains(&out, b"AUTH_ORA_EDITION"));
557        assert!(contains(&out, b"E_TEST"), "the edition value is sent");
558
559        // With both an edition and a connect string the count rises to 7.
560        let mut out2 = Vec::new();
561        append_auth_phase_two_token(&mut out2, "u", "tok", "drv", 1, "cs", Some("E_TEST")).unwrap();
562        let mut p = 4;
563        let _ = read_ub4(&out2, &mut p);
564        let _ = read_ub4(&out2, &mut p);
565        p += 1;
566        assert_eq!(read_ub4(&out2, &mut p), 7, "+ AUTH_CONNECT_STRING");
567    }
568}