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