git2/
cred.rs

1use log::{debug, trace};
2use std::ffi::CString;
3use std::io::Write;
4use std::mem;
5use std::path::Path;
6use std::process::{Command, Stdio};
7use std::ptr;
8
9use crate::util::Binding;
10use crate::{raw, Config, Error, IntoCString};
11
12/// A structure to represent git credentials in libgit2.
13pub struct Cred {
14    raw: *mut raw::git_cred,
15}
16
17/// Management of the gitcredentials(7) interface.
18pub struct CredentialHelper {
19    /// A public field representing the currently discovered username from
20    /// configuration.
21    pub username: Option<String>,
22    protocol: Option<String>,
23    host: Option<String>,
24    port: Option<u16>,
25    path: Option<String>,
26    url: String,
27    commands: Vec<String>,
28}
29
30impl Cred {
31    /// Create a "default" credential usable for Negotiate mechanisms like NTLM
32    /// or Kerberos authentication.
33    pub fn default() -> Result<Cred, Error> {
34        crate::init();
35        let mut out = ptr::null_mut();
36        unsafe {
37            try_call!(raw::git_cred_default_new(&mut out));
38            Ok(Binding::from_raw(out))
39        }
40    }
41
42    /// Create a new ssh key credential object used for querying an ssh-agent.
43    ///
44    /// The username specified is the username to authenticate.
45    pub fn ssh_key_from_agent(username: &str) -> Result<Cred, Error> {
46        crate::init();
47        let mut out = ptr::null_mut();
48        let username = CString::new(username)?;
49        unsafe {
50            try_call!(raw::git_cred_ssh_key_from_agent(&mut out, username));
51            Ok(Binding::from_raw(out))
52        }
53    }
54
55    /// Create a new passphrase-protected ssh key credential object.
56    pub fn ssh_key(
57        username: &str,
58        publickey: Option<&Path>,
59        privatekey: &Path,
60        passphrase: Option<&str>,
61    ) -> Result<Cred, Error> {
62        crate::init();
63        let username = CString::new(username)?;
64        let publickey = crate::opt_cstr(publickey)?;
65        let privatekey = privatekey.into_c_string()?;
66        let passphrase = crate::opt_cstr(passphrase)?;
67        let mut out = ptr::null_mut();
68        unsafe {
69            try_call!(raw::git_cred_ssh_key_new(
70                &mut out, username, publickey, privatekey, passphrase
71            ));
72            Ok(Binding::from_raw(out))
73        }
74    }
75
76    /// Create a new ssh key credential object reading the keys from memory.
77    pub fn ssh_key_from_memory(
78        username: &str,
79        publickey: Option<&str>,
80        privatekey: &str,
81        passphrase: Option<&str>,
82    ) -> Result<Cred, Error> {
83        crate::init();
84        let username = CString::new(username)?;
85        let publickey = crate::opt_cstr(publickey)?;
86        let privatekey = CString::new(privatekey)?;
87        let passphrase = crate::opt_cstr(passphrase)?;
88        let mut out = ptr::null_mut();
89        unsafe {
90            try_call!(raw::git_cred_ssh_key_memory_new(
91                &mut out, username, publickey, privatekey, passphrase
92            ));
93            Ok(Binding::from_raw(out))
94        }
95    }
96
97    /// Create a new plain-text username and password credential object.
98    pub fn userpass_plaintext(username: &str, password: &str) -> Result<Cred, Error> {
99        crate::init();
100        let username = CString::new(username)?;
101        let password = CString::new(password)?;
102        let mut out = ptr::null_mut();
103        unsafe {
104            try_call!(raw::git_cred_userpass_plaintext_new(
105                &mut out, username, password
106            ));
107            Ok(Binding::from_raw(out))
108        }
109    }
110
111    /// Attempt to read `credential.helper` according to gitcredentials(7) [1]
112    ///
113    /// This function will attempt to parse the user's `credential.helper`
114    /// configuration, invoke the necessary processes, and read off what the
115    /// username/password should be for a particular URL.
116    ///
117    /// The returned credential type will be a username/password credential if
118    /// successful.
119    ///
120    /// [1]: https://www.kernel.org/pub/software/scm/git/docs/gitcredentials.html
121    pub fn credential_helper(
122        config: &Config,
123        url: &str,
124        username: Option<&str>,
125    ) -> Result<Cred, Error> {
126        match CredentialHelper::new(url)
127            .config(config)
128            .username(username)
129            .execute()
130        {
131            Some((username, password)) => Cred::userpass_plaintext(&username, &password),
132            None => Err(Error::from_str(
133                "failed to acquire username/password \
134                 from local configuration",
135            )),
136        }
137    }
138
139    /// Create a credential to specify a username.
140    ///
141    /// This is used with ssh authentication to query for the username if none is
142    /// specified in the URL.
143    pub fn username(username: &str) -> Result<Cred, Error> {
144        crate::init();
145        let username = CString::new(username)?;
146        let mut out = ptr::null_mut();
147        unsafe {
148            try_call!(raw::git_cred_username_new(&mut out, username));
149            Ok(Binding::from_raw(out))
150        }
151    }
152
153    /// Check whether a credential object contains username information.
154    pub fn has_username(&self) -> bool {
155        unsafe { raw::git_cred_has_username(self.raw) == 1 }
156    }
157
158    /// Return the type of credentials that this object represents.
159    pub fn credtype(&self) -> raw::git_credtype_t {
160        unsafe { (*self.raw).credtype }
161    }
162
163    /// Unwrap access to the underlying raw pointer, canceling the destructor
164    pub unsafe fn unwrap(mut self) -> *mut raw::git_cred {
165        mem::replace(&mut self.raw, ptr::null_mut())
166    }
167}
168
169impl Binding for Cred {
170    type Raw = *mut raw::git_cred;
171
172    unsafe fn from_raw(raw: *mut raw::git_cred) -> Cred {
173        Cred { raw }
174    }
175    fn raw(&self) -> *mut raw::git_cred {
176        self.raw
177    }
178}
179
180impl Drop for Cred {
181    fn drop(&mut self) {
182        if !self.raw.is_null() {
183            unsafe {
184                if let Some(f) = (*self.raw).free {
185                    f(self.raw)
186                }
187            }
188        }
189    }
190}
191
192impl CredentialHelper {
193    /// Create a new credential helper object which will be used to probe git's
194    /// local credential configuration.
195    ///
196    /// The URL specified is the namespace on which this will query credentials.
197    /// Invalid URLs are currently ignored.
198    pub fn new(url: &str) -> CredentialHelper {
199        let mut ret = CredentialHelper {
200            protocol: None,
201            host: None,
202            port: None,
203            path: None,
204            username: None,
205            url: url.to_string(),
206            commands: Vec::new(),
207        };
208
209        // Parse out the (protocol, host) if one is available
210        if let Ok(url) = url::Url::parse(url) {
211            if let Some(url::Host::Domain(s)) = url.host() {
212                ret.host = Some(s.to_string());
213            }
214            ret.port = url.port();
215            ret.protocol = Some(url.scheme().to_string());
216        }
217        ret
218    }
219
220    /// Set the username that this credential helper will query with.
221    ///
222    /// By default the username is `None`.
223    pub fn username(&mut self, username: Option<&str>) -> &mut CredentialHelper {
224        self.username = username.map(|s| s.to_string());
225        self
226    }
227
228    /// Query the specified configuration object to discover commands to
229    /// execute, usernames to query, etc.
230    pub fn config(&mut self, config: &Config) -> &mut CredentialHelper {
231        // Figure out the configured username/helper program.
232        //
233        // see http://git-scm.com/docs/gitcredentials.html#_configuration_options
234        if self.username.is_none() {
235            self.config_username(config);
236        }
237        self.config_helper(config);
238        self.config_use_http_path(config);
239        self
240    }
241
242    // Configure the queried username from `config`
243    fn config_username(&mut self, config: &Config) {
244        let key = self.exact_key("username");
245        self.username = config
246            .get_string(&key)
247            .ok()
248            .or_else(|| {
249                self.url_key("username")
250                    .and_then(|s| config.get_string(&s).ok())
251            })
252            .or_else(|| config.get_string("credential.username").ok())
253    }
254
255    // Discover all `helper` directives from `config`
256    fn config_helper(&mut self, config: &Config) {
257        let exact = config.get_string(&self.exact_key("helper"));
258        self.add_command(exact.as_ref().ok().map(|s| &s[..]));
259        if let Some(key) = self.url_key("helper") {
260            let url = config.get_string(&key);
261            self.add_command(url.as_ref().ok().map(|s| &s[..]));
262        }
263        let global = config.get_string("credential.helper");
264        self.add_command(global.as_ref().ok().map(|s| &s[..]));
265    }
266
267    // Discover `useHttpPath` from `config`
268    fn config_use_http_path(&mut self, config: &Config) {
269        let mut use_http_path = false;
270        if let Some(value) = config.get_bool(&self.exact_key("useHttpPath")).ok() {
271            use_http_path = value;
272        } else if let Some(value) = self
273            .url_key("useHttpPath")
274            .and_then(|key| config.get_bool(&key).ok())
275        {
276            use_http_path = value;
277        } else if let Some(value) = config.get_bool("credential.useHttpPath").ok() {
278            use_http_path = value;
279        }
280
281        if use_http_path {
282            if let Ok(url) = url::Url::parse(&self.url) {
283                let path = url.path();
284                // Url::parse always includes a leading slash for rooted URLs, while git does not.
285                self.path = Some(path.strip_prefix('/').unwrap_or(path).to_string());
286            }
287        }
288    }
289
290    // Add a `helper` configured command to the list of commands to execute.
291    //
292    // see https://www.kernel.org/pub/software/scm/git/docs/technical
293    //                           /api-credentials.html#_credential_helpers
294    fn add_command(&mut self, cmd: Option<&str>) {
295        let cmd = match cmd {
296            Some("") | None => return,
297            Some(s) => s,
298        };
299
300        if cmd.starts_with('!') {
301            self.commands.push(cmd[1..].to_string());
302        } else if is_absolute_path(cmd) {
303            self.commands.push(cmd.to_string());
304        } else {
305            self.commands.push(format!("git credential-{}", cmd));
306        }
307    }
308
309    fn exact_key(&self, name: &str) -> String {
310        format!("credential.{}.{}", self.url, name)
311    }
312
313    fn url_key(&self, name: &str) -> Option<String> {
314        match (&self.host, &self.protocol) {
315            (&Some(ref host), &Some(ref protocol)) => {
316                Some(format!("credential.{}://{}.{}", protocol, host, name))
317            }
318            _ => None,
319        }
320    }
321
322    /// Execute this helper, attempting to discover a username/password pair.
323    ///
324    /// All I/O errors are ignored, (to match git behavior), and this function
325    /// only succeeds if both a username and a password were found
326    pub fn execute(&self) -> Option<(String, String)> {
327        let mut username = self.username.clone();
328        let mut password = None;
329        for cmd in &self.commands {
330            let (u, p) = self.execute_cmd(cmd, &username);
331            if u.is_some() && username.is_none() {
332                username = u;
333            }
334            if p.is_some() && password.is_none() {
335                password = p;
336            }
337            if username.is_some() && password.is_some() {
338                break;
339            }
340        }
341
342        match (username, password) {
343            (Some(u), Some(p)) => Some((u, p)),
344            _ => None,
345        }
346    }
347
348    // Execute the given `cmd`, providing the appropriate variables on stdin and
349    // then afterwards parsing the output into the username/password on stdout.
350    fn execute_cmd(
351        &self,
352        cmd: &str,
353        username: &Option<String>,
354    ) -> (Option<String>, Option<String>) {
355        macro_rules! my_try( ($e:expr) => (
356            match $e {
357                Ok(e) => e,
358                Err(e) => {
359                    debug!("{} failed with {}", stringify!($e), e);
360                    return (None, None)
361                }
362            }
363        ) );
364
365        // It looks like the `cmd` specification is typically bourne-shell-like
366        // syntax, so try that first. If that fails, though, we may be on a
367        // Windows machine for example where `sh` isn't actually available by
368        // default. Most credential helper configurations though are pretty
369        // simple (aka one or two space-separated strings) so also try to invoke
370        // the process directly.
371        //
372        // If that fails then it's up to the user to put `sh` in path and make
373        // sure it works.
374        let mut c = Command::new("sh");
375        #[cfg(windows)]
376        {
377            use std::os::windows::process::CommandExt;
378            const CREATE_NO_WINDOW: u32 = 0x08000000;
379            c.creation_flags(CREATE_NO_WINDOW);
380        }
381        c.arg("-c")
382            .arg(&format!("{} get", cmd))
383            .stdin(Stdio::piped())
384            .stdout(Stdio::piped())
385            .stderr(Stdio::piped());
386        debug!("executing credential helper {:?}", c);
387        let mut p = match c.spawn() {
388            Ok(p) => p,
389            Err(e) => {
390                debug!("`sh` failed to spawn: {}", e);
391                let mut parts = cmd.split_whitespace();
392                let mut c = Command::new(parts.next().unwrap());
393                #[cfg(windows)]
394                {
395                    use std::os::windows::process::CommandExt;
396                    const CREATE_NO_WINDOW: u32 = 0x08000000;
397                    c.creation_flags(CREATE_NO_WINDOW);
398                }
399                for arg in parts {
400                    c.arg(arg);
401                }
402                c.arg("get")
403                    .stdin(Stdio::piped())
404                    .stdout(Stdio::piped())
405                    .stderr(Stdio::piped());
406                debug!("executing credential helper {:?}", c);
407                match c.spawn() {
408                    Ok(p) => p,
409                    Err(e) => {
410                        debug!("fallback of {:?} failed with {}", cmd, e);
411                        return (None, None);
412                    }
413                }
414            }
415        };
416
417        // Ignore write errors as the command may not actually be listening for
418        // stdin
419        {
420            let stdin = p.stdin.as_mut().unwrap();
421            if let Some(ref p) = self.protocol {
422                let _ = writeln!(stdin, "protocol={}", p);
423            }
424            if let Some(ref p) = self.host {
425                if let Some(ref p2) = self.port {
426                    let _ = writeln!(stdin, "host={}:{}", p, p2);
427                } else {
428                    let _ = writeln!(stdin, "host={}", p);
429                }
430            }
431            if let Some(ref p) = self.path {
432                let _ = writeln!(stdin, "path={}", p);
433            }
434            if let Some(ref p) = *username {
435                let _ = writeln!(stdin, "username={}", p);
436            }
437        }
438        let output = my_try!(p.wait_with_output());
439        if !output.status.success() {
440            debug!(
441                "credential helper failed: {}\nstdout ---\n{}\nstderr ---\n{}",
442                output.status,
443                String::from_utf8_lossy(&output.stdout),
444                String::from_utf8_lossy(&output.stderr)
445            );
446            return (None, None);
447        }
448        trace!(
449            "credential helper stderr ---\n{}",
450            String::from_utf8_lossy(&output.stderr)
451        );
452        self.parse_output(output.stdout)
453    }
454
455    // Parse the output of a command into the username/password found
456    fn parse_output(&self, output: Vec<u8>) -> (Option<String>, Option<String>) {
457        // Parse the output of the command, looking for username/password
458        let mut username = None;
459        let mut password = None;
460        for line in output.split(|t| *t == b'\n') {
461            let mut parts = line.splitn(2, |t| *t == b'=');
462            let key = parts.next().unwrap();
463            let value = match parts.next() {
464                Some(s) => s,
465                None => {
466                    trace!("ignoring output line: {}", String::from_utf8_lossy(line));
467                    continue;
468                }
469            };
470            let value = match String::from_utf8(value.to_vec()) {
471                Ok(s) => s,
472                Err(..) => continue,
473            };
474            match key {
475                b"username" => username = Some(value),
476                b"password" => password = Some(value),
477                _ => {}
478            }
479        }
480        (username, password)
481    }
482}
483
484fn is_absolute_path(path: &str) -> bool {
485    path.starts_with('/')
486        || path.starts_with('\\')
487        || cfg!(windows) && path.chars().nth(1).is_some_and(|x| x == ':')
488}
489
490#[cfg(test)]
491mod test {
492    use std::env;
493    use std::fs::File;
494    use std::io::prelude::*;
495    use std::path::Path;
496    use tempfile::TempDir;
497
498    use crate::{Config, ConfigLevel, Cred, CredentialHelper};
499
500    macro_rules! test_cfg( ($($k:expr => $v:expr),*) => ({
501        let td = TempDir::new().unwrap();
502        let mut cfg = Config::new().unwrap();
503        cfg.add_file(&td.path().join("cfg"), ConfigLevel::App, false).unwrap();
504        $(cfg.set_str($k, $v).unwrap();)*
505        cfg
506    }) );
507
508    #[test]
509    fn smoke() {
510        Cred::default().unwrap();
511    }
512
513    #[test]
514    fn credential_helper1() {
515        let cfg = test_cfg! {
516            "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
517        };
518        let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
519            .config(&cfg)
520            .execute()
521            .unwrap();
522        assert_eq!(u, "a");
523        assert_eq!(p, "b");
524    }
525
526    #[test]
527    fn credential_helper2() {
528        let cfg = test_cfg! {};
529        assert!(CredentialHelper::new("https://example.com/foo/bar")
530            .config(&cfg)
531            .execute()
532            .is_none());
533    }
534
535    #[test]
536    fn credential_helper3() {
537        let cfg = test_cfg! {
538            "credential.https://example.com.helper" =>
539                    "!f() { echo username=c; }; f",
540            "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
541        };
542        let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
543            .config(&cfg)
544            .execute()
545            .unwrap();
546        assert_eq!(u, "c");
547        assert_eq!(p, "b");
548    }
549
550    #[test]
551    fn credential_helper4() {
552        if cfg!(windows) {
553            return;
554        } // shell scripts don't work on Windows
555
556        let td = TempDir::new().unwrap();
557        let path = td.path().join("script");
558        File::create(&path)
559            .unwrap()
560            .write(
561                br"\
562#!/bin/sh
563echo username=c
564",
565            )
566            .unwrap();
567        chmod(&path);
568        let cfg = test_cfg! {
569            "credential.https://example.com.helper" =>
570                    &path.display().to_string()[..],
571            "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
572        };
573        let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
574            .config(&cfg)
575            .execute()
576            .unwrap();
577        assert_eq!(u, "c");
578        assert_eq!(p, "b");
579    }
580
581    #[test]
582    fn credential_helper5() {
583        if cfg!(windows) {
584            return;
585        } // shell scripts don't work on Windows
586        let td = TempDir::new().unwrap();
587        let path = td.path().join("git-credential-some-script");
588        File::create(&path)
589            .unwrap()
590            .write(
591                br"\
592#!/bin/sh
593echo username=$1
594",
595            )
596            .unwrap();
597        chmod(&path);
598
599        let paths = env::var("PATH").unwrap();
600        let paths =
601            env::split_paths(&paths).chain(path.parent().map(|p| p.to_path_buf()).into_iter());
602        env::set_var("PATH", &env::join_paths(paths).unwrap());
603
604        let cfg = test_cfg! {
605            "credential.https://example.com.helper" => "some-script \"value/with\\slashes\"",
606            "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
607        };
608        let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
609            .config(&cfg)
610            .execute()
611            .unwrap();
612        assert_eq!(u, "value/with\\slashes");
613        assert_eq!(p, "b");
614    }
615
616    #[test]
617    fn credential_helper6() {
618        let cfg = test_cfg! {
619            "credential.helper" => ""
620        };
621        assert!(CredentialHelper::new("https://example.com/foo/bar")
622            .config(&cfg)
623            .execute()
624            .is_none());
625    }
626
627    #[test]
628    fn credential_helper7() {
629        if cfg!(windows) {
630            return;
631        } // shell scripts don't work on Windows
632        let td = TempDir::new().unwrap();
633        let path = td.path().join("script");
634        File::create(&path)
635            .unwrap()
636            .write(
637                br"\
638#!/bin/sh
639echo username=$1
640echo password=$2
641",
642            )
643            .unwrap();
644        chmod(&path);
645        let cfg = test_cfg! {
646            "credential.helper" => &format!("{} a b", path.display())
647        };
648        let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
649            .config(&cfg)
650            .execute()
651            .unwrap();
652        assert_eq!(u, "a");
653        assert_eq!(p, "b");
654    }
655
656    #[test]
657    fn credential_helper8() {
658        let cfg = test_cfg! {
659            "credential.useHttpPath" => "true"
660        };
661        let mut helper = CredentialHelper::new("https://example.com/foo/bar");
662        helper.config(&cfg);
663        assert_eq!(helper.path.as_deref(), Some("foo/bar"));
664    }
665
666    #[test]
667    fn credential_helper9() {
668        let cfg = test_cfg! {
669            "credential.helper" => "!f() { while read line; do eval $line; done; if [ \"$host\" = example.com:3000 ]; then echo username=a; echo password=b; fi; }; f"
670        };
671        let (u, p) = CredentialHelper::new("https://example.com:3000/foo/bar")
672            .config(&cfg)
673            .execute()
674            .unwrap();
675        assert_eq!(u, "a");
676        assert_eq!(p, "b");
677    }
678
679    #[test]
680    #[cfg(feature = "ssh")]
681    fn ssh_key_from_memory() {
682        let cred = Cred::ssh_key_from_memory(
683            "test",
684            Some("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDByAO8uj+kXicj6C2ODMspgmUoVyl5eaw8vR6a1yEnFuJFzevabNlN6Ut+CPT3TRnYk5BW73pyXBtnSL2X95BOnbjMDXc4YIkgs3YYHWnxbqsD4Pj/RoGqhf+gwhOBtL0poh8tT8WqXZYxdJQKLQC7oBqf3ykCEYulE4oeRUmNh4IzEE+skD/zDkaJ+S1HRD8D8YCiTO01qQnSmoDFdmIZTi8MS8Cw+O/Qhym1271ThMlhD6PubSYJXfE6rVbE7A9RzH73A6MmKBlzK8VTb4SlNSrr/DOk+L0uq+wPkv+pm+D9WtxoqQ9yl6FaK1cPawa3+7yRNle3m+72KCtyMkQv"),
685            r#"
686                -----BEGIN RSA PRIVATE KEY-----
687                Proc-Type: 4,ENCRYPTED
688                DEK-Info: AES-128-CBC,818C7722D3B01F2161C2ACF6A5BBAAE8
689
690                3Cht4QB3PcoQ0I55j1B3m2ZzIC/mrh+K5nQeA1Vy2GBTMyM7yqGHqTOv7qLhJscd
691                H+cB0Pm6yCr3lYuNrcKWOCUto+91P7ikyARruHVwyIxKdNx15uNulOzQJHQWNbA4
692                RQHlhjON4atVo2FyJ6n+ujK6QiBg2PR5Vbbw/AtV6zBCFW3PhzDn+qqmHjpBFqj2
693                vZUUe+MkDQcaF5J45XMHahhSdo/uKCDhfbylExp/+ACWkvxdPpsvcARM6X434ucD
694                aPY+4i0/JyLkdbm0GFN9/q3i53qf4kCBhojFl4AYJdGI0AzAgbdTXZ7EJHbAGZHS
695                os5K0oTwDVXMI0sSE2I/qHxaZZsDP1dOKq6di6SFPUp8liYimm7rNintRX88Gl2L
696                g1ko9abp/NlgD0YY/3mad+NNAISDL/YfXq2fklH3En3/7ZrOVZFKfZXwQwas5g+p
697                VQPKi3+ae74iOjLyuPDSc1ePmhUNYeP+9rLSc0wiaiHqls+2blPPDxAGMEo63kbz
698                YPVjdmuVX4VWnyEsfTxxJdFDYGSNh6rlrrO1RFrex7kJvpg5gTX4M/FT8TfCd7Hn
699                M6adXsLMqwu5tz8FuDmAtVdq8zdSrgZeAbpJ9D3EDOmZ70xz4XBL19ImxDp+Qqs2
700                kQX7kobRzeeP2URfRoGr7XZikQWyQ2UASfPcQULY8R58QoZWWsQ4w51GZHg7TDnw
701                1DRo/0OgkK7Gqf215nFmMpB4uyi58cq3WFwWQa1IqslkObpVgBQZcNZb/hKUYPGk
702                g4zehfIgAfCdnQHwZvQ6Fdzhcs3SZeO+zVyuiZN3Gsi9HU0/1vpAKiuuOzcG02vF
703                b6Y6hwsAA9yphF3atI+ARD4ZwXdDfzuGb3yJglMT3Fr/xuLwAvdchRo1spANKA0E
704                tT5okLrK0H4wnHvf2SniVVWRhmJis0lQo9LjGGwRIdsPpVnJSDvaISIVF+fHT90r
705                HvxN8zXI93x9jcPtwp7puQ1C7ehKJK10sZ71OLIZeuUgwt+5DRunqg6evPco9Go7
706                UOGwcVhLY200KT+1k7zWzCS0yVQp2HRm6cxsZXAp4ClBSwIx15eIoLIrjZdJRjCq
707                COp6pZx1fnvJ9ERIvl5hon+Ty+renMcFKz2HmchC7egpcqIxW9Dsv6zjhHle6pxb
708                37GaEKHF2KA3RN+dSV/K8n+C9Yent5tx5Y9a/pMcgRGtgu+G+nyFmkPKn5Zt39yX
709                qDpyM0LtbRVZPs+MgiqoGIwYc/ujoCq7GL38gezsBQoHaTt79yYBqCp6UR0LMuZ5
710                f/7CtWqffgySfJ/0wjGidDAumDv8CK45AURpL/Z+tbFG3M9ar/LZz/Y6EyBcLtGY
711                Wwb4zs8zXIA0qHrjNTnPqHDvezziArYfgPjxCIHMZzms9Yn8+N02p39uIytqg434
712                BAlCqZ7GYdDFfTpWIwX+segTK9ux0KdBqcQv+9Fwwjkq9KySnRKqNl7ZJcefFZJq
713                c6PA1iinZWBjuaO1HKx3PFulrl0bcpR9Kud1ZIyfnh5rwYN8UQkkcR/wZPla04TY
714                8l5dq/LI/3G5sZXwUHKOcuQWTj7Saq7Q6gkKoMfqt0wC5bpZ1m17GHPoMz6GtX9O
715                -----END RSA PRIVATE KEY-----
716            "#,
717            Some("test123"));
718        assert!(cred.is_ok());
719    }
720
721    #[cfg(unix)]
722    fn chmod(path: &Path) {
723        use std::fs;
724        use std::os::unix::prelude::*;
725        let mut perms = fs::metadata(path).unwrap().permissions();
726        perms.set_mode(0o755);
727        fs::set_permissions(path, perms).unwrap();
728    }
729    #[cfg(windows)]
730    fn chmod(_path: &Path) {}
731}