Encrypting files with YubiKey

in #software18 days ago

Secure data storage involves, among other things, a backup copy that can be restored in the event of a failure. Perhaps you store your data on your own NAS, perhaps this is your second copy. But maybe the disk on your personal computer/server/NAS is the first copy and the backup copy is stored on someone else's computer (read: in the cloud). In this case, because you are a savvy person, all your private data on someone else's computer is encrypted by you beforehand, obviously.

Symmetric vs. asymmetric encryption

I believe the most common scenario in case of an encrypted backup is symmetric encryption. For encryption we use a password that we remember or have written down somewhere. The same password is needed to decrypt the files. Simple. Potential problems, however, include password leakage, the need to constantly type/paste them, overuse of one password for many places, or... the symmetry itself. Let's say you want to automate the backup process on your server/NAS. In order for the server to upload files to the cloud, it must encrypt them beforehand and in order to encrypt them symmetrically, it must know the password. So the password is written somewhere in there in a potentially easy to recover (steal) way. If we want to have many different passwords, we already get a set of passwords and generally the mess begins.

In asymmetric encryption we don't have a password as such, just two keys. One is the public key and the other is the private key. The public key only allows you to encrypt your data. If it leaks nothing bad happens. It is impossible to decipher data with it. It is also impossible to obtain the private key from it. This is something you could entrust to your machine without falling into paranoia of questions like "is this password really safe here". The private key, on the other hand, will be useful only when it comes to decrypting the copy (rarely). Such key is generated by a computer and it is very difficult to guess. However, it must be stored. Right… but where? In this article I'll analyse the possibility of using a YubiKey to store private keys used for asymmetric encryption. In other words: how to encrypt with YubiKey?

Requirements

Methods described here should work on various operating systems (especially on Windows and Linux). Required software:

  • OpenSSL (most Linux distros already has it installed by default, for other systems see Binaries section, on Windows it may be for example version with installer from FireDaemon - in such case mark that you want the tool to be added to your PATH variable during installation).
  • Smart cards handler. This can be OpenSC Project (https://github.com/OpenSC/OpenSC/releases) in "complete" installation (all components).
  • Software that allows uploading certificates to YubiKey. This can be YubiKey PIV Manager, YubiKey Manager or even Yubico Authenticator as it seems it has gained such functionality recently, although I haven’t tested it in this program yet.

Additional requirement is some basic shell (terminal) skills.

The big picture

Preparations (one-time actions):

  • We will generate keys on private and safe computer.
  • We will load the key(s) into YubiKey.
  • Beside that public key will remain as regular file which we will be able to put wherever we need (for example on NAS which will create encrypted copies of data it stores).

To encrypt a file:

  • We will generate long random password (different for each file).
  • With this password we will encrypt the file with data (that way we will obtain ".enc" file)
  • We will encrypt the password with a public key and save it into another file (that way we will obtain ".key" file).

To decrypt a file:

  • We will let the YubiKey decrypt the password (".key" file) with a private key it stores.
  • Using decrypted password, we will decrypt the ".enc" file.

Why that complicated? It was supposed to be asymmetrical and yet the file itself is encrypted with a symmetric password? Yes. We can use the public key to encrypt the file directly (so without a password file, without a ".key" file). Decryption won’t be that easy, though. Not only the YubiKey will not accept such a long stream of data for decryption, but even if it did, decryption would be very slow. The processor in your computer is simply devastatingly faster than the processor contained in the YubiKey. What is more, this gives us flexibility of algorithms and tools selection because only one of the steps needs to be handled by YubiKey. So the YubiKey will only deal with passwords.

A different password for each file is also an additional safeguard. Even if an attacker captures many examples of your encrypted files (".key" and ".enc") along with decrypted files (assuming you don't store decrypted passwords, because why would you), comparing them to "mine" your private key would be computationally insanely complex (read: impossible).

Preparing keys

Let's enter some empty working directory in the terminal (preferably one that other users of the operating system don't even have read access to) and execute this command:
openssl genrsa -out key.pem 2048
This will generate random private key.
Now let’s generate a public key out of it:
openssl rsa -in key.pem -outform PEM -pubout -out public.pem
YubiKey won't want the "bald" key, but the entire certificate. Let's generate it like this:
openssl req -new -x509 -key key.pem -out cacert.pem -days 9999
The program will ask for details of the certificate. They are irrelevant for our use case. You can confirm empty strings with enter. You may want to enter something as the "Common Name", though. Something that will identify this certificate or its owner (many tools can display this field showing what is uploaded to the YubiKey). The "days 9999" parameter will make the certificate valid for over 27 years. That's enough time to finish reading this article.

If everything went well, you should be able to generate a file that will contain both the public key and the certificate (these are text files so you can view their content with text editor and compare them) with the following command:
openssl x509 -inform PEM -in cacert.pem -pubkey > public-and-cacert.pem

Now we run the PIV key uploading tool from Yubico. Did I mention that your YubiKey model must support PIV?

Yubico Authenticator.png
PIV feature availibility shown in Yubico Authenticator

The tool may ask if you want to set a PIN, PUK, and key management password. Setting a key management password is a good decision because not knowing it will prevent you from replacing or deleting the certificate. This way malware won't mess with your YubiKey. You won't mess with it yourself either because once you see the request for this password, you'll realize that you're changing something and maybe you don't necessarily want to. You can also assign a PIN - then it will be needed when decrypting files. PUK helps when on forgotten PIN.

Load the generated certificate ("Import file") to the YubiKey with your GUI tool. Depending on the tool you use, the appropriate file will be "cacert.pem" or "public-and-cacert.pem". Load it to the "Key Management" slot. In my YubiKey it is also marked as "9d" but it may be different in other keys.

If everything went well you should be able to extract the public key from the YubiKey. Let's use the "pkcs11-tool.exe" tool. I'll write the syntax for Windows because it's more complicated (it requires path to ".dll" file):
"C:\Program Files\OpenSC Project\OpenSC\tools\pkcs11-tool.exe" --read-object --type pubkey --id 3 -m RSA-PKCS --module "C:\Program Files\OpenSC Project\OpenSC\pkcs11\opensc-pkcs11.dll" -o public.der
If it doesn't work, try a different "id" than 3.
This will give you a ".der" file, which can be converted to the more compatible ".pem" format like this:
openssl rsa -pubin -inform DER -in public.der -outform PEM -out public-from-yubikey.pem
This way you can also write a script that will encrypt files without having a public key file. Such script can extract it from the YubiKey itself using the two commands above.

Key generation EN.png

After all this, you need to think what to do with the private key (and/or certificate). It is already uploaded to the YubiKey but if you lose the YubiKey you will not be able to recover the content of the encrypted copies. I personally have two identical YubiKeys so I simply repeated the procedure for the second device and deleted the files from the disk ("key.pem", "cacert.pem", "public-and-cacert.pem"). In your scenario it may be more appropriate to keep a copy somewhere safe, maybe on some external hidden medium. Definitely do not send it to the cloud.

Encrypting a file

Now let's create a script that will encrypt files. You can assemble the given commands into something that suits you. I will skip preparation of the file itself (selecting it, maybe creating it, if it is to be an archive, etc.).

Let's start by generating a random password. A popular solution on Linux to generate 64 alphanumeric characters is:
tr -cd '[:alnum:]' < /dev/urandom | fold -w64 | head -n1
Since we require OpenSSL anyway, let's write something that will work on more platforms:
openssl rand 48 | base64 --wrap=0
This will generate a string of random 65 characters out of the set of alphanumeric characters, "+" and "/". We don't want to save the unencrypted password anywhere on the disk (not even for a moment), so in a Bash script we can do the following to put the password directly into a variable:
PLAIN_PASS=$(openssl rand 48 | base64 --wrap=0)

Now to encrypt and save this password we do:

PUBLIC_KEY="/root/.ssl-keys/yubikey.pem"
echo -n "$PLAIN_PASS" | openssl pkeyutl -encrypt -inkey "$PUBLIC_KEY" -pubin > output.key

In the first line we defined where the public key file is. In the second line we take out what was previously put into the "PLAIN_PASS" variable, encrypt it with the public key and save the result to the "output.key" file. This will be the first of our output files. Now it's time to encrypt the actual file.
echo -n "$PLAIN_PASS" | openssl enc -aes-256-cbc -md sha256 -iter 100000 -salt -in "$1" -pass stdin > output.enc
In place of "$1" you should insert the input file to be encrypted. If you leave it as "$1" then the file will be the first argument of the whole script.

As a result, after all this we will have two output files: "output.key" and "output.enc". We can safely put both in the cloud. They should always travel together.

Encryption EN.png

Decrypting a file

After reading the previous steps you already know what to expect here. First, we need to decrypt the password, then the file with that password. To decrypt the password, we need to ask YubiKey to do it. The "pksc11-tool" tool has a "decrypt" command for this. Again, I'll give the syntax for Windows because it's more complicated:
"C:\Program Files\OpenSC Project\OpenSC\tools\pkcs11-tool.exe" --id 3 --decrypt -m RSA-PKCS --module "C:\Program Files\OpenSC Project\OpenSC\pkcs11\opensc-pkcs11.dll" --input-file input.key
You must provide the same "id" argument that worked when you downloaded the public key. If you uploaded the certificate to the YubiKey in the "Key Management" slot, it will most likely be 3, but it may depend on the YubiKey model. Instead of "input.key", provide the input file with the encrypted password (the ".key" file). The result will be displayed on the screen so in the script you would probably want to save it to a variable or stream it to a temporary file with > (the latter is less secure).

Knowing the decrypted password we can decrypt the file:
openssl enc -d -aes-256-cbc -md sha256 -iter 100000 -in input.enc -out output
This command will take the encrypted input file with the data called "input.enc", decrypt it and save the result as the file called "output". File can be any size. Running it in this form will ask us interactively for a password. If we want to pass the password we just decrypted automatically, we need to add the "-pass" parameter. To read the password from a file (less secure) we need to add the "file:" prefix (then the path to the file with password):
openssl enc -d -aes-256-cbc -md sha256 -iter 100000 -in input.enc -out output -pass file:path_to_password_file
To read the password from a variable (better idea) you need to add the prefix "env:" (then a variable name):
openssl enc -d -aes-256-cbc -md sha256 -iter 100000 -in input.enc -out output -pass env:PLAIN_PASS

Integration with GUI tools

Manipulating with commands is all that is needed to write the appropriate scripts to perform or restore encrypted backups. Sometimes, however, it is more convenient to use graphical tools for encryption or decryption. The commands listed above can be integrated into your favourite file manager. An example of a simple integration with Directory Opus 12 or later will look like the one I have pasted at the bottom of this article. This source text should be pasted into a new text file (named e.g. "Yubikey.js"). In Notepad it is worth changing the line from "C:\\Windows\\Icons\\Yubikey 1.ico" to the path to some default icon for YubiKey encryption/decryption operations. You can find plenty of them in the Internet, mine looks like the one below.

YubiKey Icons.png

In Directory Opus the script will be installed by selecting script settings ("Script Management") then "Install" and selecting the script file. In the same window script provides (under the cog icon) options to select paths to OpenSSL and OpenSC and the correct ID number with the certificate in YubiKey (you have to use the same one as used with "pkcs11-tool"). As a result, Directory Opus will be enriched with the YubiKeyEncrypt command to encrypt files with YubiKey. When used with REVERSE parameter it can be used to decrypt files as well. This command can be used in Directory Opus by defining a new button for the file encryption/decryption function (right click on the panel and "Customize...", then right again and "New item" - "New button").

DOpus encrypt button.jpg

DOpus decrypt button.jpg

The JS (JavaScript) script:

// Global scope, so object will not be created for each item
var shell = new ActiveXObject("WScript.Shell");

var OK = 1;
var SKIP = 2;
var CANCEL = 0;
var ABORT = "a";

function OnInit(initData) {
    initData.name = Str("script_name");
    initData.desc = Str("script_desc");
    initData.version = "1.0";
    initData.copyright = "Mario";
    initData.default_enable = true;
    initData.min_version = "12.27";
    
    initData.config.openssl="C:\\Program Files\\OpenSSL";
    initData.config.opensc="C:\\Program Files\\OpenSC Project\\OpenSC";
    initData.config.pksc_id=3;
}

function OnAddCommands(addCmdData) {
    var cmd = addCmdData.AddCommand();
    cmd.name = "YubiKeyEncrypt";
    cmd.method = "OnEncrypt";
    cmd.desc = Str("cmd_desc");
    cmd.label = Str("cmd_name");
    cmd.icon = "C:\\Windows\\Icons\\Yubikey 1.ico"; // Use your own icon
    cmd.template = "REVERSE/S,HERE/S";
}

function OnEncrypt(data) {
    if (!CheckConfig()) return;
    var progress = InitProgress(data);
    
    var sslTool = GetSslTool();
    var pkscTool = GetPkscTool();
    var pkscDll = GetPkscDll();
    var pkscId = GetPkscId();
    var here = data.func.args.here; // Operate on same dir instead of destination dir
    
    var enumFiles = new Enumerator(data.func.sourcetab.selected_files);
    
    if (data.func.args.reverse) { // Decrypt
        Decrypt(data, enumFiles, progress, here, sslTool, pkscTool, pkscDll, pkscId);
    } else { // Encrypt
        var pubKey = FetchPubKeyFromYubiKey(data, sslTool, pkscTool, pkscDll, pkscId);
        if (pubKey == null) {
            Error(data, Str("error_encrypt_fail_cert_read"));
        } else {
            Encrypt(data, enumFiles, progress, here, pubKey, sslTool, pkscTool, pkscDll, pkscId);
            DeleteTempFile(pubKey);
        }
    }
    
    progress.Hide();
}

function Decrypt(data, enumFiles, progress, here, sslTool, pkscTool, pkscDll, pkscId) {
    var decrypted = DOpus.Create.StringSetI();
    
    while (!enumFiles.atEnd()) {
        var currentFile = enumFiles.item();
        if (currentFile.is_dir) {
            Error(data, Str("error_decrypt_dir"));
            data.func.command.RemoveFile(currentFile);
            return;
        }
        currentFile = currentFile.realpath;
        
        progress.SetName(currentFile);
        progress.Show();
        if (progress.GetAbortState(true, ABORT, true) == ABORT) {
            return;
        }
        
        var encFile = ToEncFile(currentFile);
        if (encFile == null) {
            Error(data, Str("error_decrypt_no_enc") + currentFile.filepart);
            data.func.command.RemoveFile(currentFile);
        } else if (decrypted.insert(encFile)) { // Don't decrypt twice
            var keyFile = ToKeyFile(currentFile);
            if (keyFile == null) {
                Error(data, Str("error_decrypt_no_key") + currentFile.filepart);
                data.func.command.RemoveFile(currentFile);
            } else {
                if (here || data.func.desttab.path == null) {
                    var outFile = DOpus.FSUtil.NewPath(currentFile.pathpart);
                } else {
                    var outFile = DOpus.FSUtil.NewPath(data.func.desttab.path);
                }
                outFile.Add(currentFile.stem);
                
                if (progress.GetAbortState(true, ABORT, true) == ABORT) {
                    return;
                }
                
                var replace = OK;
                if (DOpus.FSUtil.Exists(outFile)) {
                    replace = AskReplace(data, outFile);
                    if (replace == CANCEL) {
                        return;
                    } else if (replace == SKIP) {
                        data.func.command.RemoveFile(currentFile);
                    }
                }
                
                if (progress.GetAbortState(true, ABORT, true) == ABORT) {
                    return;
                }
                
                if (replace == OK) { // OK to replace or dest file is not there yet
                    var tempFile = GetTempFile();
                    
                    // Decrypt key
                    var result = Execute('"' + pkscTool + '" --id ' + pkscId
                        + ' --decrypt -m RSA-PKCS --module "' + pkscDll + '" --input-file "'
                        + keyFile + '" --output-file "' + tempFile + '"');
                        
                    if (!result) {
                        Error(data, Str("error_decrypt_fail_key") + currentFile.filepart);
                        data.func.command.RemoveFile(currentFile);
                    } else if (progress.GetAbortState(false, ABORT, true) != ABORT) {
                        // Decrypt data
                        ExecuteSilent(data.func.command, '"' + sslTool
                            + '" enc -d -aes-256-cbc -md sha256 -iter 100000 -in "' + encFile
                            + '" -out "' + outFile + '" -pass "file:' + tempFile + '"');
                    }
                    
                    DeleteTempFile(tempFile);
                }
            }
        }
        
        enumFiles.moveNext();
        progress.StepFiles(1);
    }
}

function Encrypt(data, enumFiles, progress, here, pubKey, sslTool, pkscTool, pkscDll, pkscId) {
    while (!enumFiles.atEnd()) {
        var currentFile = enumFiles.item();
        if (currentFile.is_dir) {
            Error(data, Str("error_encrypt_dir"));
            data.func.command.RemoveFile(currentFile);
            return;
        }
        currentFile = currentFile.realpath;
        
        progress.SetName(currentFile);
        progress.Show();
        if (progress.GetAbortState(true, ABORT, true) == ABORT) {
            return;
        }
        
        if (here || data.func.desttab.path == null) {
            var encFile = DOpus.FSUtil.NewPath(currentFile.pathpart);
            var keyFile = DOpus.FSUtil.NewPath(currentFile.pathpart);
        } else {
            var encFile = DOpus.FSUtil.NewPath(data.func.desttab.path);
            var keyFile = DOpus.FSUtil.NewPath(data.func.desttab.path);
        }
        encFile.Add(currentFile.filepart + ".enc");
        keyFile.Add(currentFile.filepart + ".key");
                                
        var replace = OK;
        if (DOpus.FSUtil.Exists(encFile)) {
            replace = AskReplace(data, encFile);
            if (replace == CANCEL) {
                return;
            } else if (replace == SKIP) {
                data.func.command.RemoveFile(currentFile);
            }
        }
        if (progress.GetAbortState(true, ABORT, true) == ABORT) {
            return;
        }
        if (replace == OK) { // OK to replace or dest ENC file is not there yet
            if (DOpus.FSUtil.Exists(keyFile)) {
                replace = AskReplace(data, keyFile);
                if (replace == CANCEL) {
                    return;
                } else if (replace == SKIP) {
                    data.func.command.RemoveFile(currentFile);
                }
            }
            if (progress.GetAbortState(true, ABORT, true) == ABORT) {
                return;
            }
            
            if (replace == OK) { // OK to replace or dest KEY file is not there yet
                var plainKeyFile = GetRandomTempFile(data);
                
                // Encrypt key
                ExecuteSilent(data.func.command, '"' + sslTool + '" pkeyutl -encrypt -inkey "'
                    + pubKey + '" -pubin -in "' + plainKeyFile + '" -out "' + keyFile + '"');
                    
                if (!DOpus.FSUtil.Exists(keyFile) || IsEmptyFile(keyFile)) {
                    Error(data, Str("error_encrypt_fail_key") + "\n" + keyFile);
                } else if (progress.GetAbortState(false, ABORT, true) != ABORT) {
                    // Encrypt data
                    ExecuteSilent(data.func.command, '"' + sslTool + '" enc -aes-256-cbc '
                        + '-md sha256 -iter 100000 -salt -in "' + currentFile + '" -pass file:"'
                        + plainKeyFile + '" -out "' + encFile + '"');
                }
                
                DeleteTempFile(plainKeyFile);
            }
        }
        
        enumFiles.moveNext();
        progress.StepFiles(1);
    }
}

function CheckConfig() {
    var sslTool = GetSslTool();
    var pkscTool = GetPkscTool();
    var pkscDll = GetPkscDll();
    var pkscId = GetPkscId();
    
    if (!DOpus.FSUtil.Exists(sslTool)) {
        DOpus.Output(Str("error_config_no_file") + sslTool, true);
        return false;
    }
    if (!DOpus.FSUtil.Exists(pkscTool)) {
        DOpus.Output(Str("error_config_no_file") + pkscTool, true);
        return false;
    }
    if (!DOpus.FSUtil.Exists(pkscDll)) {
        DOpus.Output(Str("error_config_no_file") + pkscDll, true);
        return false;
    }
    if (pkscId < 0 || pkscId > 10) {
        DOpus.Output(Str("error_config_invalid_id") + pkscId, true);
        return false;
    }
    return true;
}

function InitProgress(data) {
    var progress = data.func.command.progress;
    progress.abort = true;
    progress.bytes = false;
    progress.delay = true;
    progress.pause = true;
    progress.skip = false;
    progress.full = false;
    progress.Init(data.func.sourcetab);
    progress.SetFiles(data.func.sourcetab.selstats.selfiles);
    progress.SetTitle("YubiKey");
    progress.SetStatus("");
    return progress;
}

function GetSslTool() {
    return Script.config.openssl + "\\bin\\openssl.exe";
}

function GetPkscTool() {
    return Script.config.opensc + "\\tools\\pkcs11-tool.exe";
}

function GetPkscDll() {
    return Script.config.opensc + "\\pkcs11\\opensc-pkcs11.dll";
}

function GetPkscId() {
    return Script.config.pksc_id;
}

function GetTempFile() {
    return DOpus.FSUtil.GetTempFilePath(".tmp","script-yubikey-")
}

function GetRandomTempFile(data) {
    var tempFile = DOpus.FSUtil.GetTempFile(".tmp", "script-yubikey-", "r", data.func.sourcetab);
    tempFile.Write(RandomString(64));
    tempFile.Close();
    return tempFile.path;
}

function RandomString(length) {
    var result = '';
    var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    var charactersLength = characters.length;
    for (var i = 0; i < length; i++) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
}

function DeleteTempFile(path) {
    cmdLine = 'Delete QUIET NORECYCLE FILE="' + path + '"';
    DOpus.Create().Command().RunCommand(cmdLine);
}

function ExecuteSilent(cmd, cmdLine) {
    cmd.SetModifier("runmode", "hide");
    cmd.RunCommand('@sync:' + cmdLine);
    cmd.ClearModifier("runmode");
}

function Execute(cmdLine) {
    return shell.Run(cmdLine,1,true) == 0;
}

function ToEncFile(path) {
    if (path.ext == ".enc") {
        return path;
    }
    if (path.ext == ".key") {
        var encFile = DOpus.FSUtil.NewPath(path.pathpart);
        encFile.Add(path.stem + ".enc");
        if (DOpus.FSUtil.Exists(encFile)) {
            return encFile;
        }
    }
    return null;
}

function ToKeyFile(path) {
    if (path.ext == ".key") {
        return path;
    }
    if (path.ext == ".enc") {
        var keyFile = DOpus.FSUtil.NewPath(path.pathpart);
        keyFile.Add(path.stem + ".key");
        if (DOpus.FSUtil.Exists(keyFile)) {
            return keyFile;
        }
    }
    return null;
}

function FetchPubKeyFromYubiKey(data, sslTool, pkscTool, pkscDll, pkscId) {
    var derCert = GetTempFile();
    ExecuteSilent(data.func.command, '"' + pkscTool + '" --id ' + pkscId
        + ' --read-object --type pubkey -m RSA-PKCS --module "' + pkscDll
        + '" -o "' + derCert + '"');
    if (!DOpus.FSUtil.Exists(derCert) || IsEmptyFile(derCert)) {
        return null;
    }
    
    var pemCert = GetTempFile();
    ExecuteSilent(data.func.command, '"' + sslTool + '" rsa -pubin -inform DER -in "'
        + derCert + '" -outform PEM -out "' + pemCert + '"');
    DeleteTempFile(derCert);
    if (!DOpus.FSUtil.Exists(pemCert) || IsEmptyFile(pemCert)) {
        return null;
    }
    
    return pemCert;
}

function IsEmptyFile(path) {
    return DOpus.FSUtil.GetItem(path).size == "0";
}

function Str(str) {
    return DOpus.strings.Get(str);
}

function Error(data, message) {
    var dlg = data.func.Dlg();
    dlg.icon = "error";
    dlg.message = message;
    dlg.window = data.func.sourcetab;
    dlg.buttons = "&OK";
    dlg.Show();
}

function AskReplace(data, destination) {
    var dlg = data.func.Dlg();
    dlg.icon = "question";
    dlg.message = Str("error_file_exists") + "\n" + destination + "\n" + Str("question_replace");
    dlg.window = data.func.sourcetab;
    dlg.buttons = Str("button_replace") + "|" + Str("button_skip") + "|" + Str("button_cancel");
    return dlg.Show();
}

==SCRIPT RESOURCES

<resources>
    <resource type="strings">
        <strings lang="english">
            <string id="script_name" text="Yubikey files encryption" />
            <string id="script_desc" text="Adds a command to encrypt and decrypt files with use of YubiKey" />
            <string id="cmd_name" text="Encrypt or decrypt selected files" />
            <string id="cmd_desc" text="Encrypt or decrypt selected files using YubiKey PIV" />
            <string id="error_encrypt_fail_cert_read" text="Failed on fetching public key out of YubiKey" />
            <string id="error_encrypt_fail_key" text="Failed on creating .key file in location:" />
            <string id="error_encrypt_dir" text="Cannot encrypt directories" />
            <string id="error_decrypt_dir" text="Cannot decrypt directories" />
            <string id="error_decrypt_no_key" text="Cannot find .key file to decrypt for: " />
            <string id="error_decrypt_no_enc" text="Cannot find .enc file to decrypt for: " />
            <string id="error_decrypt_fail_key" text="Cannot decrypt .key file for: " />
            <string id="error_config_no_file" text="Cannot find: " />
            <string id="error_config_invalid_id" text="Invalid ID configured: " />
            <string id="error_file_exists" text="Destination file already exists in location:" />
            <string id="question_replace" text="Replace it?" />
            <string id="button_replace" text="&Replace" />
            <string id="button_skip" text="&Skip" />
            <string id="button_cancel" text="&Cancel" />
        </strings>
        <strings lang="polski">
            <string id="script_name" text="Szyfrowanie z Yubikey" />
            <string id="script_desc" text="Dodaje komendę do szyfrowania i deszyfrowania plików z użyciem YubiKey" />
            <string id="cmd_name" text="Szyfruj lub deszyfruj wybrane pliki" />
            <string id="cmd_desc" text="Szyfruj lub deszyfruj wybrane pliki z użyciem YubiKey PIV" />
            <string id="error_encrypt_fail_cert_read" text="Nie powiodło się pobieranie klucza publicznego z urządzenia YubiKey" />
            <string id="error_encrypt_fail_key" text="Nie udało się utworzyć pliku .key w lokacji:" />
            <string id="error_encrypt_dir" text="Nie można szyfrować katalogów" />
            <string id="error_decrypt_dir" text="Nie można rozszyfrowywać katalogów" />
            <string id="error_decrypt_no_key" text="Nie znalazłem pliku .key do rozszyfrowania dla: " />
            <string id="error_decrypt_no_enc" text="Nie znalazłem pliku .enc do rozszyfrowania dla: " />
            <string id="error_decrypt_fail_key" text="Nie udało się rozszyfrowanie pliku .key dla: " />
            <string id="error_config_no_file" text="Nie znalazłem: " />
            <string id="error_config_invalid_id" text="Skonfigurowano niepoprawny ID: " />
            <string id="error_file_exists" text="Plik docelowy już istnieje w lokacji:" />
            <string id="question_replace" text="Zastąpić go?" />
            <string id="button_replace" text="&Zastąp" />
            <string id="button_skip" text="P&omiń" />
            <string id="button_cancel" text="&Przerwij" />
        </strings>
    </resource>
</resources>