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 Feb 10, 2015
1 parent 1c972cb commit 6b70036
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/libexpr/local.mk
Expand Up @@ -8,7 +8,7 @@ libexpr_SOURCES := $(wildcard $(d)/*.cc) $(d)/lexer-tab.cc $(d)/parser-tab.cc

libexpr_LIBS = libutil libstore libformat

libexpr_LDFLAGS = -ldl
libexpr_LDFLAGS = -ldl $(SODIUM_LIBS)

# The dependency on libgc must be propagated (i.e. meaning that
# programs/libraries that use libexpr must explicitly pass -lgc),
Expand Down
39 changes: 39 additions & 0 deletions src/libexpr/primops.cc
Expand Up @@ -18,6 +18,8 @@
#include <cstring>
#include <dlfcn.h>

#include <sodium.h>


namespace nix {

Expand Down Expand Up @@ -1465,6 +1467,40 @@ static void prim_compareVersions(EvalState & state, const Pos & pos, Value * * a
}


/*************************************************************
* 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);

This comment has been minimized.

Copy link
@nbp

nbp Nov 11, 2016

Should we make a cache that some random bytes are associated with the hash of their inputs? This would at least remove the non-determinism from local evaluation, avoiding recompilation&restart of services which depends on a configuration file with a password.

This comment has been minimized.

Copy link
@edolstra

edolstra Nov 11, 2016

Author Owner

Associating the nonce with a hash of the input probably wouldn't provide extra security over (say) a fixed random value per user. I.e. we could generate a fixed "nonce" (a misnomer in this case) stored in ~/.config/nix/nonce, reused across evaluations.

It would be good to know what the exact security implications of reusing the nonce are. The libsodium docs say that this should never be done. But if the only implication is that an attacker can see that an encrypted value hasn't changed between evaluations, we can probably live with that. However, it also allows an attacker to see that the same value is used in different places (so for wpa_supplicant.conf, he could see that I use the same key for multiple networks). This could be prevented by including the file name and offset of the encrypted value in the nonce. However, that would require changing the interface (since encryptString doesn't know anything about file names or offsets). So maybe a function encryptFile :: keyfile -> plaintext -> storepath. I kind of liked the idea of only encrypting the sensitive parts of files, though...

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 @@ -1600,6 +1636,9 @@ void EvalState::createBaseEnv()
// Derivations
addPrimOp("derivationStrict", 1, prim_derivationStrict);

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

/* Add a wrapper around the derivation primop that computes the
`drvPath' and `outPath' attributes lazily. */
string path = findFile("nix/derivation.nix");
Expand Down
31 changes: 19 additions & 12 deletions src/libutil/util.cc
Expand Up @@ -1232,27 +1232,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 @@ -1267,4 +1268,10 @@ string base64Decode(const string & s)
}


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


}
1 change: 1 addition & 0 deletions src/libutil/util.hh
Expand Up @@ -401,6 +401,7 @@ string filterANSIEscapes(const string & s, bool nixOnly = false);
/* Base64 encoding/decoding. */
string base64Encode(const string & s);
string base64Decode(const string & s);
bool isBase64Char(char c);


}
69 changes: 69 additions & 0 deletions src/nix-store/nix-store.cc
Expand Up @@ -1034,6 +1034,71 @@ static void opGenerateBinaryCacheKey(Strings opFlags, Strings opArgs)
}


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

sodium_init();

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

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


static void opDecrypt(Strings opFlags, Strings opArgs)
{
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();

sodium_init();

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;
}


/* 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 @@ -1104,6 +1169,10 @@ 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 6b70036

Please sign in to comment.