ACC SHELL
/**
* File: modules/GPG.ycp
* Package: yast2
* Summary: A wrapper for gpg binary
* Authors: Ladislav Slezák <lslezak@suse.cz>
*
* $Id: GPG.ycp 60336 2010-01-12 14:18:15Z lslezak $
*
* This module provides GPG key related functions. It is a wrapper around gpg
* binary. It uses caching for reading GPG keys from the keyrings.
*/
{
import "String";
import "Report";
import "FileUtils";
module "GPG";
textdomain "base";
// value for --homedir gpg option, empty string means default home directory
string home = "";
// key cache
list<map> public_keys = nil;
// key cache
list<map> private_keys = nil;
/**
* (Re)initialize the module, the cache is invalidated if the home directory is changed.
* @param home_dir home directory for gpg (location of the keyring)
* @param force unconditionaly clear the key caches
*/
global boolean Init(string home_dir, boolean force)
{
if (FileUtils::IsDirectory(home_dir) != true)
{
y2error("Path %1 is not a directory", home_dir);
return false;
}
if (home_dir != home || force)
{
// clear the cache, home has been changed
public_keys = nil;
private_keys = nil;
}
home = home_dir;
return true;
}
/**
* Build GPG option string
* @param options additional gpg options
* @return string gpg option string
*/
string buildGPGcommand(string options)
{
string home_opt = (size(home) > 0) ? sformat("--homedir '%1' ", String::Quote(home)) : "";
string ret = "gpg " + home_opt + options;
y2milestone("gpg command: %1", ret);
return ret;
}
/**
* Execute gpg with the specified parameters, --homedir is added to the options
* @param options additional gpg options
* @return map result of the execution
*/
map callGPG(string options)
{
map ret = $[];
string command = "LC_ALL=C " + buildGPGcommand(options);
ret = (map) SCR::Execute(.target.bash_output, command);
if (ret["exit"]:-1 != 0)
{
y2error("gpg error: %1", ret);
}
return ret;
}
/**
* Map for parsing gpg output. Key is regexp, value is the key returned
* in the result of the parsing.
*/
map<string,string> parsing_map = $[
// secret key ID
"^sec .*/([^ ]*) " : "id",
// public key id
"^pub .*/([^ ]*) " : "id",
// user id
"^uid *(.*)" : "uid",
// fingerprint
"^ Key fingerprint = (.*)" : "fingerprint"
];
/**
* Parse gpg output using the parsing map
* @param lines gpg output (splitted into lines)
* @return map parsed output
*/
map parse_key(list<string> lines)
{
map ret = $[];
foreach(string line, lines,
{
foreach(string regexp, string key, parsing_map,
{
string parsed = regexpsub(line, regexp, "\\1");
if (parsed != nil)
{
// there might be more UIDs
if (key == "uid")
{
y2milestone("%1: %2", key, parsed);
ret[key] = add(ret[key]:[], parsed);
}
else
{
if (haskey(ret, key))
{
y2warning("Key %1: replacing old value '%2' with '%3'", key, ret[key]:"", parsed);
}
ret[key] = parsed;
}
}
}
);
}
);
y2milestone("Parsed key: %1", ret);
return ret;
}
/**
* Parse gpg output
* @param input gpg output
* @return list<map> parsed keys
*/
list<map> parseKeys(string input)
{
// note: see /usr/share/doc/packages/gpg/DETAILS for another way
list<map> ret = [];
list<string> lines = splitstring(input, "\n");
if (size(input) > 2)
{
// remove the header
lines = remove(lines, 0);
lines = remove(lines, 0);
}
list<list<string> > key_lines = [];
list<string> key_line_list = [];
// create groups
foreach(string line, lines,
{
if (line == "")
{
key_lines = add(key_lines, key_line_list);
key_line_list = [];
}
else
{
key_line_list = add(key_line_list, line);
}
}
);
// not needed anymore, save some memory
lines = [];
// parse each group to map
foreach(list<string> keylines, key_lines,
{
map parsed = parse_key(keylines);
if (size(parsed) > 0)
{
ret = add(ret, parsed);
}
}
);
y2milestone("Parsed keys: %1", ret);
return ret;
}
/**
* Return list of the public keys in the keyring.
* @return list<map> public keys: [ $["fingerprint": string key_fingerprint, "id": string key_ID, "uid": list<string> user_ids], ...]
*/
global list<map> PublicKeys()
{
// return the cached values if available
if (public_keys != nil)
{
return public_keys;
}
map out = callGPG("--list-keys --fingerprint");
if (out["exit"]:-1 == 0)
{
public_keys = parseKeys(out["stdout"]:"");
}
return public_keys;
}
/**
* Return list of the private keys in the keyring.
* @return list<map> public keys: [ $["fingerprint": string key_fingerprint, "id": string key_ID, "uid": list<string> user_ids], ...]
*/
global list<map> PrivateKeys()
{
// return the cached values if available
if (private_keys != nil)
{
return private_keys;
}
map out = callGPG("--list-secret-keys --fingerprint");
if (out["exit"]:-1 == 0)
{
private_keys = parseKeys(out["stdout"]:"");
}
return private_keys;
}
/**
* Create a new gpg key. Executes 'gpg --gen-key' in an xterm window (in the QT UI)
* or in the terminal window (in the ncurses UI).
*/
global boolean CreateKey()
{
string xterm = "/usr/bin/xterm";
string command = buildGPGcommand("--gen-key");
boolean text_mode = UI::GetDisplayInfo()["TextMode"]:false;
y2debug("text_mode: %1", text_mode);
boolean ret = false;
if (!text_mode)
{
if (SCR::Read(.target.size, xterm) < 0)
{
// TODO FIXME
Report::Error(_("Xterm is missing, install xterm package."));
return false;
}
string exit_file = ((string)SCR::Read(.target.tmpdir)) + "/gpg_tmp_exit_file";
if (FileUtils::Exists(exit_file))
{
SCR::Execute(.target.execute, "rm -f " + exit_file);
}
command = "LC_ALL=C " + xterm + " -e \"" + command + "; echo $? > " + exit_file + "\"";
y2internal("Executing: %1", command);
// in Qt start GPG in a xterm window
SCR::Execute(.target.bash, command);
if (FileUtils::Exists(exit_file))
{
// read the exit code from file
// (the exit code from the SCR call above is the xterm exit code which is not what we want here)
string exit_code = (string)SCR::Read(.target.string, exit_file);
y2milestone("Read exit code from tmp file %1: %2", exit_file, exit_code);
ret = exit_code == "0\n";
}
else
{
y2warning("Exit file is missing, the gpg command has failed");
ret = false;
}
}
else
{
command = "LC_ALL=C " + command;
y2internal("Executing in terminal: %1", command);
// in ncurses use UI::RunInTerminal
ret = UI::RunInTerminal(command) == 0;
}
if (ret)
{
// invalidate cache, force reloading
Init(home, true);
}
return ret;
}
/**
* Sign a file. The ASCII armored signature is stored in file with .asc suffix
* @param keyid id of the signing key
* @param file the file to sign
* @param passphrase passphrase to unlock the private key
* @param ascii_signature if true ASCII armored signature is created
* (with suffix .asc) otherwise binary signature (with suffix .sig) is created
* @return boolean true if the file has been successfuly signed
*/
boolean SignFile(string keyid, string file, string passphrase, boolean ascii_signature)
{
if (passphrase == nil || keyid == nil || keyid == "" || file == nil || file == "")
{
y2error("Invalid parameters: keyid: %1, file: %2, passphrase: %3", keyid, file, passphrase);
return false;
}
// signature suffix depends on the format
string suffix = ascii_signature ? ".asc" : ".sig";
if ((integer)SCR::Read(.target.size, file + suffix) >= 0)
{
// remove the existing key
SCR::Execute(.target.bash, sformat("rm -f '%1%2'", String::Quote(file), suffix));
}
// save the passphrase to a file
string tmpfile = (string)SCR::Read(.target.tmpdir) + "/stdin";
boolean written = (boolean)SCR::Write(.target.string, tmpfile, passphrase + "\n");
if (!written)
{
return false;
}
// use the passphrase
map out = callGPG(sformat("--detach-sign -u '%1' --no-tty --batch --command-fd=0 --passphrase-fd 0 %2 '%3' < '%4'", String::Quote(keyid), (ascii_signature ? "-a" : ""), String::Quote(file), String::Quote(tmpfile)));
return out["exit"]:-1 == 0;
}
/**
* Sign a file. The ASCII armored signature is stored in file with .asc suffix
* @param keyid id of the signing key
* @param file the file to sign
* @param passphrase passphrase to unlock the private key
* @return boolean true if the file has been successfuly signed
*/
global boolean SignAsciiDetached(string keyid, string file, string passphrase)
{
return SignFile(keyid, file, passphrase, true);
}
/**
* Sign a file. The binary signature is stored in file with .sig suffix
* @param keyid id of the signing key
* @param file the file to sign
* @param passphrase passphrase to unlock the private key
* @return boolean true if the file has been successfuly signed
*/
global boolean SignDetached(string keyid, string file, string passphrase)
{
return SignFile(keyid, file, passphrase, false);
}
/**
* Verify a file using a signature file. The key which has been used for signing must be imported in the keyring.
* @param sig_file file with the signature
* @param file file to verify
* @return boolean true if the file has been successfuly verified
*/
global boolean VerifyFile(string sig_file, string file)
{
map out = callGPG(sformat("--verify '%1' '%2'", String::Quote(sig_file), String::Quote(file)));
return out["exit"]:-1 == 0;
}
/**
* Export a public gpg key in ACSII armored file.
* @param keyid id of the key
* @param file the target file
* @return boolean true if the file has been successfuly signed
*/
global boolean ExportAsciiPublicKey(string keyid, string file)
{
map out = callGPG(sformat("-a --export '%1' > '%2'", String::Quote(keyid), String::Quote(file)));
return out["exit"]:-1 == 0;
}
/**
* Export a public gpg key in binary format.
* @param keyid id of the key
* @param file the target file
* @return boolean true if the file has been successfuly signed
*/
global boolean ExportPublicKey(string keyid, string file)
{
map out = callGPG(sformat("--export '%1' > '%2'", String::Quote(keyid), String::Quote(file)));
return out["exit"]:-1 == 0;
}
}
ACC SHELL 2018