Skip to main content

git2/
cred.rs

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