/** * @file encryption.c * @author syxhe (https://t.me/syxhe) * @brief A collection of all encryption related functions * @version 0.1 * @date 2025-06-09 * * @copyright Copyright (c) 2026, [Simplified BSD License](../../LICENSE) * */ #define _GNU_SOURCE 1 #ifndef __VXGG_REWRITE___ENCRYPTION_C___1481879318188___ #define __VXGG_REWRITE___ENCRYPTION_C___1481879318188___ 1 #define TPSIZE (1<<13) #include "shared.c" #include "threadpool.c" #include #include #include #include #include #include #include #include #include #include #include /// Chunk size for encryption/decryption #define CHUNKSIZE (1 << 9) //! A list of possible words for password creation #define PASSWORD_WORDS ((const char * const []){\ "the", "of", "to", "and", "for", "our", "their", "has", "in", "he", "a", "them", "that", "these", "by", "have", "we", \ "us", "people", "which", "all", "is", "with", "laws", "be", "are", "his", "states", "on", "they", "right", "it", "from", \ "government", "such", "among", "powers", "most", "an", "time", "should", "new", "as", "been", "colonies", "assent", \ "large", "at", "independent", "free", "united", "when", "mankind", "hold", "rights", "governments", "consent", "its", \ "long", "themselves", "abolishing", "usurpations", "absolute", "repeated", "this", "world", "refused", "pass", "other", \ "others", "without", "justice", "peace", "power", "seas", "war", "do", "declaration", "america", "becomes", "necessary", \ "political", "equal", "declare", "causes", "separation", "men", "happiness", "any", "form", "alter", "or", "will", \ "forms", "same", "object", "off", "necessity", "history", "great", "britain", "tyranny", "over", "public", "good", \ "unless", "suspended", "so", "would", "legislature", "only", "legislative", "bodies", "purpose", "into", "dissolved", \ "state", "endeavoured", "refusing", "hither", "conditions", "establishing", "offices", "out", "armies", "legislatures", \ "render", "jurisdiction", "foreign", "acts", "pretended", "trial", "inhabitants", "cases", "transporting", "rule", \ "declaring", "here", "protection", "against", "lives", "circumstances", "ages", "totally", "friends", "brethren", "whose", \ "every", "may", "therefore", "ought", "unanimous", "thirteen", "course", "human", "events", "one", "dissolve", "bands", \ "connected", "another", "assume", "earth", "separate", "station", "nature", "natures", "god", "entitle", "decent", \ "respect", "opinions", "requires", "impel", "truths", "self", "evident", "created", "endowed", "creator", "certain", \ "unalienable", "life", "liberty", "pursuit", "secure", "instituted", "deriving", "just", "governed", "whenever", \ "destructive", "ends", "abolish", "institute", "laying", "foundation", "principles", "organizing", "shall", "seem", \ "likely", "effect", "safety", "prudence", "indeed", "dictate", "established", "not", "changed", "light", "transient", \ "accordingly", "experience", "hath", "shewn", "more", "disposed", "suffer", "while", "evils", "sufferable", "than", \ "accustomed", "but", "train", "abuses", "pursuing", "invariably", "evinces", "design", "reduce", "under", "despotism", \ "duty", "throw", "provide", "guards", "future", "security", "patient", "sufferance", "now", "constrains", "former", \ "systems", "present", "king", "injuries", "having", "direct", "establishment", "prove", "let", "facts", "submitted", \ "candid", "wholesome", "forbidden", "governors", "immediate", "pressing", "importance", "operation", "till", "obtained", \ "utterly", "neglected", "attend", "accommodation", "districts", "those", "relinquish", "representation", "inestimable", \ "formidable", "tyrants", "called", "together", "places", "unusual", "uncomfortable", "distant", "depository", "records", \ "sole", "fatiguing", "compliance", "measures", "representative", "houses", "repeatedly", "opposing", "manly", "firmness", \ "invasions", "after", "dissolutions", "cause", "elected", "whereby", "incapable", "annihilation", "returned", "exercise", \ "remaining", "mean", "exposed", "dangers", "invasion", "convulsions", "within", "prevent", "population", "obstructing", \ "naturalization", "foreigners", "encourage", "migrations", "raising", "appropriations", "lands", "obstructed", \ "administration", "judiciary", "made", "judges", "dependent", "alone", "tenure", "amount", "payment", "salaries", \ "erected", "multitude", "sent", "swarms", "officers", "harrass", "eat", "substance", "kept", "times", "standing", \ "affected", "military", "superior", "civil", "combined", "subject", "constitution", "unacknowledged", "giving", \ "legislation", "quartering", "armed", "troops", "protecting", "mock", "punishment", "murders", "commit", "cutting", \ "trade", "parts", "imposing", "taxes", "depriving", "many", "benefits", "jury", "beyond", "tried", "offences", "system", \ "english", "neighbouring", "province", "therein", "arbitrary", "enlarging", "boundaries", "once", "example", "fit", \ "instrument", "introducing", "taking", "away", "charters", "valuable", "altering", "fundamentally", "suspending", "own", \ "invested", "legislate", "whatsoever", "abdicated", "waging", "plundered", "ravaged", "coasts", "burnt", "towns", \ "destroyed", "mercenaries", "compleat", "works", "death", "desolation", "already", "begun", "cruelty", "perfidy", \ "scarcely", "paralleled", "barbarous", "unworthy", "head", "civilized", "nation", "constrained", "fellow", "citizens", \ "taken", "captive", "high", "bear", "arms", "country", "become", "executioners", "fall", "hands", "excited", "domestic", \ "insurrections", "amongst", "bring", "frontiers", "merciless", "indian", "savages", "known", "warfare", "undistinguished", \ "destruction", "sexes", "stage", "oppressions", "petitioned", "redress", "humble", "terms", "petitions", "answered", \ "injury", "prince", "character", "thus", "marked", "act", "define", "tyrant", "unfit", "ruler", "nor", "wanting", \ "attentions", "brittish", "warned", "attempts", "extend", "unwarrantable", "reminded", "emigration", "settlement", \ "appealed", "native", "magnanimity", "conjured", "ties", "common", "kindred", "disavow", "inevitably", "interrupt", \ "connections", "correspondence", "too", "deaf", "voice", "consanguinity", "must", "acquiesce", "denounces", "rest", \ "enemies", "representatives", "general", "congress", "assembled", "appealing", "supreme", "judge", "rectitude", \ "intentions", "name", "authority", "solemnly", "publish", "absolved", "allegiance", "british", "crown", "connection", \ "between", "full", "levy", "conclude", "contract", "alliances", "establish", "commerce", "things", "support", "firm", \ "reliance", "divine", "providence", "mutually", "pledge", "each", "fortunes", "sacred", "honor"\ }) //! Short macro for getting the `PASSWORD_WORDS` array size #define PASSWORD_WORDS_LEN (STATIC_ARRAY_LEN(PASSWORD_WORDS)) /** * @brief open() with the flags O_TMPFILE, O_WRONLY, O_CLOEXEC, and O_SYNC. Opened with mode S_IRUSR, S_IWUSR * * @param dest The filename the new descriptor should have. Must be non-null * @retval (int)[-1,int] A new file descriptor. -1 on error */ int maketmp(const char * const dest) { if(!dest) ERRRET(EINVAL, -1); return open(dest, (O_TMPFILE | O_WRONLY | O_CLOEXEC | O_SYNC), (S_IRUSR | S_IWUSR)); } /** * @brief Link a file descriptor into the filesystem * * @param target New filename the descriptor should have * @param tgfd The file descriptor to link * @retval (int)[-1, 0] 0 on success, -1 on error */ int linkto(const char * const target, int tgfd) { if(!target || tgfd < 0) ERRRET(EINVAL, -1); if(access(target, F_OK) != -1) ERRRET(EEXIST, -1); char *path = NULL; int res = -1; asprintf(&path, "/proc/self/fd/%d", tgfd); if(!path) {WARN(errno, " Couldn't get path to move file into system",); goto CLEANUP_linkto;} res = linkat(AT_FDCWD, path, AT_FDCWD, target, AT_SYMLINK_FOLLOW); CLEANUP_linkto: free(path); return res; } /** * @brief Encrypt src to dst using libsodium's xchacha encryption suite * * @param src File to encrypt * @param dst Destination to write encrypted file * @param key Key for encryption * @retval (int)[-1, 0] Returns 0 on success, sets errno and returns -1 on error * @todo Rewrite this into being one of my own functions instead of copying from libsodium */ int encrypttofile(FILE *src, FILE *dst, const unsigned char key[crypto_secretstream_xchacha20poly1305_KEYBYTES]) { if(!src || !dst || !key) ERRRET(EINVAL, -1); unsigned char buf[CHUNKSIZE], cbuf[CHUNKSIZE + crypto_secretstream_xchacha20poly1305_ABYTES]; unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]; crypto_secretstream_xchacha20poly1305_state state; unsigned long long cbuflen; unsigned char tag; size_t bytesread; int eof; // Write the header crypto_secretstream_xchacha20poly1305_init_push(&state, header, key); if(fwrite(header, 1, sizeof(header), dst) < sizeof(header)) { if(ferror(dst)) { WARN(errno, " Could not write header",); return -1; } } // Encrypt each chunk do { if((bytesread = fread(buf, 1, sizeof(buf), src)) < sizeof(buf)) if(ferror(src)) { WARN(errno, " Could not read from source",); return -1; } eof = feof(src); tag = eof ? crypto_secretstream_xchacha20poly1305_TAG_FINAL : 0; crypto_secretstream_xchacha20poly1305_push(&state, cbuf, &cbuflen, buf, bytesread, NULL, 0, tag); if(fwrite(cbuf, 1, (size_t)cbuflen, dst) < (size_t)cbuflen) if(ferror(dst)) { WARN(errno, " Could not write to target",); return -1; } } while (!eof); return 0; } /** * @brief Decrypt src to dst using libsodium's xchacha encryption suite * * @param src File to decrypt * @param dst Destination to write decrypted file * @param key Key used to encrypt * @retval (int)[-1, 0] Returns 0 on success, sets errno and returns -1 on error * @todo Rewrite this into being one of my own functions instead of copying from libsodium */ int decrypttofile(FILE *src, FILE *dst, const unsigned char key[crypto_secretstream_xchacha20poly1305_KEYBYTES]) { if(!src || !dst || !key) ERRRET(EINVAL, -1); unsigned char cbuf[CHUNKSIZE + crypto_secretstream_xchacha20poly1305_ABYTES], buf[CHUNKSIZE]; unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]; crypto_secretstream_xchacha20poly1305_state state; unsigned long long buflen; unsigned char tag; size_t bytesread; int eof; // Read the header if(fread(header, 1, sizeof(header), src) < sizeof(header)) { if(ferror(src)) { WARN(errno, " Couldn't read header", ); return -1; } } // Make sure the header isn't fuckey if(crypto_secretstream_xchacha20poly1305_init_pull(&state, header, key) != 0) { WARN(errno, " Incomplete header", ); return -1; } // Decrypt each chunk do { if((bytesread = fread(cbuf, 1, sizeof(cbuf), src)) < sizeof(cbuf)) { if(ferror(src)) { WARN(errno, " Ran into problem reading for decryption", ); return -1; } } eof = feof(src); if (crypto_secretstream_xchacha20poly1305_pull(&state, buf, &buflen, &tag, cbuf, bytesread, NULL, 0) != 0) { WARN(errno, " Corrupted chunk", ); return -1; } if(tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL && !eof) { WARN(errno, " End of stream before end of file", ); return -1; } if(eof && tag != crypto_secretstream_xchacha20poly1305_TAG_FINAL) { WARN(errno, " End of file before end of stream", ); return -1; } fwrite(buf, 1, (size_t)buflen, dst); } while(! eof); return 0; } /** * @brief Encrypt file at `target` to `output` using Linux's named temp file system to do it in the background * * @param target * @param output * @param key * @retval (int)[,] * @todo Fill out warning messages & documentation */ int encryptviatmp(const char * const target, const char * const output, const unsigned char key[crypto_secretstream_xchacha20poly1305_KEYBYTES]) { if(!target || !output || !key) ERRRET(EINVAL, -1); int fd = -1, tfd = -1, res = -1, eflag = 0; FILE *src, *dst; char *targetdir; // Open the target file if((fd = open(target, O_RDONLY)) < 0) {eflag = 1; goto CLEANUP_encryptviatmp;} // Create a temp file for writing targetdir = vxdirname(output); if(!targetdir) {eflag = 2; goto CLEANUP_encryptviatmp;} // Actually get the file descriptor for the temp file tfd = maketmp(targetdir); if(tfd < 0) {eflag = 3; goto CLEANUP_encryptviatmp;} // Create a FILE* version of the source fd if(!(src = fdopen(fd, "rb"))) {eflag = 4; goto CLEANUP_encryptviatmp;} // Create a FILE* version of the target fd if(!(dst = fdopen(tfd, "wb"))) {eflag = 5; goto CLEANUP_encryptviatmp;} // Do the encryption now that everything has been set up if(encrypttofile(src, dst, key) < 0) {eflag = 6; goto CLEANUP_encryptviatmp;} // Link the temp file into the system if(linkto(output, tfd) < 0) {eflag = 7; goto CLEANUP_encryptviatmp;} res = 0; CLEANUP_encryptviatmp: if(___VXGG___VERBOSE_ERRORS___) { switch (eflag) { case 0: break; case 1: WARN(errno, " Warning: Could not open target fd \"%s\"",, target); case 2: WARN(errno, " Warning: Could not get real dirname for \"%s\"",, output); case 3: WARN(errno, " Warning: Could not make temp file in target dir \"%s\"",, targetdir); case 4: WARN(errno, " Warning: Could not get FILE* handle for source file \"%s\"",, target); case 5: WARN(errno, " Warning: Could not get FILE* handle for output file",); case 6: ERROR(1, ENOTRECOVERABLE, " ERROR: I don't even have a way to cause an error here. How did you do it?",); case 7: WARN(errno, " Warning: Could not link \"%s\" into system after encryption",, output); default: WARN(0, " Warning: Ran into some unknown error",); } } free(targetdir); fclose(src); fclose(dst); close(fd); close(tfd); return res; } /** * @brief Decrypt the file at `encrypted` to `target` * * @param encrypted * @param target * @param key * @retval (int)[,] * @todo Fill out documentation */ int decryptto(const char * const target, const char * const output, const unsigned char key[crypto_secretstream_xchacha20poly1305_KEYBYTES]) { if(!target || !output || !key) ERRRET(EINVAL, -1); FILE *src = NULL, *dst = NULL; int fdst = -1, eflag = -1, res = -1; // Open the source file if(!(src = fopen(target, "rb"))) {eflag = 0; goto CLEANUP_decryptto;} // Get a temp descriptor for the temp file if(!(fdst = maketmp(output))) {eflag = 1; goto CLEANUP_decryptto;} // Open a FILE* version of the temp file if(!(dst = fdopen(fdst, "wb"))) {eflag = 2; goto CLEANUP_decryptto;} // Follow through with the rest of the decryption if(decrypttofile(src, dst, key) < 0) {eflag = 3; goto CLEANUP_decryptto;} // Link temp into system if(linkto(output, fdst) < 0) {eflag = 4; goto CLEANUP_decryptto;} res = 0; CLEANUP_decryptto: fclose(src); fclose(dst); close(fdst); if(___VXGG___VERBOSE_ERRORS___) { switch (eflag) { case 0: WARN(errno, " Could not open \"%s\" for decryption",, target); break; case 1: WARN(errno, " Could not get temp file for decryption",); break; case 2: WARN(errno, " Could not open \"%s\" for writing decrypted data",, output); break; case 3: ERROR(1, errno, " How did you even cause an error?",); break; case 4: WARN(errno, " Could not link \"%s\" into system",, output); break; default: WARN(errno, " Ran into an error",); break; } } return res; } /** * @brief Generate a password viable for use in the derivation of a key * * @param str Pointer to a string. This will be filled by a malloc'ed string of words (the password). Must be non-null * @param words The number of words to include in the password. A password of at least 20 words and probably not more than 40 is recommended * @retval (int)[-1, words] On success, returns the number of words requested. On error, returns -1 and sets errno */ int genpassword(char **str, unsigned int words) { if(words < 1) return 0; if(!str) ERRRET(EINVAL, -1); // Bootstrap the first word char *lstr = NULL, *tmp = NULL; if(asprintf(&lstr, "%s", PASSWORD_WORDS[randombytes_uniform(PASSWORD_WORDS_LEN)]) < 0) return -1; // Concat the rest of the words into the password (without leaking memory) int ret; for(unsigned int i = 1; i < words; i++) { ret = asprintf(&tmp, "%s %s", lstr, PASSWORD_WORDS[randombytes_uniform(PASSWORD_WORDS_LEN)]); sodium_memzero(lstr, strlen(lstr) + 1); free(lstr); if(ret < 0) return -1; lstr = tmp; } *str = lstr; return words; } enum CRYPTSCAN_CRYPTMODE { CRYPTSCAN_CRYPTMODE__UNSPEC, CRYPTSCAN_CRYPTMODE__ENCRYPT, CRYPTSCAN_CRYPTMODE__DECRYPT, CRYPTSCAN_CRYPTMODE__TOOBIG }; struct _cryptscan_args { char *folder; taskqueue *toscan; ctqueue *tocrypt; enum CRYPTSCAN_CRYPTMODE mode; }; /// Directory entries (files and folders) to exclude from encryption #define EXCLUDED_ENTRIES ((const char* const []){\ /* Entries found in /home/ */ \ ".config", ".bashrc", \ /* Entries included for extra safety, so that the encryption doesn't accidentally render the system unusable */ \ "bin", "dev", "lib", "root", "boot", "etc", "lib32", "run", "sys", "usr", "home", "lib64", "proc", "sbin", "var"\ }) /// Helper function to select files to be encrypted static int _cryptscan__selector(const struct dirent *de) { // entries with non-zero returns get included, zeros get excluded if(!de) return 0; for(int i = 0; i < STATIC_ARRAY_LEN(EXCLUDED_ENTRIES); i++) { // Would use strncmp here but d_name doesn't have a fixed size and is supposedly guaranteed to have a null terminator so it shouldn't be a big deal if(strcmp(de->d_name, EXCLUDED_ENTRIES[i]) == 0) return 0; } return 1; } void _cryptscan_args_free(void *data) { if(!data) return; struct _cryptscan_args *real = data; free(real->folder); free(real); return; } int _cryptscan__crypt(void *data); int _cryptscan__scan(void *data); /// helper function to deduplicate code for dealing with scandir. Scans the directory at `folder` and places dirents into their respective lists int _cryptscan__process_scandir(const char * const folder, taskqueue *toscan, ctqueue *tocrypt, enum CRYPTSCAN_CRYPTMODE mode) { if(!folder || !toscan || !tocrypt || mode <= CRYPTSCAN_CRYPTMODE__UNSPEC || mode >= CRYPTSCAN_CRYPTMODE__TOOBIG) return -1; int res = 0; struct dirent **namelist = NULL; int entries = scandir(folder, &namelist, _cryptscan__selector, alphasort); if(!toscan || !tocrypt || entries < 0) {res = -1; goto _cryptscan__process_scandir_CLEANUP;} struct stat sb; task *tmptsk = NULL; struct _cryptscan_args *args = NULL; char tflag = 0; for(int i = 0; i < entries; i++) { args = VXGG_CALLOC(1, sizeof(*args)); if(!args) { if(___VXGG___VERBOSE_ERRORS___) WARN(errno, "<_cryptscan__process_scandir> Warning: Could not create arg holder for task",); } args->folder = strdup(namelist[i]->d_name); args->tocrypt = tocrypt; args->toscan = toscan; args->mode = mode; if(!args->folder) {if(___VXGG___VERBOSE_ERRORS___) WARN(errno, "<_cryptscan__process_scandir> Warning: Could not duplicate file \"%s\"s name for processing",, namelist[i]->d_name); continue;} switch(namelist[i]->d_type) { // Try to stat the file if it's unknown case DT_UNKNOWN: if(stat(namelist[i]->d_name, &sb) < 0) goto UNKNOWN; if(S_ISDIR(sb.st_mode)) goto SCAN; if(S_ISREG(sb.st_mode)) goto CRYPT; break; // Add it to the ctq case DT_REG: CRYPT: tmptsk = task_new(_cryptscan__crypt, _cryptscan_args_free, args); if(!tmptsk) { if(___VXGG___VERBOSE_ERRORS___) WARN(errno, "<_cryptscan__process_scandir> Warning: Could not generate crypt task for \"%s\"",, args->folder); break; } if(ctqueue_waitpush(tocrypt, tmptsk) < 0) { if(___VXGG___VERBOSE_ERRORS___) WARN(errno, "<_cryptscan__process_scandir> Warning: Could not add crypt task to ctq",); task_free(tmptsk); break; } res++; tflag++; break; // Add it to the scanlist case DT_DIR: SCAN: tmptsk = task_new(_cryptscan__scan, _cryptscan_args_free, args); if(!tmptsk) { if(___VXGG___VERBOSE_ERRORS___) WARN(errno, "<_cryptscan__process_scandir> Warning: Could not generate scan task for \"%s\"",, args->folder); break; } if(taskqueue_push(toscan, tmptsk) < 0) { if(___VXGG___VERBOSE_ERRORS___) WARN(errno, "<_cryptscan__process_scandir> Warning: Could not add scan task to scanlist",); task_free(tmptsk); break; } res++; tflag++; break; // Ignore or spit out a warning default: UNKNOWN: if(___VXGG___VERBOSE_ERRORS___) WARN(ENOSYS, "<_cryptscan__process_scandir> Info: file \"%s\"s type is unsupported",, namelist[i]->d_name); break; } if(!tflag) _cryptscan_args_free(args); tflag = 0; } _cryptscan__process_scandir_CLEANUP: for(int i = 0; i < entries; i++) { free(namelist[i]); } free(namelist); return res; } int _cryptscan__scan(void *data) { if(!data) return -1; struct _cryptscan_args *real = data; return _cryptscan__process_scandir(real->folder, real->toscan, real->tocrypt, real->mode); } // TODO: Implement int _cryptscan__crypt(void *data) { if(!data) return -1; struct _cryptscan_args *real = data; // Read data for filename & crypt mode to generate tasks return 0; } // Going to implement this using a taskqueue. Each folder is added as a task to scan, with each file then added to a ctq for later. Scanning will be done linearly // for my sake, and because I do not care to do generics in C beyond what is absolutely necessary (aka I don't want to implement a hashmap/hashset to make parallel // execution efficient) ctqueue * cryptscan(int threads, const char * const start, enum CRYPTSCAN_CRYPTMODE mode) { if(!start || threads < 1 || mode <= CRYPTSCAN_CRYPTMODE__UNSPEC || mode >= CRYPTSCAN_CRYPTMODE__TOOBIG) return NULL; taskqueue *toscan = taskqueue_new(); ctqueue *tocrypt = ctqueue_init(threads); // Initialize the lists if(_cryptscan__process_scandir(start, toscan, tocrypt, mode) < 1) goto cryptscan_ERR; // Loop through the scanlist until it's empty while(taskqueue_size(toscan) > 0) { task_fired(taskqueue_pop(toscan)); } taskqueue_free(toscan); return tocrypt; cryptscan_ERR: taskqueue_free(toscan); ctqueue_free(tocrypt); return NULL; } #endif