mirror of
https://github.com/AGWA/git-crypt.git
synced 2025-12-27 05:03:07 -08:00
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:
166
key.cpp
166
key.cpp
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user