gpg-rs / gpgme

GPGme bindings for Rust
GNU Lesser General Public License v2.1
83 stars 13 forks source link

Exporting secret keys #33

Closed DBLouis closed 3 years ago

DBLouis commented 4 years ago

The following code fails to export secret keys. The result is an empty file. Public keys however seems to be exported correctly. I ran it using the master branch.

use gpgme::{
    Context, CreateKeyFlags as Flags, ExportMode, PassphraseRequest, PinentryMode, Protocol,
};
use std::{fs::File, io::Write, iter, path::PathBuf, time::Duration};

fn main() {
    let validity = Duration::from_secs(3600);

    let mut ctx = Context::from_protocol(Protocol::OpenPgp).unwrap();
    ctx.set_offline(true);
    ctx.set_armor(true);
    ctx.set_pinentry_mode(PinentryMode::Loopback).unwrap();
    ctx.set_engine_home_dir("/tmp").unwrap();

    let mut ctx = ctx.set_passphrase_provider(|_: PassphraseRequest, out: &mut dyn Write| {
        out.write_all("password".as_bytes())?;
        Ok(())
    });

    let res = ctx
        .create_key_with_flags("john doe <john@doe.com>", "ed25519", validity, Flags::CERT)
        .unwrap();

    let fpr = res.fingerprint().unwrap();
    let key = ctx.get_key(fpr).unwrap();

    ctx.create_subkey_with_flags(&key, "ed25519", validity, Flags::AUTH)
        .unwrap();
    ctx.create_subkey_with_flags(&key, "cv25519", validity, Flags::ENCR)
        .unwrap();
    ctx.create_subkey_with_flags(&key, "ed25519", validity, Flags::SIGN)
        .unwrap();

    let mut path = PathBuf::from("/tmp");

    path.push(format!("{}.pub.asc", fpr));
    let file = File::create(&path).unwrap();
    ctx.export(iter::once(fpr), ExportMode::empty(), &file)
        .unwrap();

    path.pop();
    path.push(format!("{}.sec.asc", fpr));
    let file = File::create(&path).unwrap();
    ctx.export(iter::once(fpr), ExportMode::SECRET, &file)
        .unwrap();
}
johnschug commented 4 years ago

I think think this fails because recent(ish) versions of gpg don't seem to allow export of a password protected secret key without user interaction (see T2324: gpgme uses --batch internally). I'm not sure why it doesn't return an error (might be worth reporting upstream). If the key doesn't need to be password protected you can use CreateKeyFlags::NOPASSWD to create it without one (for example see tests/export.rs).

DBLouis commented 4 years ago

I need the keys to be password protected. I don't get why is it not working because I do have the password callback set and the engine configured to loopback. I know I successfully made this work before in a shell script so it shouldn't be any different.


This works without user interaction:

gpg2 --passphrase "${user_pass}" --pinentry-mode loopback \
  --batch --export-secret-keys --armor "${fingerprint}" \
  >"${fingerprint}.asc"
johnschug commented 4 years ago

The command that gpgme actually executes can be seen by setting the GPGME_DEBUG environment variable. With GPGME_DEBUG=9, the above Context::export call produces something like this. The command can be seen on lines 26-49. It appears that a passphrase is requested but is unable to be retrieved. Comparing this to the command used by an earlier successful call to Context::create_key, the major difference seems to be in the --command-fd 7 argument that is passed. After the passphrase is requested, the passphrase callback writes the passphrase to other end (fd 8) of the pipe.

Looking at the source for gpgme_op_createkey, it appears that gpgme_op_createkey (really createkey_start) sets a command handler if a passphrase callback is set on the context. I would suggest opening a bug report (or submitting a patch) upstream to request gpgme_op_export be modified to do this if secret key export is requested.

DBLouis commented 4 years ago

It works with the patch I posted there: https://dev.gnupg.org/T5046.

johnschug commented 3 years ago

It looks like the patch has been merged upstream and is included in the 1.15 release.