From 6b7003672ff8d73d2a1f3b6709b216b3d0a7ad9c Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 10 Feb 2015 13:10:35 +0100 Subject: [PATCH] =?UTF-8?q?Add=20an=20=E2=80=98encryptString=E2=80=99=20pr?= =?UTF-8?q?imop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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’. --- src/libexpr/local.mk | 2 +- src/libexpr/primops.cc | 39 +++++++++++++++++++++ src/libutil/util.cc | 31 ++++++++++------- src/libutil/util.hh | 1 + src/nix-store/nix-store.cc | 69 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 13 deletions(-) diff --git a/src/libexpr/local.mk b/src/libexpr/local.mk index 4c1f4de1918..d19e971e71c 100644 --- a/src/libexpr/local.mk +++ b/src/libexpr/local.mk @@ -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), diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index a4efd397ec7..375c779683d 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -18,6 +18,8 @@ #include #include +#include + namespace nix { @@ -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); + 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 *************************************************************/ @@ -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"); diff --git a/src/libutil/util.cc b/src/libutil/util.cc index be0a9bf317d..d54e4785508 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -1232,18 +1232,19 @@ 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; @@ -1251,8 +1252,8 @@ string base64Decode(const string & 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; @@ -1267,4 +1268,10 @@ string base64Decode(const string & s) } +bool isBase64Char(char c) +{ + return base64Reverse[c] <= 0x3f; +} + + } diff --git a/src/libutil/util.hh b/src/libutil/util.hh index 20330fb7699..930d79e3093 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -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); } diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index c16adf04962..9c42736a485 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -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. */ @@ -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")