Make key files extensible, store key name in key file

Storing the key name in the key file makes it unnecessary to pass the
--key-name option to git-crypt unlock.

This breaks compatibility with post-revamp keys.  On the plus side,
keys are now extensible so in the future it will be easier to make
changes to the format without breaking compatibility.
This commit is contained in:
Andrew Ayer
2014-06-29 21:54:28 -07:00
parent 3c8273cd4b
commit 3511033f7f
3 changed files with 206 additions and 33 deletions

166
key.cpp
View File

@@ -40,9 +40,69 @@
#include <sstream>
#include <cstring>
#include <stdexcept>
#include <vector>
Key_file::Entry::Entry ()
{
version = 0;
std::memset(aes_key, 0, AES_KEY_LEN);
std::memset(hmac_key, 0, HMAC_KEY_LEN);
}
void Key_file::Entry::load (std::istream& in)
{
while (true) {
uint32_t field_id;
if (!read_be32(in, field_id)) {
throw Malformed();
}
if (field_id == KEY_FIELD_END) {
break;
}
uint32_t field_len;
if (!read_be32(in, field_len)) {
throw Malformed();
}
if (field_id == KEY_FIELD_VERSION) {
if (field_len != 4) {
throw Malformed();
}
if (!read_be32(in, version)) {
throw Malformed();
}
} else if (field_id == KEY_FIELD_AES_KEY) {
if (field_len != AES_KEY_LEN) {
throw Malformed();
}
in.read(reinterpret_cast<char*>(aes_key), AES_KEY_LEN);
if (in.gcount() != AES_KEY_LEN) {
throw Malformed();
}
} else if (field_id == KEY_FIELD_HMAC_KEY) {
if (field_len != HMAC_KEY_LEN) {
throw Malformed();
}
in.read(reinterpret_cast<char*>(hmac_key), HMAC_KEY_LEN);
if (in.gcount() != HMAC_KEY_LEN) {
throw Malformed();
}
} else if (field_id & 1) { // unknown critical field
throw Incompatible();
} else {
// unknown non-critical field - safe to ignore
in.ignore(field_len);
if (in.gcount() != field_len) {
throw Malformed();
}
}
}
}
void Key_file::Entry::load_legacy (uint32_t arg_version, std::istream& in)
{
version = arg_version;
// First comes the AES key
in.read(reinterpret_cast<char*>(aes_key), AES_KEY_LEN);
if (in.gcount() != AES_KEY_LEN) {
@@ -58,12 +118,28 @@ void Key_file::Entry::load (std::istream& in)
void Key_file::Entry::store (std::ostream& out) const
{
// Version
write_be32(out, KEY_FIELD_VERSION);
write_be32(out, 4);
write_be32(out, version);
// AES key
write_be32(out, KEY_FIELD_AES_KEY);
write_be32(out, AES_KEY_LEN);
out.write(reinterpret_cast<const char*>(aes_key), AES_KEY_LEN);
// HMAC key
write_be32(out, KEY_FIELD_HMAC_KEY);
write_be32(out, HMAC_KEY_LEN);
out.write(reinterpret_cast<const char*>(hmac_key), HMAC_KEY_LEN);
// End
write_be32(out, KEY_FIELD_END);
}
void Key_file::Entry::generate ()
void Key_file::Entry::generate (uint32_t arg_version)
{
version = arg_version;
random_bytes(aes_key, AES_KEY_LEN);
random_bytes(hmac_key, HMAC_KEY_LEN);
}
@@ -79,15 +155,15 @@ const Key_file::Entry* Key_file::get (uint32_t version) const
return it != entries.end() ? &it->second : 0;
}
void Key_file::add (uint32_t version, const Entry& entry)
void Key_file::add (const Entry& entry)
{
entries[version] = entry;
entries[entry.version] = entry;
}
void Key_file::load_legacy (std::istream& in)
{
entries[0].load(in);
entries[0].load_legacy(0, in);
}
void Key_file::load (std::istream& in)
@@ -103,12 +179,52 @@ void Key_file::load (std::istream& in)
if (load_be32(preamble + 12) != FORMAT_VERSION) {
throw Incompatible();
}
load_header(in);
while (in.peek() != -1) {
uint32_t version;
if (!read_be32(in, version)) {
Entry entry;
entry.load(in);
add(entry);
}
}
void Key_file::load_header (std::istream& in)
{
while (true) {
uint32_t field_id;
if (!read_be32(in, field_id)) {
throw Malformed();
}
entries[version].load(in);
if (field_id == HEADER_FIELD_END) {
break;
}
uint32_t field_len;
if (!read_be32(in, field_len)) {
throw Malformed();
}
if (field_id == HEADER_FIELD_KEY_NAME) {
if (field_len > KEY_NAME_MAX_LEN) {
throw Malformed();
}
std::vector<char> bytes(field_len);
in.read(&bytes[0], field_len);
if (in.gcount() != field_len) {
throw Malformed();
}
key_name.assign(&bytes[0], field_len);
if (!validate_key_name(key_name.c_str())) {
key_name.clear();
throw Malformed();
}
} else if (field_id & 1) { // unknown critical field
throw Incompatible();
} else {
// unknown non-critical field - safe to ignore
in.ignore(field_len);
if (in.gcount() != field_len) {
throw Malformed();
}
}
}
}
@@ -116,8 +232,13 @@ void Key_file::store (std::ostream& out) const
{
out.write("\0GITCRYPTKEY", 12);
write_be32(out, FORMAT_VERSION);
if (!key_name.empty()) {
write_be32(out, HEADER_FIELD_KEY_NAME);
write_be32(out, key_name.size());
out.write(key_name.data(), key_name.size());
}
write_be32(out, HEADER_FIELD_END);
for (Map::const_iterator it(entries.begin()); it != entries.end(); ++it) {
write_be32(out, it->first);
it->second.store(out);
}
}
@@ -157,7 +278,8 @@ std::string Key_file::store_to_string () const
void Key_file::generate ()
{
entries[is_empty() ? 0 : latest() + 1].generate();
uint32_t version(is_empty() ? 0 : latest() + 1);
entries[version].generate(version);
}
uint32_t Key_file::latest () const
@@ -168,3 +290,29 @@ uint32_t Key_file::latest () const
return entries.begin()->first;
}
bool validate_key_name (const char* key_name, std::string* reason)
{
if (!*key_name) {
if (reason) { *reason = "Key name may not be empty"; }
return false;
}
if (std::strcmp(key_name, "default") == 0) {
if (reason) { *reason = "`default' is not a legal key name"; }
return false;
}
// Need to be restrictive with key names because they're used as part of a Git filter name
size_t len = 0;
while (char c = *key_name++) {
if (!std::isalnum(c) && c != '-' && c != '_') {
if (reason) { *reason = "Key names may contain only A-Z, a-z, 0-9, '-', and '_'"; }
return false;
}
if (++len > KEY_NAME_MAX_LEN) {
if (reason) { *reason = "Key name is too long"; }
return false;
}
}
return true;
}