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
This commit is contained in:
James Prestwood 2022-02-15 13:41:29 -08:00 committed by Denis Kenzior
parent 429eabf042
commit 01cd858760
3 changed files with 313 additions and 12 deletions

View File

@ -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

View File

@ -39,11 +39,14 @@
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <ell/ell.h>
#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(<zero>, 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));
}
}

View File

@ -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);