Skip to content

Commit

Permalink
Add an ‘encryptString’ primop
Browse files Browse the repository at this point in the history
This can be used for generating world-readable configuration files
containing secrets (such as passwords). ‘encryptString keyFile str’
encrypts the string ‘str’ using the key stored in ‘keyFile’. Keys can
be generated using ‘nix-store --generate-key’. Files containing
encrypted strings can be decrypted using ‘nix-store --decrypt’.
  • Loading branch information
edolstra committed Jan 31, 2019
1 parent 92d08c0 commit 10053c5
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/libexpr/local.mk
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ libexpr_SOURCES := $(wildcard $(d)/*.cc) $(wildcard $(d)/primops/*.cc) $(d)/lexe

libexpr_LIBS = libutil libstore

libexpr_LDFLAGS =
libexpr_LDFLAGS = $(SODIUM_LIBS)
ifneq ($(OS), FreeBSD)
libexpr_LDFLAGS += -ldl
endif
Expand Down
39 changes: 39 additions & 0 deletions src/libexpr/primops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#include <regex>
#include <dlfcn.h>

#include <sodium.h>


namespace nix {

Expand Down Expand Up @@ -2133,6 +2135,40 @@ static void prim_fetchTarball(EvalState & state, const Pos & pos, Value * * args
}


/*************************************************************
* Cryptography
*************************************************************/


/* Encrypt a string using a key stored in a file. */
static void prim_encryptString(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
PathSet context;
Path path = state.coerceToPath(pos, *args[0], context);
if (!context.empty())
throw EvalError(format("string '%1%' cannot refer to other paths, at %2%") % path % pos);

// FIXME: lock/wipe the key in memory.
string key = base64Decode(readFile(path));
if (key.size() != crypto_secretbox_KEYBYTES)
throw Error(format("file '%1%' does not contain a key created using 'nix-store --generate-key'") % path);

string s = state.forceStringNoCtx(*args[1], pos);

/* Note: since the nonce is random, encryptString is
non-deterministic, which is unfortunate... */
string nonce(crypto_secretbox_NONCEBYTES, 0);
randombytes_buf((unsigned char *) nonce.data(), nonce.size());

string res(crypto_secretbox_MACBYTES + s.size(), ' ');
if (crypto_secretbox_easy((unsigned char *) res.data(), (unsigned char *) s.data(),
s.size(), (unsigned char *) nonce.data(), (unsigned char *) key.data()) != 0)
throw Error("encryption failed");

mkString(v, "<{|nixcrypt:" + base64Encode(nonce + res) + "|}>");
}


/*************************************************************
* Primop registration
*************************************************************/
Expand Down Expand Up @@ -2321,6 +2357,9 @@ void EvalState::createBaseEnv()
addPrimOp("__fetchurl", 1, prim_fetchurl);
addPrimOp("fetchTarball", 1, prim_fetchTarball);

// Cryptography
addPrimOp("__encryptString", 2, prim_encryptString);

/* Add a wrapper around the derivation primop that computes the
`drvPath' and `outPath' attributes lazily. */
string path = canonPath(settings.nixDataDir + "/nix/corepkgs/derivation.nix", true);
Expand Down
10 changes: 9 additions & 1 deletion src/libstore/crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ SecretKey::SecretKey(const string & s)
}

#if !HAVE_SODIUM
[[noreturn]] static void noSodium()
[[noreturn]] void noSodium()
{
throw Error("Nix was not compiled with libsodium, required for signed binary cache support");
}
Expand Down Expand Up @@ -123,4 +123,12 @@ PublicKeys getDefaultPublicKeys()
return publicKeys;
}

void initCrypto()
{
#if HAVE_SODIUM
if (sodium_init() == -1)
throw Error("could not initialise libsodium");
#endif
}

}
4 changes: 4 additions & 0 deletions src/libstore/crypto.hh
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,8 @@ bool verifyDetached(const std::string & data, const std::string & sig,

PublicKeys getDefaultPublicKeys();

void initCrypto();

[[noreturn]] void noSodium();

}
31 changes: 19 additions & 12 deletions src/libutil/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1357,27 +1357,28 @@ string base64Encode(const string & s)
}


string base64Decode(const string & s)
string reverseBase64Chars()
{
bool init = false;
char decode[256];
if (!init) {
// FIXME: not thread-safe.
memset(decode, -1, sizeof(decode));
for (int i = 0; i < 64; i++)
decode[(int) base64Chars[i]] = i;
init = true;
}
string s(256, 0x40);
for (int i = 0; i < 64; i++)
s[(int) base64Chars[i]] = i;
return s;
}

string base64Reverse = reverseBase64Chars();


string base64Decode(const string & s)
{
string res;
unsigned int d = 0, bits = 0;

for (char c : s) {
if (c == '=') break;
if (c == '\n') continue;

char digit = decode[(unsigned char) c];
if (digit == -1)
char digit = base64Reverse[(unsigned char) c];
if (digit > 0x3f)
throw Error("invalid character in Base64 string");

bits += 6;
Expand All @@ -1392,6 +1393,12 @@ string base64Decode(const string & s)
}


bool isBase64Char(char c)
{
return base64Reverse[c] <= 0x3f;
}


void callFailure(const std::function<void(std::exception_ptr exc)> & failure, std::exception_ptr exc)
{
try {
Expand Down
1 change: 1 addition & 0 deletions src/libutil/util.hh
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ std::string filterANSIEscapes(const std::string & s,
/* Base64 encoding/decoding. */
string base64Encode(const string & s);
string base64Decode(const string & s);
bool isBase64Char(char c);


/* Get a value for the specified key from an associate container, or a
Expand Down
83 changes: 80 additions & 3 deletions src/nix-store/nix-store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "worker-protocol.hh"
#include "graphml.hh"
#include "legacy.hh"
#include "crypto.hh"

#include <iostream>
#include <algorithm>
Expand Down Expand Up @@ -968,8 +969,7 @@ static void opGenerateBinaryCacheKey(Strings opFlags, Strings opArgs)
string publicKeyFile = *i++;

#if HAVE_SODIUM
if (sodium_init() == -1)
throw Error("could not initialise libsodium");
initCrypto();

unsigned char pk[crypto_sign_PUBLICKEYBYTES];
unsigned char sk[crypto_sign_SECRETKEYBYTES];
Expand All @@ -980,7 +980,7 @@ static void opGenerateBinaryCacheKey(Strings opFlags, Strings opArgs)
umask(0077);
writeFile(secretKeyFile, keyName + ":" + base64Encode(string((char *) sk, crypto_sign_SECRETKEYBYTES)));
#else
throw Error("Nix was not compiled with libsodium, required for signed binary cache support");
noSodium();
#endif
}

Expand All @@ -991,6 +991,79 @@ static void opVersion(Strings opFlags, Strings opArgs)
}


static void opGenerateKey(Strings opFlags, Strings opArgs)
{
#if HAVE_SODIUM
if (!opFlags.empty()) throw UsageError("no flags expected");
if (!opArgs.empty()) throw UsageError("no arguments expected");

initCrypto();

unsigned char key[crypto_secretbox_KEYBYTES];
randombytes_buf(key, sizeof key);

std::cout << base64Encode(string((char *) key, crypto_secretbox_KEYBYTES)) << std::endl;
#else
noSodium();
#endif
}


static void opDecrypt(Strings opFlags, Strings opArgs)
{
#if HAVE_SODIUM
if (!opFlags.empty()) throw UsageError("no flags expected");
if (opArgs.size() != 2) throw UsageError("two arguments expected");
string keyFile = opArgs.front(); opArgs.pop_front();
string inFile = opArgs.front();

initCrypto();

string in = readFile(inFile), out, startMarker = "<{|nixcrypt:";

for (size_t pos = 0; pos < in.size(); ) {
if (string(in, pos, startMarker.size()) != startMarker) {
out += in[pos++];
continue;
}

size_t begin = pos + startMarker.size(), end = begin;
for (; end < in.size() && (isBase64Char(in[end]) || in[end] == '='); end++) ;
if (string(in, end, 3) != "|}>") {
out += string(in, begin, end - begin);
pos = end;
continue;
}

string b64 = string(in, begin, end - begin);
string decoded = base64Decode(b64);

if (decoded.size() < crypto_secretbox_NONCEBYTES + crypto_secretbox_MACBYTES)
throw Error(format("encrypted data in '%1%' lacks nonce or MAC") % inFile);
string nonce(decoded, 0, crypto_secretbox_NONCEBYTES);
string encrypted(decoded, crypto_secretbox_NONCEBYTES);

// FIXME: lock/wipe the key in memory.
string key = base64Decode(readFile(keyFile));
if (key.size() != crypto_secretbox_KEYBYTES)
throw Error(format("file '%1%' does not contain a key created using 'nix-store --generate-key'") % keyFile);

string decrypted(encrypted.size() - crypto_secretbox_MACBYTES, 0);
if (crypto_secretbox_open_easy((unsigned char *) decrypted.data(), (unsigned char *) encrypted.data(),
encrypted.size(), (unsigned char *) nonce.data(), (unsigned char *) key.data()) != 0)
throw Error(format("unable to decrypt data in '%1%'") % inFile);
out += decrypted;

pos = end + 3;
}

std::cout << out;
#else
noSodium();
#endif
}


/* Scan the arguments; find the operation, set global flags, put all
other flags in a list, and put all other arguments in another
list. */
Expand Down Expand Up @@ -1055,6 +1128,10 @@ static int _main(int argc, char * * argv)
op = opServe;
else if (*arg == "--generate-binary-cache-key")
op = opGenerateBinaryCacheKey;
else if (*arg == "--generate-key")
op = opGenerateKey;
else if (*arg == "--decrypt")
op = opDecrypt;
else if (*arg == "--add-root")
gcRoot = absPath(getArg(*arg, arg, end));
else if (*arg == "--indirect")
Expand Down

0 comments on commit 10053c5

Please sign in to comment.