From 01cd8587606bf2da1af245163150589834126c1c Mon Sep 17 00:00:00 2001 From: James Prestwood Date: Tue, 15 Feb 2022 13:41:29 -0800 Subject: [PATCH] storage: implement network profile encryption Some users don't like the idea of storing network credentials in plaintext on the file system. This patch implements an option to encrypt such profiles using a secret key. The origin of the key can in theory be anything, but would typically be provided by systemd via 'LoadEncryptedCredential' setting in the iwd unit file. The encryption operates on the entire [Security] group as well as all embedded groups. Once encrypted the [Security] group will be replaced with two key/values: EncryptedSalt - A random string of bytes used for the encryption EncryptedSecurity - A string of bytes containing the encrypted [Security] group, as well as all embedded groups. After the profile has been encrypted these values should not be modified. Note that any values added to [Security] after encryption has no effect. Once the profile is encrypted there is no way to modify [Security] without manually decrypting first, or just re-creating it entirely which effectively treated a 'new' profile. The encryption/decryption is done using AES-SIV with a salt value and the network SSID as the IV. Once a key is set any profiles opened will automatically be encrypted and re-written to disk. Modules using network_storage_open will be provided the decrypted profile, and will be unaware it was ever encrypted in the first place. Similarly when network_storage_sync is called the profile will by automatically encrypted and written to disk without the caller needing to do anything special. A few private storage.c helpers were added to serve several purposes: storage_init/exit(): This sets/cleans up the encryption key direct from systemd then uses extract and expand to create a new fixed length key to perform encryption/decryption. __storage_decrypt(): Low level API to decrypt an l_settings object using a previously set key and the SSID/name for the network. This returns a 'changed' out parameter signifying that the settings need to be encrypted and re-written to disk. The purpose of exposing this is for a standalone decryption tool which does not re-write any settings. storage_decrypt(): Wrapper around __storage_decrypt() that handles re-writing a new profile to disk. This was exposed in order to support hotspot profiles. __storage_encrypt(): Encrypts an l_settings object and returns the full profile as data --- Makefile.am | 3 +- src/storage.c | 312 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/storage.h | 10 ++ 3 files changed, 313 insertions(+), 12 deletions(-) diff --git a/Makefile.am b/Makefile.am index 89c053a6..35938d22 100644 --- a/Makefile.am +++ b/Makefile.am @@ -381,7 +381,8 @@ tools_hwsim_SOURCES = tools/hwsim.c src/mpdu.h \ src/nl80211util.h src/nl80211util.c \ src/storage.h src/storage.c \ src/common.h src/common.c \ - src/band.h src/band.c + src/band.h src/band.c \ + src/crypto.h src/crypto.c tools_hwsim_LDADD = $(ell_ldadd) if DBUS_POLICY diff --git a/src/storage.c b/src/storage.c index 4b89c615..82a72443 100644 --- a/src/storage.c +++ b/src/storage.c @@ -39,11 +39,14 @@ #include #include #include +#include #include +#include "ell/useful.h" #include "src/common.h" #include "src/storage.h" +#include "src/crypto.h" #define STORAGE_DIR_MODE (S_IRUSR | S_IWUSR | S_IXUSR) #define STORAGE_FILE_MODE (S_IRUSR | S_IWUSR) @@ -52,6 +55,8 @@ static char *storage_path = NULL; static char *storage_hotspot_path = NULL; +static uint8_t system_key[32]; +static bool system_key_set = false; static int create_dirs(const char *filename) { @@ -347,24 +352,263 @@ const char *storage_network_ssid_from_path(const char *path, return buf; } +/* Groups requiring encryption (if enabled) */ +static char *encrypt_groups[] = { + "Security", + NULL +}; + +static bool encrypt_group(const char *group) +{ + char **g; + + for (g = encrypt_groups; *g; g++) { + if (!strcmp(*g, group)) + return true; + } + + return false; +} + +/* + * Encrypt needed groups of 'settings' without modifying the object. Returns + * the entire settings object as data, with encrypted groups as a bytestring + * set as the value to [Security].EncryptedSecurity. This also includes any + * embedded groups. + * + * Note: If encryption is not enabled or there is no Security group this is + * effectively l_settings_to_data. + */ +char *__storage_encrypt(const struct l_settings *settings, const char *name, + size_t *out_len) +{ + struct iovec ad[2]; + uint8_t salt[32]; + size_t len; + _auto_(l_settings_free) struct l_settings *to_encrypt = NULL; + _auto_(l_settings_free) struct l_settings *original = NULL; + _auto_(l_free) char *plaintext = NULL; + _auto_(l_free) uint8_t *enc = NULL; + _auto_(l_strv_free) char **groups = NULL; + char **i; + + if (!system_key_set || !l_settings_has_group(settings, "Security")) + return l_settings_to_data(settings, out_len); + + /* + * Make two copies of the settings: One will contain only data to be + * encrypted (to_encrypt), the other will contain data to be left + * unencrypted (original). At the end any encrypted data will be set + * into 'original' as EncryptedSecurity. + */ + to_encrypt = l_settings_clone(settings); + original = l_settings_clone(settings); + + groups = l_settings_get_groups(to_encrypt); + for (i = groups; *i; i++) { + if (encrypt_group(*i)) + l_settings_remove_group(original, *i); + else + l_settings_remove_group(to_encrypt, *i); + } + + l_settings_remove_embedded_groups(original); + + plaintext = l_settings_to_data(to_encrypt, &len); + if (!plaintext) + return NULL; + + l_getrandom(salt, 32); + + ad[0].iov_base = (void *) salt; + ad[0].iov_len = 32; + ad[1].iov_base = (void *) name; + ad[1].iov_len = strlen(name); + + /* + * AES-SIV automatically prepends the IV (16 bytes) to the encrypted + * data. + */ + enc = l_malloc(len + 16); + + if (!aes_siv_encrypt(system_key, sizeof(system_key), plaintext, len, + ad, 2, enc)) { + l_error("Could not encrypt [Security] group"); + return NULL; + } + + l_settings_set_bytes(original, "Security", "EncryptedSalt", salt, 32); + l_settings_set_bytes(original, "Security", "EncryptedSecurity", + enc, len + 16); + + return l_settings_to_data(original, out_len); +} + +/* + * Decrypt data in [Security].EncryptedSecurity. This data also includes + * embedded groups potentially. Once decrypted the data is put back into the + * object. + * + * Note: if encryption is not enabled or there is no Security group settings + * is not modified. + */ +int __storage_decrypt(struct l_settings *settings, const char *ssid, + bool *encrypt) +{ + _auto_(l_settings_free) struct l_settings *security = NULL; + _auto_(l_free) uint8_t *encrypted = NULL; + _auto_(l_free) uint8_t *decrypted = NULL; + _auto_(l_free) uint8_t *salt = NULL; + _auto_(l_strv_free) char **embedded = NULL; + _auto_(l_strv_free) char **groups = NULL; + char **i; + size_t elen, slen; + struct iovec ad[2]; + + if (!system_key_set) + goto done; + + if (!l_settings_has_group(settings, "Security")) + goto done; + + encrypted = l_settings_get_bytes(settings, "Security", + "EncryptedSecurity", &elen); + salt = l_settings_get_bytes(settings, "Security", + "EncryptedSalt", &slen); + + /* + * Either profile has never been loaded after enabling encryption or is + * missing Encrypted{Salt,Security} values. If either are missing this + * profile is corrupted and must be fixed. + */ + if (!(encrypted && salt)) { + /* Profile corrupted */ + if (encrypted || salt) { + l_warn("Profile %s is corrupted reconfigure manually", + ssid); + return -EBADMSG; + } + + if (encrypt) + *encrypt = true; + + return 0; + } + + /* + * AES-SIV automatically verifies the IV (16 bytes) and returns only + * the decrypted data portion. We add one here for the NULL terminator + * since this is always going to be textual data after decryption. + */ + decrypted = l_malloc(elen - 16 + 1); + + ad[0].iov_base = (void *)salt; + ad[0].iov_len = slen; + ad[1].iov_base = (void *)ssid; + ad[1].iov_len = strlen(ssid); + + if (!aes_siv_decrypt(system_key, sizeof(system_key), encrypted, elen, + ad, 2, decrypted)) { + l_error("Could not decrypt %s profile, did the secret change?", + ssid); + return -ENOKEY; + } + + decrypted[elen - 16] = '\0'; + + /* + * Remove any groups that are marked as encrypted (plus embedded), + * and copy the decrypted groups back into settings. + */ + groups = l_settings_get_groups(settings); + for (i = groups; *i; i++) { + if (encrypt_group(*i)) + l_settings_remove_group(settings, *i); + } + + l_settings_remove_embedded_groups(settings); + + /* + * Load decrypted data into existing settings. This is not how the API + * is indended to be used (since this could result in duplicate groups) + * but since the Security group was just removed and EncryptedSecurity + * should only contain a Security group its safe to use it this way. + */ + if (!l_settings_load_from_data(settings, (const char *) decrypted, + elen - 16)) { + l_error("Could not load decrypted security group"); + return -EBADMSG; + } + +done: + if (encrypt) + *encrypt = false; + + return 0; +} + +/* + * Decrypts a network profile (if needed). If profile encryption is enabled + * and the profile is unencrypted it will be encrypted and written back to + * the file system automatically. + * + * 'name' is used for decryption and is used as part of the IV. Callers + * should provide a unique identifier here if available. For example, the + * SSID, consortium name, etc. + */ +bool storage_decrypt(struct l_settings *settings, const char *path, + const char *name) +{ + bool needs_encryption; + _auto_(l_free) char *encrypted = NULL; + size_t elen; + + if (__storage_decrypt(settings, name, &needs_encryption) < 0) + return false; + + if (!needs_encryption) + return true; + + /* Profile never encrypted before. Encrypt and write to disk */ + encrypted = __storage_encrypt(settings, name, &elen); + if (!encrypted) { + l_error("Could not encrypt new profile %s", name); + return false; + } + + if (write_file(encrypted, elen, false, "%s", path) < 0) { + l_error("Failed to write out encrypted profile"); + return false; + } + + l_debug("Encrypted a new profile %s", path); + + return true; +} + struct l_settings *storage_network_open(enum security type, const char *ssid) { struct l_settings *settings; - char *path; + _auto_(l_free) char *path = NULL; if (ssid == NULL) return NULL; path = storage_get_network_file_path(type, ssid); + settings = l_settings_new(); - if (!l_settings_load_from_file(settings, path)) { - l_settings_free(settings); - settings = NULL; - } + if (!l_settings_load_from_file(settings, path)) + goto error; + + if (type != SECURITY_NONE && !storage_decrypt(settings, path, ssid)) + goto error; - l_free(path); return settings; + +error: + l_settings_free(settings); + return NULL; } int storage_network_touch(enum security type, const char *ssid) @@ -388,15 +632,19 @@ int storage_network_touch(enum security type, const char *ssid) void storage_network_sync(enum security type, const char *ssid, struct l_settings *settings) { - char *data; + _auto_(l_free) char *data = NULL; + _auto_(l_free) char *path = NULL; size_t length = 0; - char *path; path = storage_get_network_file_path(type, ssid); - data = l_settings_to_data(settings, &length); + data = __storage_encrypt(settings, ssid, &length); + + if (!data) { + l_error("Unable to sync profile %s", ssid); + return; + } + write_file(data, length, true, "%s", path); - l_free(data); - l_free(path); } int storage_network_remove(enum security type, const char *ssid) @@ -463,3 +711,45 @@ bool storage_is_file(const char *filename) return false; } + +/* + * Initialize a systemd encryption key for encrypting/decrypting credentials. + * This uses the 'extract and expand' concept from RFC 5869 to derive a new + * fixed length key. Note that a 'zero salt' is used in this derivation which + * is handled internally in hkdf_extract(). 'TK' denotes the temporary key + * derived from 'extract' and used as the input to 'expand'. + * + * Inputs: + * IKM - Input keying material of arbitrary length. This is the key + * obtained directly from systemd. + * Outputs: + * OKM - Output key material of 32 bytes + * + * TK = HKDF-Extract(, IKM) + * OKM = HKDF-Expand(TK, "System Key", 32) + */ +bool storage_init(const uint8_t *key, size_t key_len) +{ + uint8_t tmp[32]; + + if (mlock(system_key, sizeof(system_key)) < 0) + return false; + + if (!hkdf_extract(L_CHECKSUM_SHA256, NULL, 0, 1, tmp, key, key_len)) + return false; + + system_key_set = hkdf_expand(L_CHECKSUM_SHA256, tmp, sizeof(tmp), + "System Key", + system_key, sizeof(system_key)); + + explicit_bzero(tmp, sizeof(tmp)); + return system_key_set; +} + +void storage_exit(void) +{ + if (system_key_set) { + explicit_bzero(system_key, sizeof(system_key)); + munlock(system_key, sizeof(system_key)); + } +} diff --git a/src/storage.h b/src/storage.h index e1ec2cd4..6877fb65 100644 --- a/src/storage.h +++ b/src/storage.h @@ -50,3 +50,13 @@ int storage_network_remove(enum security type, const char *ssid); struct l_settings *storage_known_frequencies_load(void); void storage_known_frequencies_sync(struct l_settings *known_freqs); + +int __storage_decrypt(struct l_settings *settings, const char *ssid, + bool *changed); +char *__storage_encrypt(const struct l_settings *settings, const char *ssid, + size_t *out_len); +bool storage_decrypt(struct l_settings *settings, const char *path, + const char *name); + +bool storage_init(const uint8_t *key, size_t key_len); +void storage_exit(void);