Giter Club home page Giter Club logo

kr's Introduction

kr : a simple file encryption/decryption tool.

kr is a simple file encryption/decryption program based on Monocypher. Under the hood, it uses the incremental AEAD interface of Monocypher to encrypt/decrypt files using XChaCha20-Poly1305.

kr offers two modes of operation:

  • Keyfile-based: a private key is stored on the user's machine and is used to encrypt and decrypt files.

  • Passphrase-based: an encryption/decryption key is generated, on the fly, using Argon2i (with a random salt).

When using keyfiles, kr can help you generate either random or deterministic keyfiles (based on a passphrase and a uid). See Keyfiles Management below.

Installation

Clone this repository, then run:

$ make install

By default, this will install the kr program to /usr/local/bin, and the manual page in /usr/local/share/man/man1 You can change that by adding PREFIX=~/.local (for example) to the previous command.

Usage:

Invoking kr with the -h (or --help) option will give you a summary of its usage. Read on for more details.

As stated above, kr may be used either with passphrases or keyfiles. Before we dive into the encryption/decryption operations, let us explore keyfiles management first.

Keyfiles Management

kr is based on symmetric private keys. These keys may be totally random or deterministic (predictable). Both types of keys serve to encrypt and decrypt, and may be protected with a passphrase if the user wants to.

Random Keyfiles Generation -g

To generate a random key, one can simply invoke kr as follows:

$ kr -g ~/key.sec

The user is, then, prompted to type a passphrase if they want to protect the generated key.

Deterministic Keyfiles Generation -g -u

Instead of storing private keys on disk and carrying them from a machine to another, one can opt to generate them on the spot every time they need them. Indeed, given a passphrase and a unique user ID (a simple string such as a username, an email address, etc.), kr will generate the same key for the same pair (passphrase, userID) on every invocation. For example:

$ kr -guUSERID -p"PASS PHRASE" ~/key.sec

Or

$ kr -g --uid=USERID --passphrase="PASS PHRASE" ~/key.sec

kr will use the uid USERIDand the passphrase "PASS PHRASE" to generate a key that will be stored in ~/key.sec. Note that if the passphrase is not specified in the command above, kr will prompt the user to type it (and confirm it). Long options are also available as follows:

$ kr --generate --uid=USERID --passphrase="PASS PHRASE" ~/key.sec

Note that if the output keyfile is not provided in the two commands above, the key will be output to stdout.

Editing Keyfiles -m

The user can add, edit, or remove the protection passphrase of a given keyfile simply by using the --edit (or -m) option as follows:

$ kr -m ~/key.sec

Encryption/Decryption

Now, to the core feature of kr: encryption and decryption. These can be done with either keyfiles or passphrases.

Keyfile-based Encryption/Decryption

Encryption -e -k
$ kr -e -k ~/.key.sec inputfile outputfile

Encrypts the input file inputfile using the key stored in ~/.key.sec and puts the output in outputfile (or stdout if no output file is provided in the command above).

We can also use the long options:

$ kr --encrypt --keyfile ~/.key.sec inputfile outputfile
Decryption -d -k
$ kr -d -k ~/.key.sec inputfile outputfile

Decrypts the encrypted input file inputfile using the key stored in ~/.key.sec and puts the output in outputfile. (or stdout if no output file is provided in the command above)

Or using the long options:

$ kr --decrypt --keyfile ~/.key.sec inputfile outputfile

Passphrase-based Encryption/Decryption

Instead of using key files, we can use passphrases as follows:

Encryption -e -p
$ kr -e -p"PASS PHRASE" inputfile outputfile

Encrypts the input file inputfile using the passphrase "PASS PHRASE" and puts the output in outputfile (or stdout if no output file is provided in the command above). Note that you are prompted to type a passphrase (and confirm it) if the passphrase is not provided in the command line above.

We can also use the long options:

$ kr --encrypt --passphrase="PASS PHRASE" inputfile outputfile
Decryption -d -p
$ kr -d -p"PASS PHRASE" inputfile outputfile

Decrypts the encrypted input file inputfile using the passphrase "PASS PHRASE" and puts the output in outputfile. (or stdout if no output file is provided in the command above). Note that you are prompted to type a passphrase if the passphrase is not provided in the command line above.

Or using the long options:

$ kr --decrypt --passphrase="PASS PHRASE" inputfile outputfile

Support for streams

kr is able to process streams as well. For instance, these examples with pipes:

$ echo 'Hello, world!' | kr -epPASS | kr -dpPASS

or with keyfiles

$ echo 'Hello, world!' | kr -ek ~/.key.sec | kr -dk ~/.key.sec

will output, as you might have guessed it, the string Hello, world!.

A note on passphrases: It is not advised to use passphrases in clear in the command line. They will most probably be stored in your shell's history. Thus, leaving the -p option empty and making the program prompt you to type the passphrase is preferable. Moreover, it is often advised to use randomly generated passphrases (such as those made using the Diceware technique).

Disclaimer and thanks

I am not a cryptologist. This code is written for my own use. Use it at your own risk. It is provided as is, without warranty of any kind, and put in the public domain (please see the UNLICENSE file). Your contributions, ideas, fixes, and suggestions are most welcome.

A special thank you! goes to:

  • @LoupVaillant and the contributors to the Monocypher project.
  • @skeeto for his awesome C code of which I borrowed (and learned a lot). Namely, kr started as an imitation of his monocrypt project, and uses Optparse and two other functions (read_password and fillrand).

kr's People

Contributors

chkoreff avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

kr's Issues

Consider storing password hash parameters in the key file

Evidenced in gen_key() function, though it's a more general problem with the file format.

The problem here is that password hashing is a rather fast moving field, where the relatively weak entropy of passwords encourage us to routinely update the strength of our password hashes, or even the algorithm itself. And depending on context (are you using your beefy desktop or your crappy palmtop?) you may want to adjust the password strength. Version numbers are insufficient to deal with such a fast evolution, or the various use cases. Which is why I believe it is worth putting at least Argon2 parameters in the file format itself:

  • At least 1 bit to chose between Argon2i and Argon2id, though I would recommend a full byte for (i) easy of coding, and (ii) having some room to use other password hashing algorithms. Note : while I used to favour Argon2i, and still believe the case for Argon2id is quite flimsy, I'm now mostly uncertain about which is best, most notably because this is so highly dependent on the threat model: how much do you fear timing attacks, vs just having your key file stolen and attacked offline?
  • At least 7 bits for the number of passes.
  • 32 bits for the number of blocks.
  • At least 4 bits for the number of lanes. Or make it 8, we never know when we'll get those 256 core CPUs. Or give up and just use 1 lane, I don't think it's the most important parameter.

Speaking of lanes, I recall some papers noting that parallel versions of Argon2 are a bit weaker than just running several serial instances in parallel (with different additional data to distinguish them), then hashing the results together. So if you use a parallelism parameter, you should probably do that instead of using Argon2 lanes (whose support is not universal anyway, see Monocypher not supporting threads, and libsodium not even supporting lanes).


Of course, nothing here stops you from setting good defaults, or even hard code the parameters you encode into the key file. You can always add options or change the defaults later, as long as the file format itself (and the reader side) is prepared for the change.

Stylistic suggestion: macros to shorten standard error handling patterns

I noticed a couple recurring patterns:

if (!condition) {
    BAIL(ERROR_CODE);
}

With a little macro those 3 lines could be reduced to just one:

#define CHECK(condition, ERROR_CODE) do { \
        if (!condition) {                 \
            BAIL(ERROR_CODE);             \
        }                                 \
    } while (0)

// ...

CHECK(condition, ERROR_CODE);

You could stop there. But I noticed another, quite frequent sub-pattern:

err = function(arg1, arg2, arg3);
if (err != ERR_OK) {
    BAIL(err);
}

This one could also be reduced to a single line. (Gcc and Clang support having zero optional argument, but they may complain about it not being standard).

#define CHECKED_CALL(function, ...) do { \
        err = function(__VA_ARGS__);     \
        if (err != ERR_OK) {             \
            BAIL(err);                   \
        }                                \
    } while (0)

// ...

CHECKED_CALL(function, arg1, arg2, arg3);
CHECKED_CALL(function2); // zero optional arguments is not standard unfortunately

Or you can avoid __VA_ARGS__ altogether by wrapping the function call itself:

#define CHECKED_EXPR(expr) do { \
        err = expr;             \
        if (err != ERR_OK) {    \
            BAIL(err);          \
        }                       \
    } while (0)

// ...

CHECKED_EXPR(function(arg1, arg2, arg3));
CHECKED_EXPR(function2()); // perfectly standard !

Now many would say this is too many macros. Or the macros are too heavy, or it's unmaintainable because they're macros… Perhaps. It's a balance between compactness and approachability. My guess here is that those macros are worth the trouble, because they are part of a consistent error handling system that pervades the whole program. The patterns they replace are everywhere, so the 30 seconds spent on understanding them are quickly saved when reading the rest of the program.

And compactness in my opinion doesn't just make the code shorter, it makes it more readable: less error handling noise, more business logic, and with shorter function there's a greater chance that the exit point is visible in the same screen as the BAIL() call.

Now that's the kind of thing that should be tested, see if it suits your taste. I've tested this on other projects and it certainly suits mine. 😄

Consider replacing /dev/urandom by a system/library call when possible

Spotted here.

The problem with /dev/urandom is that it's a file. Many errors specific to reading files could happen, up to and including failing to fill the buffer. Even if we assume the return code is enough to detect all errors (which may be a safe assumption, I won't bet my hat), returning an error code is the wrong API in my opinion: what's the user to do when faced with the failure something as basic as the RNG? In practice I believe the only choice is to panic with a suitable error message.

What I'm saying is, what you really want is an interface like this:

void fillrandom(uint8_t *buf, size_t len);

(Note my preference for size_t when it comes down to buffer sizes. Others would use ssize_t instead. The point is to use a type that can describe the whole range of possible sizes, and those are it. Though of course in practice you'll rarely do that on huge buffers, so if you're crystal clear that the admissible range is small, int or an unsigned can be enough.)

That way users don't have to, and cannot forget to, check the return value. fillrandom() will either successfully complete, or panic. Now panicking is not nice, so we want to minimise that ever happening. On non-Windows systems my answer would be:

  • If arc4random_buf(3) is available, use it: it does exactly what we want and virtually never crashes.
  • On Linux systems unlucky enough to lack arc4random_buf(3), try getrandom(2) instead. This one can fail, so you may want to read the exact error and panic or retry accordingly.
  • If your system is lacking any sort of system call to get randomness, either use /dev/urandom or (and I would consider it a valid approach in 2023) refuse to compile altogether: properly handling file related errors and retries is no picnic. @fscoto once showed me a soberingly long example.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.