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    )
141}
142
143/// Phase-two auth payload with optional proxy authentication: the reference
144/// writes `PROXY_CLIENT_NAME` as the first key/value pair when the connect
145/// user is of the form `user[proxy_user]` (messages/auth.pyx).
146#[allow(clippy::too_many_arguments)]
147pub fn build_auth_phase_two_payload_with_proxy_with_seq(
148    user: &str,
149    encrypted: &crate::crypto::EncryptedPassword,
150    driver_name: &str,
151    version_num: u32,
152    connect_string: &str,
153    seq_num: u8,
154    app_context: &[(String, String, String)],
155    proxy_user: Option<&str>,
156    edition: Option<&str>,
157) -> Result<Vec<u8>> {
158    let mut writer = TtcWriter::new();
159    writer.write_function_code_with_seq(TNS_FUNC_AUTH_PHASE_TWO, seq_num);
160    writer.write_ub8(0);
161    let mut num_pairs = 6u32;
162    if encrypted.speedy_key.is_some() {
163        num_pairs += 1;
164    }
165    if proxy_user.is_some() {
166        num_pairs += 1;
167    }
168    if !connect_string.is_empty() {
169        num_pairs += 1;
170    }
171    if edition.is_some() {
172        num_pairs += 1;
173    }
174    let app_context_pairs =
175        app_context
176            .len()
177            .checked_mul(3)
178            .ok_or(ProtocolError::InvalidPacketLength {
179                length: app_context.len(),
180                minimum: 0,
181            })?;
182    num_pairs +=
183        u32::try_from(app_context_pairs).map_err(|_| ProtocolError::InvalidPacketLength {
184            length: app_context.len(),
185            minimum: 0,
186        })?;
187    write_auth_header(
188        &mut writer,
189        user,
190        TNS_AUTH_MODE_LOGON | TNS_AUTH_MODE_WITH_PASSWORD,
191        num_pairs,
192    )?;
193    if let Some(proxy_user) = proxy_user {
194        write_key_value(&mut writer, "PROXY_CLIENT_NAME", proxy_user, 0)?;
195    }
196    write_key_value(&mut writer, "AUTH_SESSKEY", &encrypted.session_key, 1)?;
197    if let Some(speedy_key) = &encrypted.speedy_key {
198        write_key_value(&mut writer, "AUTH_PBKDF2_SPEEDY_KEY", speedy_key, 0)?;
199    }
200    write_key_value(&mut writer, "AUTH_PASSWORD", &encrypted.password, 0)?;
201    write_key_value(&mut writer, "SESSION_CLIENT_CHARSET", "873", 0)?;
202    write_key_value(&mut writer, "SESSION_CLIENT_DRIVER_NAME", driver_name, 0)?;
203    write_key_value(
204        &mut writer,
205        "SESSION_CLIENT_VERSION",
206        &version_num.to_string(),
207        0,
208    )?;
209    write_key_value(
210        &mut writer,
211        "AUTH_ALTER_SESSION",
212        "ALTER SESSION SET TIME_ZONE='+00:00'\0",
213        1,
214    )?;
215    // Edition-Based Redefinition: select the session edition during auth, exactly
216    // as the reference does (messages/auth.pyx writes `AUTH_ORA_EDITION` when
217    // `params.edition is not None`). Applied before any user SQL.
218    if let Some(edition) = edition {
219        write_key_value(&mut writer, "AUTH_ORA_EDITION", edition, 0)?;
220    }
221    for (namespace, name, value) in app_context {
222        write_key_value(&mut writer, "AUTH_APPCTX_NSPACE\0", namespace, 0)?;
223        write_key_value(&mut writer, "AUTH_APPCTX_ATTR\0", name, 0)?;
224        write_key_value(&mut writer, "AUTH_APPCTX_VALUE\0", value, 0)?;
225    }
226    if !connect_string.is_empty() {
227        write_key_value(&mut writer, "AUTH_CONNECT_STRING", connect_string, 0)?;
228    }
229    Ok(writer.into_bytes())
230}
231
232/// Change-password payload: an AUTH_PHASE_TWO message carrying only the
233/// combo-key-encrypted old/new passwords (reference
234/// connection.pyx `_create_change_password_message` + messages/auth.pyx
235/// `_write_message`: auth mode WITH_PASSWORD|CHANGE_PASSWORD, two pairs).
236pub fn build_change_password_payload_with_seq(
237    user: &str,
238    encoded_password: &str,
239    encoded_newpassword: &str,
240    seq_num: u8,
241) -> Result<Vec<u8>> {
242    let mut writer = TtcWriter::new();
243    writer.write_function_code_with_seq(TNS_FUNC_AUTH_PHASE_TWO, seq_num);
244    writer.write_ub8(0);
245    write_auth_header(
246        &mut writer,
247        user,
248        TNS_AUTH_MODE_WITH_PASSWORD | TNS_AUTH_MODE_CHANGE_PASSWORD,
249        2,
250    )?;
251    write_key_value(&mut writer, "AUTH_PASSWORD", encoded_password, 0)?;
252    write_key_value(&mut writer, "AUTH_NEWPASSWORD", encoded_newpassword, 0)?;
253    Ok(writer.into_bytes())
254}
255
256pub fn parse_auth_response(payload: &[u8]) -> Result<AuthResponse> {
257    parse_auth_response_with_limits(payload, ProtocolLimits::DEFAULT)
258}
259
260pub fn parse_auth_response_with_limits(
261    payload: &[u8],
262    limits: ProtocolLimits,
263) -> Result<AuthResponse> {
264    let mut reader = TtcReader::with_limits(payload, limits)?;
265    let mut response = AuthResponse::default();
266    while reader.remaining() > 0 {
267        let message_type = reader.read_u8()?;
268        match message_type {
269            TNS_MSG_TYPE_PROTOCOL => {
270                if let Some(capabilities) = skip_protocol_message(&mut reader)? {
271                    response.capabilities = Some(capabilities);
272                }
273            }
274            TNS_MSG_TYPE_DATA_TYPES => skip_data_types_response(&mut reader)?,
275            TNS_MSG_TYPE_PARAMETER => {
276                let mut parsed = parse_return_parameters(&mut reader)?;
277                response.session_data.append(&mut parsed.session_data);
278                if parsed.verifier_type.is_some() {
279                    response.verifier_type = parsed.verifier_type;
280                }
281            }
282            TNS_MSG_TYPE_STATUS => {
283                let _call_status = reader.read_ub4()?;
284                let _seq = reader.read_ub2()?;
285            }
286            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
287                let _ = skip_server_side_piggyback(&mut reader)?;
288            }
289            TNS_MSG_TYPE_END_OF_RESPONSE => break,
290            TNS_MSG_TYPE_ERROR => {
291                if let Some(message) = parse_server_error(&mut reader, 13)? {
292                    return Err(ProtocolError::ServerError(message));
293                }
294            }
295            _ => {
296                return Err(ProtocolError::UnknownMessageType {
297                    message_type,
298                    position: reader.position().saturating_sub(1),
299                })
300            }
301        }
302    }
303    Ok(response)
304}
305
306pub(crate) fn write_auth_header(
307    writer: &mut TtcWriter,
308    user: &str,
309    auth_mode: u32,
310    num_pairs: u32,
311) -> Result<()> {
312    let user_bytes = user.as_bytes();
313    writer.write_u8(u8::from(!user_bytes.is_empty()));
314    writer.write_ub4(u32::try_from(user_bytes.len()).map_err(|_| {
315        ProtocolError::InvalidPacketLength {
316            length: user_bytes.len(),
317            minimum: 0,
318        }
319    })?);
320    writer.write_ub4(auth_mode);
321    writer.write_u8(1);
322    writer.write_ub4(num_pairs);
323    writer.write_u8(1);
324    writer.write_u8(1);
325    if !user_bytes.is_empty() {
326        writer.write_bytes_with_length(user_bytes)?;
327    }
328    Ok(())
329}
330
331pub(crate) fn write_key_value(
332    writer: &mut TtcWriter,
333    key: &str,
334    value: &str,
335    flags: u32,
336) -> Result<()> {
337    writer.write_str_two_lengths(key)?;
338    writer.write_str_two_lengths(value)?;
339    writer.write_ub4(flags);
340    Ok(())
341}
342
343pub(crate) fn parse_return_parameters(reader: &mut TtcReader<'_>) -> Result<AuthResponse> {
344    let num_params = reader.read_ub2()?;
345    reader
346        .limits()
347        .check_length_prefixed_elements(usize::from(num_params))?;
348    let mut response = AuthResponse::default();
349    for _ in 0..num_params {
350        let key = reader
351            .read_string_with_length()?
352            .ok_or(ProtocolError::TtcDecode("missing auth response key"))?;
353        let value = reader.read_string_with_length()?.unwrap_or_default();
354        if key == "AUTH_VFR_DATA" {
355            response.verifier_type = Some(reader.read_ub4()?);
356        } else {
357            let _flags = reader.read_ub4()?;
358        }
359        response.session_data.insert(key, value);
360    }
361    Ok(response)
362}
363
364#[cfg(test)]
365mod token_auth_tests {
366    use super::*;
367
368    /// Decode an Oracle `ub4` at `*pos`, advancing it (see `WriteBuffer::write_ub4`).
369    fn read_ub4(bytes: &[u8], pos: &mut usize) -> u32 {
370        let len = bytes[*pos] as usize;
371        *pos += 1;
372        let mut value = 0u32;
373        for _ in 0..len {
374            value = (value << 8) | u32::from(bytes[*pos]);
375            *pos += 1;
376        }
377        value
378    }
379
380    fn contains(haystack: &[u8], needle: &[u8]) -> bool {
381        haystack.windows(needle.len()).any(|w| w == needle)
382    }
383
384    /// The token auth message must encode the token as `AUTH_TOKEN`, in auth mode
385    /// `LOGON` (never `WITH_PASSWORD`), with no `AUTH_SESSKEY`/`AUTH_PASSWORD` and
386    /// the correct key/value-pair count. This is the deterministic "cassette" that
387    /// pins the wire format against the reference (messages/auth.pyx).
388    #[test]
389    fn token_message_carries_auth_token_not_password() {
390        let mut out = Vec::new();
391        append_auth_phase_two_token(
392            &mut out,
393            "scott",
394            "HEADER.PAYLOAD.SIG",
395            "drv",
396            300_000_000,
397            "cs",
398            None,
399        )
400        .unwrap();
401
402        // Function header: TTC function message, phase two, then the auth header.
403        assert_eq!(out[0], TNS_MSG_TYPE_FUNCTION);
404        assert_eq!(out[1], TNS_FUNC_AUTH_PHASE_TWO);
405        // out[2] is the sequence byte; out[3] is the has_user flag.
406        assert_eq!(out[3], 1, "user is present");
407        let mut pos = 4;
408        assert_eq!(read_ub4(&out, &mut pos), 5, "user length = len(\"scott\")");
409        assert_eq!(
410            read_ub4(&out, &mut pos),
411            TNS_AUTH_MODE_LOGON,
412            "token auth uses LOGON only — never the WITH_PASSWORD bit"
413        );
414        assert_eq!(out[pos], 1); // authivl pointer
415        pos += 1;
416        assert_eq!(
417            read_ub4(&out, &mut pos),
418            6,
419            "AUTH_TOKEN + 4 session pairs + AUTH_CONNECT_STRING"
420        );
421
422        assert!(contains(&out, b"AUTH_TOKEN"));
423        assert!(
424            contains(&out, b"HEADER.PAYLOAD.SIG"),
425            "the token value is sent"
426        );
427        assert!(contains(&out, b"AUTH_CONNECT_STRING"));
428        assert!(
429            !contains(&out, b"AUTH_PASSWORD") && !contains(&out, b"AUTH_SESSKEY"),
430            "token auth must not send any password material"
431        );
432    }
433
434    /// Without a connect string the pair count drops to exactly the token + the
435    /// four mandatory session pairs.
436    #[test]
437    fn token_message_pair_count_without_connect_string() {
438        let mut out = Vec::new();
439        append_auth_phase_two_token(&mut out, "u", "tok", "drv", 1, "", None).unwrap();
440        let mut pos = 4;
441        let _user_len = read_ub4(&out, &mut pos);
442        let _auth_mode = read_ub4(&out, &mut pos);
443        pos += 1; // authivl pointer
444        assert_eq!(read_ub4(&out, &mut pos), 5, "AUTH_TOKEN + 4 session pairs");
445        assert!(!contains(&out, b"AUTH_CONNECT_STRING"));
446    }
447
448    /// Edition-Based Redefinition must reach the server on the token path too:
449    /// `AUTH_ORA_EDITION` is written and counted, exactly as on the password path
450    /// (regression guard for the 0.2.0 bug where token auth dropped the edition).
451    #[test]
452    fn token_message_carries_edition() {
453        let mut out = Vec::new();
454        append_auth_phase_two_token(&mut out, "u", "tok", "drv", 1, "", Some("E_TEST")).unwrap();
455        let mut pos = 4;
456        let _user_len = read_ub4(&out, &mut pos);
457        let _auth_mode = read_ub4(&out, &mut pos);
458        pos += 1; // authivl pointer
459        assert_eq!(
460            read_ub4(&out, &mut pos),
461            6,
462            "AUTH_TOKEN + 4 session pairs + AUTH_ORA_EDITION"
463        );
464        assert!(contains(&out, b"AUTH_ORA_EDITION"));
465        assert!(contains(&out, b"E_TEST"), "the edition value is sent");
466
467        // With both an edition and a connect string the count rises to 7.
468        let mut out2 = Vec::new();
469        append_auth_phase_two_token(&mut out2, "u", "tok", "drv", 1, "cs", Some("E_TEST")).unwrap();
470        let mut p = 4;
471        let _ = read_ub4(&out2, &mut p);
472        let _ = read_ub4(&out2, &mut p);
473        p += 1;
474        assert_eq!(read_ub4(&out2, &mut p), 7, "+ AUTH_CONNECT_STRING");
475    }
476}