Migrate from GitHub

This commit is contained in:
floriano committed 2026-08-31 12:20:27 +02:00
commit 7333327f87
97 files changed
+8272

No files matched your search

Submodule
+1
Submodule lib/Callback added at 6565b0a8a0.
+286
View File
@@ -0,0 +1,286 @@
#include "cryptographic_operations.h"
#include <cstring>
#include <mbedtls/md.h>
#include <mbedtls/gcm.h>
#include <mbedtls/pkcs5.h>
#include <esp_system.h>
namespace pvault_cryptography {
bool generate_salt(uint8_t* salt) {
if(salt == nullptr) { return false; }
esp_fill_random(salt, pvault::salt_size);
return true;
}
bool generate_nonce(uint8_t* nonce) {
if(nonce == nullptr) { return false; }
esp_fill_random(nonce, pvault::nonce_size);
return true;
}
bool derive_key(const String& password, const uint8_t* salt, uint8_t* key) {
// argument validation
if(salt == nullptr || key == nullptr) { return false; }
// init ESP32 cryptography
mbedtls_md_context_t md_ctx;
mbedtls_md_init(&md_ctx);
// SHA256 algorithm descriptions
const mbedtls_md_info_t* md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
if(md_info == nullptr) { return false; }
// create HMAC context
int result = mbedtls_md_setup(&md_ctx, md_info, 1);
if(result != 0) {
mbedtls_md_free(&md_ctx);
return false;
}
// generate AES128 key from provided password
result = mbedtls_pkcs5_pbkdf2_hmac(
&md_ctx,
reinterpret_cast<const unsigned char*>(password.c_str()),
password.length(),
salt,
pvault::salt_size,
pvault::iterations,
pvault::key_size,
key
);
// free HMAC context
mbedtls_md_free(&md_ctx);
if(result != 0) {
secure_zero(key, pvault::key_size);
return false;
}
return true;
}
bool decrypt_verify(
const uint8_t* key,
const uint8_t* nonce,
File& file,
const uint32_t ciphertext_length,
const uint8_t* tag,
const uint32_t header_size
) {
// argument validation
if(!key || !nonce || !file || !tag) { return false; }
// GCM init
mbedtls_gcm_context gcm;
mbedtls_gcm_init(&gcm);
int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 128);
if(ret != 0) {
mbedtls_gcm_free(&gcm);
return false;
}
file.seek(header_size);
// buffers
uint8_t chunk_bytes[CHUNK_SIZE];
uint32_t bytes_remaining = ciphertext_length;
mbedtls_gcm_starts(&gcm, MBEDTLS_GCM_DECRYPT, nonce, pvault::nonce_size, NULL, 0);
uint32_t read_size;
while(bytes_remaining > 0) {
if(bytes_remaining > CHUNK_SIZE) {
read_size = CHUNK_SIZE;
} else {
read_size = bytes_remaining;
}
file.read(chunk_bytes, read_size);
mbedtls_gcm_update(&gcm, read_size, chunk_bytes, chunk_bytes);
bytes_remaining -= read_size;
}
uint8_t computed_tag[pvault::tag_size];
mbedtls_gcm_finish(&gcm, computed_tag, pvault::tag_size);
mbedtls_gcm_free(&gcm);
return memcmp(tag, computed_tag, pvault::tag_size) == 0;
}
bool encrypt_chunked(
const uint8_t* key,
const uint8_t* nonce,
const uint8_t* plaintext,
uint32_t plaintext_length,
File& file
) {
// argument validation
if(!key || !nonce || !plaintext || !file) { return false; }
// buffers
uint8_t tag[pvault::tag_size];
uint8_t chunk_bytes[CHUNK_SIZE];
// GCM init
mbedtls_gcm_context gcm;
mbedtls_gcm_init(&gcm);
int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 128);
if(ret != 0) {
mbedtls_gcm_free(&gcm);
return false;
}
// encryption
mbedtls_gcm_starts(&gcm, MBEDTLS_GCM_ENCRYPT, nonce, pvault::nonce_size, NULL, 0);
uint32_t remaining = plaintext_length;
uint32_t read_offset = 0;
while(remaining > 0) {
uint32_t n;
if(remaining > CHUNK_SIZE) {
n = CHUNK_SIZE;
} else {
n = remaining;
}
mbedtls_gcm_update(&gcm, n, plaintext + read_offset, chunk_bytes);
file.write(chunk_bytes, n);
read_offset += n;
remaining -= n;
}
mbedtls_gcm_finish(&gcm, tag, pvault::tag_size);
mbedtls_gcm_free(&gcm);
file.write(tag, pvault::tag_size);
return true;
}
bool decrypt_chunked( // will return junk if get_key earlier returned false but program still decided to decrypt
const uint8_t* key,
const uint8_t* nonce,
File& file,
uint32_t ciphertext_length,
uint8_t* plaintext
) {
// argument validation
if(!key || !nonce || !file || !plaintext) { return false; }
// buffers
uint32_t file_size = file.size();
uint8_t tag[pvault::tag_size];
uint8_t chunk_bytes[CHUNK_SIZE];
// read tag
file.seek(file_size - pvault::tag_size);
file.read(tag, pvault::tag_size);
// GCM init
mbedtls_gcm_context gcm;
mbedtls_gcm_init(&gcm);
int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 128);
if(ret != 0) {
mbedtls_gcm_free(&gcm);
return false;
}
// decryption
mbedtls_gcm_starts(&gcm, MBEDTLS_GCM_DECRYPT, nonce, pvault::nonce_size, NULL, 0);
// seek back to ciphertext start
file.seek(sizeof(pvault::header));
uint32_t bytes_remaining = ciphertext_length;
uint32_t write_offset = 0;
uint32_t read_size;
while(bytes_remaining > 0) {
if(bytes_remaining > CHUNK_SIZE) {
read_size = CHUNK_SIZE;
} else {
read_size = bytes_remaining;
}
file.read(chunk_bytes, read_size);
mbedtls_gcm_update(&gcm, read_size, chunk_bytes, plaintext + write_offset);
write_offset += read_size;
bytes_remaining -= read_size;
}
mbedtls_gcm_finish(&gcm, tag, pvault::tag_size);
mbedtls_gcm_free(&gcm);
return true;
}
bool write_header(File& file, const pvault::header& hdr) {
// file validation
if(!file) { return false; }
// write header
uint32_t written = file.write(reinterpret_cast<const uint8_t*>(&hdr), sizeof(pvault::header));
// validate written data
if(written != sizeof(pvault::header)) { return false; }
return true;
}
bool read_header(File& file, pvault::header& hdr) {
// file validation
if(!file) { return false; }
// read binary header
uint32_t read_bytes = file.read(reinterpret_cast<uint8_t*>(&hdr), sizeof(pvault::header));
// validate read data
if(read_bytes != sizeof(pvault::header)) { return false; }
return true;
}
bool write_tag(File& file, const uint8_t* tag) {
// file and tag validation
if(!file || !tag) { return false; }
// write binary tag
uint32_t written = file.write(tag, pvault::tag_size);
// validate written data
if(written != pvault::tag_size) { return false; }
return true;
}
bool read_tag(File& file, uint8_t* tag) {
// file and tag validation
if(!file || !tag) { return false; }
// read binary tag
uint32_t read_bytes = file.read(tag, pvault::tag_size);
// validate read data
if(read_bytes != pvault::tag_size) { return false; }
return true;
}
void secure_zero(void* ptr, uint32_t size) {
volatile uint8_t* p = reinterpret_cast<volatile uint8_t*>(ptr);
while(size--) {
*p++ = 0;
}
}
}
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <Arduino.h>
#include <SD.h>
#include "pvault.h"
#define CHUNK_SIZE 1024 // bytes
namespace pvault_cryptography {
// generate values
bool generate_salt(uint8_t* salt);
bool generate_nonce(uint8_t* nonce);
// cryptography
bool derive_key(const String& password, const uint8_t* salt, uint8_t* key); // Derive key from password
bool decrypt_verify( // lightweight function to check if used key works with the ciphertext
const uint8_t* key,
const uint8_t* nonce,
File& file,
const uint32_t ciphertext_length,
const uint8_t* tag,
uint32_t header_size
);
bool encrypt_chunked( // writes ciphertext + tag to provided file
const uint8_t* key,
const uint8_t* nonce,
const uint8_t* plaintext,
uint32_t plaintext_length,
File& file
);
bool decrypt_chunked( // reads tag itself + plaintext to provided pointer
const uint8_t* key,
const uint8_t* nonce,
File& file,
uint32_t ciphertext_length,
uint8_t* plaintext
);
// vault file structure
bool write_header(File& file, const pvault::header& hdr);
bool read_header(File& file, pvault::header& hdr); // assumes that file pointer is set at the beginning of the file
bool write_tag(File& file, const uint8_t* tag);
bool read_tag(File& file, uint8_t* tag); // assumes that file pointer is set on tag's location
// memory security
void secure_zero(void* ptr, uint32_t size);
}
+403
View File
@@ -0,0 +1,403 @@
#include "pvault.h"
#include "cryptographic_operations.h"
namespace pvault {
bool get_key(
const char* path,
const String& password,
uint8_t* key
) {
File file = SD.open(path, FILE_READ);
if(!file) { return false; }
// get master salt and nonce
header hdr{};
pvault_cryptography::read_header(file, hdr);
// obtain key candidate
pvault_cryptography::derive_key(password, hdr.master_salt, key);
uint32_t len = hdr.ciphertext_length;
// get tag
uint8_t tag[pvault::tag_size];
uint32_t file_size = file.size();
file.seek(file_size - pvault::tag_size);
file.read(tag, pvault::tag_size);
bool decryption = pvault_cryptography::decrypt_verify(key, hdr.nonce, file, len, tag, sizeof(pvault::header));
file.close();
return decryption;
}
bool init_vault(
const char* path,
const String& password,
const device_settings& settings,
const vault& data
) {
// input validation
if(!path || password.length() == 0) { return false; }
// open file
if(SD.exists(path)) { SD.remove(path); }
File file = SD.open(path, FILE_WRITE);
if(!file) { return false; }
// buffers
header hdr{};
uint8_t salt[salt_size];
uint8_t nonce[nonce_size];
uint8_t key[key_size];
uint32_t len = sizeof(data);
// generate parameters
pvault_cryptography::generate_salt(salt);
pvault_cryptography::generate_nonce(nonce);
pvault_cryptography::derive_key(password, salt, key);
// build header
hdr.magic[0] = 'C';
hdr.magic[1] = 'R';
hdr.magic[2] = 'Y';
hdr.magic[3] = 'P';
hdr.version = file_version;
hdr.iterations = iterations;
memcpy(hdr.master_salt, salt, salt_size);
memcpy(hdr.nonce, nonce, nonce_size);
hdr.settings = settings;
hdr.ciphertext_length = len;
pvault_cryptography::write_header(file, hdr);
// encrypt
bool encryption = pvault_cryptography::encrypt_chunked(
key,
nonce,
reinterpret_cast<const uint8_t*>(&data),
len,
file
);
// cleanup
pvault_cryptography::secure_zero(salt, salt_size);
pvault_cryptography::secure_zero(nonce, nonce_size);
pvault_cryptography::secure_zero(key, key_size);
file.close();
return encryption;
}
bool update_vault(
const char* path,
const uint8_t* key,
const device_settings& settings,
const vault& data
) {
// input validation
if(!path || !key) { return false; }
// open file
File read = SD.open(path, FILE_READ);
if(!read) { return false; }
// read header
header hdr{};
if(!pvault_cryptography::read_header(read, hdr)) {
read.close();
return false;
}
// close file (we have everything needed)
read.close();
// write header and encrypted data
SD.remove(path);
File write = SD.open(path, FILE_WRITE);
if(!write) {
return false;
}
// generate new nonce
pvault_cryptography::generate_nonce(hdr.nonce);
// modify header
hdr.settings = settings;
pvault_cryptography::write_header(write, hdr);
// encrypt
uint32_t len = sizeof(data);
bool encryption = pvault_cryptography::encrypt_chunked(
key,
hdr.nonce,
reinterpret_cast<const uint8_t*>(&data),
len,
write
);
// cleanup
write.close();
return true;
}
bool load_vault(
const char* path,
const uint8_t* key,
device_settings& settings,
vault& data
) {
// input validation
if(!path || !key) { return false; }
// open file
File file = SD.open(path, FILE_READ);
if(!file) { return false; }
// read header
header hdr{};
if(!pvault_cryptography::read_header(file, hdr)) {
file.close();
return false;
}
// header validation
if(hdr.magic[0] != 'C' || hdr.magic[1] != 'R' || hdr.magic[2] != 'Y' || hdr.magic[3] != 'P') {
file.close();
return false;
}
if(hdr.version != file_version) {
file.close();
return false;
}
if(hdr.ciphertext_length == 0) {
file.close();
return false;
}
settings = hdr.settings;
// buffers
uint32_t len = hdr.ciphertext_length;
uint8_t nonce[nonce_size];
// parameters
memcpy(nonce, hdr.nonce, nonce_size);
bool decryption = pvault_cryptography::decrypt_chunked(
key,
nonce,
file,
len,
reinterpret_cast<uint8_t*>(&data)
);
// cleanup
file.close();
return decryption;
}
bool update_config(
const char* path,
const device_settings& settings
) {
// open the old file
File old_file = SD.open(path, FILE_READ);
if(!old_file) { return false; }
// read its header
header hdr{};
if(!pvault_cryptography::read_header(old_file, hdr)) {
old_file.close();
return false;
}
// update settings in header
hdr.settings = settings;
// obtain length and add tag size to it
uint32_t len = hdr.ciphertext_length;
len += pvault::tag_size;
// open temp file
String temp_path = String(path);
temp_path += ".temp";
if(SD.exists(temp_path)) { SD.remove(temp_path); }
File new_file = SD.open(temp_path, FILE_WRITE);
if(!new_file) {
old_file.close();
return false;
}
// rewrite header and remaining bytes
if(!pvault_cryptography::write_header(new_file, hdr)) {
new_file.close();
old_file.close();
return false;
}
uint32_t to_write = len;
uint32_t write_size;
uint8_t buffer[CHUNK_SIZE];
while(to_write > 0) {
if(to_write > CHUNK_SIZE) {
write_size = CHUNK_SIZE;
} else {
write_size = to_write;
}
old_file.read(buffer, write_size);
new_file.write(buffer, write_size);
to_write -= write_size;
}
old_file.close();
new_file.close();
// delete old file and replace with new
SD.remove(path);
if(SD.exists(temp_path)) { SD.rename(temp_path, path); }
// exit
return true;
}
bool read_config(
const char* path,
device_settings& settings
) {
File file = SD.open(path, FILE_READ);
if(!file) { return false; }
header hdr{};
pvault_cryptography::read_header(file, hdr);
file.close();
settings = hdr.settings;
return true;
}
bool get_salt(
const char* path,
uint8_t* salt
) {
if (!path) { return false; }
File file = SD.open(path, FILE_READ);
if(!file) { return false; }
header hdr{};
if(!pvault_cryptography::read_header(file, hdr)) {
file.close();
return false;
}
memcpy(salt, hdr.master_salt, pvault::salt_size);
file.close();
return true;
}
bool replace_vault(
const char* path,
const device_settings& settings,
const uint8_t* key,
const uint8_t* salt,
const char* import_path
) {
File import_file = SD.open(import_path, FILE_READ);
// read file header
// magic 8 bytes
uint8_t magic[8];
import_file.read(magic, 8);
if(memcmp(magic, "PWIMPORT", 8) != 0) {
return false;
}
// nonce 12 bytes
uint8_t nonce[pvault::nonce_size];
import_file.read(nonce, pvault::nonce_size);
// length 4 bytes
uint32_t len;
import_file.read(reinterpret_cast<uint8_t*>(&len), sizeof(uint32_t));
// read tag
uint32_t file_size = import_file.size();
uint8_t tag[pvault::tag_size];
import_file.seek(file_size - pvault::tag_size);
import_file.read(tag, pvault::tag_size);
// check if key that we have can decrypt the imported vault. Otherwise there's no point doing it.
bool decryption = pvault_cryptography::decrypt_verify(key, nonce, import_file, len, tag, 24);
if(!decryption) { return false; }
// overwrite vault with provided settings
header hdr{};
hdr.magic[0] = 'C';
hdr.magic[1] = 'R';
hdr.magic[2] = 'Y';
hdr.magic[3] = 'P';
hdr.iterations = pvault::iterations;
hdr.settings = settings;
hdr.ciphertext_length = len;
hdr.version = pvault::file_version;
memcpy(hdr.master_salt, salt, pvault::salt_size);
memcpy(hdr.nonce, nonce, pvault::nonce_size);
if(SD.exists(path)) { SD.remove(path); }
File new_file = SD.open(path, FILE_WRITE);
pvault_cryptography::write_header(new_file, hdr);
// copy ciphertext from old file to new
uint32_t to_copy = len;
uint32_t copy_size;
uint8_t copy_buffer[CHUNK_SIZE];
import_file.seek(24);
while(to_copy > 0) {
if(to_copy > CHUNK_SIZE) {
copy_size = CHUNK_SIZE;
} else {
copy_size = to_copy;
}
import_file.read(copy_buffer, copy_size);
new_file.write(copy_buffer, copy_size);
to_copy -= copy_size;
}
pvault_cryptography::write_tag(new_file, tag);
import_file.close();
new_file.close();
// remove import file
SD.remove(import_path);
return true;
}
}
+109
View File
@@ -0,0 +1,109 @@
#pragma once
#include <Arduino.h>
#include <SD.h>
// PWDer vault file format 1.0
// and functions for its operation
// let's see
namespace pvault {
constexpr uint8_t magic_size = 4;
constexpr uint8_t file_version = 1;
constexpr uint32_t iterations = 10000;
constexpr uint8_t salt_size = 16;
constexpr uint8_t nonce_size = 12;
constexpr uint8_t key_size = 16;
constexpr uint8_t tag_size = 16;
constexpr uint16_t max_entries = 101; // first is used for configuration
constexpr uint16_t max_length = 128;
#pragma pack(push, 1)
// encrypted data
struct credential {
char title[max_length];
char username[max_length];
char password[max_length];
char totp_secret[max_length];
};
struct vault {
uint16_t credential_count;
credential credentials[max_entries];
};
// unencrypted settings in header
struct device_settings {
uint8_t speaker_on_auto_lock; // speaker on bit, then auto lock bit
uint8_t input_mode;
uint8_t brightness;
uint8_t color_scheme;
};
// file header
struct header {
char magic[magic_size];
uint8_t version;
uint32_t iterations;
uint8_t master_salt[salt_size];
uint8_t nonce[nonce_size];
device_settings settings;
uint32_t ciphertext_length;
};
#pragma pack(pop)
// surface-level functions
bool get_key( // returns false if password is incorrect
const char* path,
const String& password,
uint8_t* key
);
bool init_vault( // create new vault using provided password
const char* path,
const String& password,
const device_settings& settings,
const vault& data
);
bool update_vault( // update vault data with new information
const char* path,
const uint8_t* key,
const device_settings& settings,
const vault& data
);
bool load_vault( // load existing vault to memory
const char* path,
const uint8_t* key,
device_settings& settings,
vault& data
);
bool update_config( // change unencrypted config without touching the vault data
const char* path,
const device_settings& settings
);
bool read_config( // read config without need to decrypt the entire file
const char* path,
device_settings& settings
);
bool get_salt( // because we need to give it to the PC if we're importing passwords
const char* path,
uint8_t* salt
);
bool replace_vault( // with import file
const char* path,
const device_settings& settings,
const uint8_t* key,
const uint8_t* salt,
const char* import_path
);
}