pax_global_header00006660000000000000000000000064141171015650014513gustar00rootroot0000000000000052 comment=d2682a1549b635ed80305b4393555b759bde7491 ylva-1.7/000077500000000000000000000000001411710156500123355ustar00rootroot00000000000000ylva-1.7/.gitignore000077500000000000000000000001541411710156500143300ustar00rootroot00000000000000*.o ylva TAGS *.geany Titan.vcxproj* bin/ obj/ *.html .editorconfig Ylva.sln Ylva.vcxproj Ylva.vcxproj.user ylva-1.7/INSTALL000066400000000000000000000015671411710156500133770ustar00rootroot00000000000000Installing Ylva is easy. Ready made packages are available for example on Debian stable. If you're running Debian or Ubuntu (21.04 or later) you can simply install Ylva with command sudo apt install ylva. There are also package in FreeBSD ports, I don't use FreeBSD so I don't know how to install ylva from the ports. It's probably very easy. Please note, that packages might not give you the latest stable version of Ylva. How to install Ylva from the source code? You need development libraries for Sqlite, OpenSSL and libqrcodegen. You will also need a modern C compiler and GNU Make. On Fedora: sudo dnf install sqlite-devel openssl-devel libqrcodegen-devel cd into Ylva source directory and type commands: On Debian: sudo apt install libsqlite3-dev libssl-dev libqrcodegen-dev make sudo make install Now you should be able to start Ylva by typing ylva in your terminal. ylva-1.7/LICENSE000066400000000000000000000020571411710156500133460ustar00rootroot00000000000000Copyright 2017 Niko Rosvall Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ylva-1.7/NEWS000066400000000000000000000027651411710156500130460ustar00rootroot00000000000000Release 1.7 (2021-09-11) Support for displaying passwords as QR code in terminal Support for YLVA_DEFAULT_USERNAME environment variable Default new password length is now 20 characters Added install documentation Better directory structure for the project Many smaller changes to improve reliability Release 1.6 (2020-04-17) Some changes to command line switches Fixing typos in the manual Makefile fixes Minor code refactoring Very stable long term release Release 1.5 (2019-05-24) Password generator now uses RAND_bytes from OpenSSL Regex search can now search all the entry fields Minor memory leak fixed Entries can now be duplicated with -D, --duplicate Decrypted database is now only readable by the owner Release 1.4 (2019-05-18) *Titan is now Ylva *Ylva no longer supports encrypting individual files or directories, it's not a job for a password manager. *Note that if you used colors, change TITAN_COLOR to YLVA_COLOR *This release is made mostly because of the name change, but active development will continue Release 1.3 (2018-09-04) *Titan now supports custom count of listing latest entries Release 1.2 (2017-11-14) *Support for colors. See man titan(1) for more information. *Added Support encrypting individual files *Added support for encrypting directories *Many smaller internal changes and bug fixes ylva-1.7/README.md000066400000000000000000000003761411710156500136220ustar00rootroot00000000000000# Ylva - Command line password manager Password management belongs to the command line, deep into the Unix heartland, the shell. Ylva is written in C and is licensed under the MIT license. Ylva is mostly developed on Linux, but should work on BSD too. ylva-1.7/src/000077500000000000000000000000001411710156500131245ustar00rootroot00000000000000ylva-1.7/src/Makefile000066400000000000000000000014041411710156500145630ustar00rootroot00000000000000CC?=gcc CFLAGS?=-g CFLAGS+=-std=c11 -Wall PREFIX?=/usr/ MANDIR?=$(PREFIX)/share/man LIBS=-lcrypto -lsqlite3 -lqrcodegen -lrt PROG=ylva OBJS=$(patsubst %.c, %.o, $(sort $(wildcard *.c))) HEADERS=$(wildcard *.h) all: $(PROG) %.o: %.c $(HEADERS) $(CC) $(CFLAGS) -c $< -o $@ $(PROG): $(OBJS) $(CC) $(OBJS) $(LDFLAGS) $(LIBS) -o $@ clean: rm -f *.o rm -f $(PROG) DESTBINDIR = $(DESTDIR)$(PREFIX)/bin install: all if [ ! -d $(DESTDIR)$(MANDIR)/man1 ];then \ mkdir -p $(DESTDIR)$(MANDIR)/man1; \ fi install -m644 ylva.1 $(DESTDIR)$(MANDIR)/man1/ gzip -f $(DESTDIR)$(MANDIR)/man1/ylva.1 if [ ! -d $(DESTBINDIR) ] ; then \ mkdir -p $(DESTBINDIR) ; \ fi install -m755 ylva $(DESTBINDIR)/ uninstall: rm $(PREFIX)/bin/ylva rm $(DESTDIR)$(MANDIR)/man1/ylva.1.gz ylva-1.7/src/cmd_ui.c000066400000000000000000000306271411710156500145400ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include "cmd_ui.h" #include "entry.h" #include "db.h" #include "utils.h" #include "crypto.h" #include "pwd-gen.h" #include "regexfind.h" /*Removes new line character from a string.*/ static void strip_newline_str(char *str) { char *i = str; char *j = str; while (*j != '\0') { *i = *j++; if(*i != '\n') i++; } *i = '\0'; } /* Function assumes that the in_buffer has enough space. This * function is only used when user wants to create a new entry * Length is hard coded to keep things simple. */ static void generate_new_password(char *in_buffer) { char *new_pass = NULL; new_pass = generate_password(20); if(!new_pass) { fprintf(stderr, "WARNING: Unable to generate new password.\n"); } else { strcpy(in_buffer, new_pass); free(new_pass); } } /*Turns echo of from the terminal and asks for a passphrase. *Usually stream is stdin. Returns length of the passphrase, *passphrase is stored to lineptr. Lineptr must be allocated beforehand. */ static size_t my_getpass(char *prompt, char **lineptr, size_t *n, FILE *stream) { struct termios old, new; int nread; /*Turn terminal echoing off.*/ if(tcgetattr(fileno(stream), &old) != 0) return -1; new = old; new.c_lflag &= ~ECHO; if(tcsetattr(fileno(stream), TCSAFLUSH, &new) != 0) return -1; if(prompt) printf("%s", prompt); /*Read the password.*/ nread = getline(lineptr, n, stream); if(nread >= 1 && (*lineptr)[nread - 1] == '\n') { (*lineptr)[nread - 1] = 0; nread--; } printf("\n"); /*Restore terminal echo.*/ tcsetattr(fileno(stream), TCSAFLUSH, &old); return nread; } static void auto_enc() { fprintf(stdout, "Auto encrypt enabled, type password to encrypt.\n"); encrypt_database(); } void init_database(const char *path, int force, int auto_encrypt) { if(!has_active_database() || force == 1) { //If forced, delete any existing file if(force == 1) { if(file_exists(path)) unlink(path); } if(db_init_new(path)) { write_active_database_path(path); if(auto_encrypt == 1) auto_enc(); } } else { fprintf(stderr, "Existing database is already active. " "Encrypt it before creating a new one.\n"); } } bool decrypt_database(const char *path) { if(has_active_database()) { fprintf(stderr, "Existing database is already active. " "Encrypt it before decrypting another one.\n"); return false; } size_t pwdlen = 1024; char pass[pwdlen]; char *ptr = pass; my_getpass("Password: ", &ptr, &pwdlen, stdin); if(!decrypt_file(pass, path)) { fprintf(stderr, "Failed to decrypt %s.\n", path); return false; } write_active_database_path(path); return true; } bool encrypt_database() { if(!has_active_database()) { fprintf(stderr, "No decrypted database found.\n"); return false; } size_t pwdlen = 1024; char pass[pwdlen]; char *ptr = pass; char pass2[pwdlen]; char *ptr2 = pass2; char *path = NULL; char *open_db_holder_path = NULL; path = read_active_database_path(); if(!path) { fprintf(stderr, "Unable to read activate database path.\n"); return false; } my_getpass("Password: ", &ptr, &pwdlen, stdin); my_getpass("Password again: ", &ptr2, &pwdlen, stdin); if(strcmp(pass, pass2) != 0) { fprintf(stderr, "Password mismatch.\n"); free(path); return false; } if(!encrypt_file(pass, path)) { fprintf(stderr, "Encryption of %s failed.\n", path); free(path); return false; } free(path); open_db_holder_path = get_open_db_path_holder_filepath(); if(!open_db_holder_path) { fprintf(stderr, "Unable to retrieve the ylva.open_db file path.\n"); return false; } //Finally delete the file that holds the activate database path. //This way we allow Ylva to create a new database or open another one. unlink(open_db_holder_path); free(open_db_holder_path); return true; } /* Interactively adds a new entry to the database */ bool add_new_entry(int auto_encrypt) { if(!has_active_database()) { fprintf(stderr, "No decrypted database found.\n"); return false; } char title[1024] = {0}; char user[1024] = {0}; char* user_default = NULL; char url[1024] = {0}; char notes[1024] = {0}; size_t pwdlen = 1024; char pass[pwdlen]; char *ptr = pass; fprintf(stdout, "Title: "); fgets(title, 1024, stdin); user_default = get_default_username(); if (user_default == NULL) { fprintf(stdout, "Username: "); fgets(user, 1024, stdin); } else { fprintf(stdout, "Username: using default (%s)\n", user_default); snprintf(user, 1024, "%s", user_default); } fprintf(stdout, "Url: "); fgets(url, 1024, stdin); fprintf(stdout, "Notes: "); fgets(notes, 1024, stdin); my_getpass("Password (empty to generate new): ", &ptr, &pwdlen, stdin); if(strcmp(pass, "") == 0) generate_new_password(pass); strip_newline_str(title); strip_newline_str(user); strip_newline_str(url); strip_newline_str(notes); Entry_t *entry = entry_new(title, user, url, pass, notes); if(!entry) return false; if(!db_insert_entry(entry)) { fprintf(stderr, "Failed to add a new entry.\n"); return false; } entry_free(entry); if(auto_encrypt == 1) auto_enc(); return true; } bool edit_entry(int id, int auto_encrypt) { if(!has_active_database()) { fprintf(stderr, "No decrypted database found.\n"); return false; } Entry_t *entry = db_get_entry_by_id(id); if(!entry) return false; if(entry->id == -1) { printf("Nothing found.\n"); entry_free(entry); return false; } char title[1024] = {0}; char user[1024] = {0}; char url[1024] = {0}; char notes[1024] = {0}; size_t pwdlen = 1024; char pass[pwdlen]; char *ptr = pass; bool update = false; fprintf(stdout, "Current title %s\n", entry->title); fprintf(stdout, "New title: "); fgets(title, 1024, stdin); fprintf(stdout, "Current username %s\n", entry->user); fprintf(stdout, "New username: "); fgets(user, 1024, stdin); fprintf(stdout, "Current url %s\n", entry->url); fprintf(stdout, "New url: "); fgets(url, 1024, stdin); fprintf(stdout, "Current notes %s\n", entry->notes); fprintf(stdout, "New note: "); fgets(notes, 1024, stdin); fprintf(stdout, "Current password %s\n", entry->password); my_getpass("New password (empty to generate new): ", &ptr, &pwdlen, stdin); if (strcmp(pass, "") == 0) { generate_new_password(pass); } strip_newline_str(title); strip_newline_str(user); strip_newline_str(url); strip_newline_str(notes); //We need to manually free the entry field to avoid dublicate allocation //caused by the database query. Password field will be always allocated, other //are freed if necessary below free(entry->password); if(title[0] != '\0') { free(entry->title); entry->title = strdup(title); update = true; } if(user[0] != '\0') { free(entry->user); entry->user = strdup(user); update = true; } if(url[0] != '\0') { free(entry->url); entry->url = strdup(url); update = true; } if(notes[0] != '\0') { free(entry->notes); entry->notes = strdup(notes); update = true; } if(pass[0] != '\0') { entry->password = strdup(pass); update = true; } if(update) db_update_entry(entry->id, entry); entry_free(entry); if(auto_encrypt == 1) auto_enc(); return true; } bool copy_entry(int id) { if(!has_active_database()) { fprintf(stderr, "No decrypted database found.\n"); return false; } Entry_t *old = db_get_entry_by_id(id); if(!old) return false;; if(old->id == -1) { printf("Nothing found with id %d.\n", id); entry_free(old); return false; } Entry_t *new = entry_dup(old); if(!db_insert_entry(new)) { entry_free(old); entry_free(new); fprintf(stderr, "Failed to add a new entry.\n"); return false; } entry_free(old); entry_free(new); return true; } bool remove_entry(int id, int auto_encrypt) { if(!has_active_database()) { fprintf(stderr, "No decrypted database found.\n"); return false; } bool changes = false; if(db_delete_entry(id, &changes)) { if(changes == true) fprintf(stdout, "Entry was deleted from the database.\n"); else fprintf(stdout, "No entry with id %d was found.\n", id); return true; } if(auto_encrypt == 1) auto_enc(); return false; } void list_by_id(int id, int show_password, int auto_encrypt, int as_qrcode) { if(!has_active_database()) { fprintf(stderr, "No decrypted database found.\n"); return; } Entry_t *entry = db_get_entry_by_id(id); if(!entry) return; if(entry->id == -1) { printf("Nothing found with id %d.\n", id); entry_free(entry); return; } print_entry(entry, show_password, as_qrcode); entry_free(entry); if(auto_encrypt == 1) auto_enc(); } /* Loop through all entries in the database and output them to stdout. * Latest count points out how many latest items we may want to show. * If latest_count if -1, display all items. */ void list_all(int show_password, int auto_encrypt, int latest_count) { if(!has_active_database()) { fprintf(stderr, "No decrypted database found.\n"); return; } Entry_t *entry = db_get_list(latest_count); if(!entry) return; /* Because of how sqlite callbacks work, we need to initialize * the list with dummy data. * Skip the dummy data to the next entry in the list */ Entry_t *head = entry->next; while(head != NULL) { print_entry(head, show_password, 0); head = head->next; } if(auto_encrypt == 1) auto_enc(); entry_free(entry); } /* Uses sqlite "like" query and prints results to stdout. * This is ok for the command line version of Ylva. However * better design is needed _if_ GUI version will be developed. */ void find(const char *search, int show_password, int auto_encrypt) { if(!has_active_database()) { fprintf(stderr, "No decrypted database found.\n"); return; } Entry_t *list = db_find(search); if(!list) return; Entry_t *head = list->next; while(head != NULL) { print_entry(head, show_password, 0); head = head->next; } if(auto_encrypt == 1) auto_enc(); entry_free(list); } void find_regex(const char *regex, int show_password) { Entry_t *list = db_get_list(-1); Entry_t *head = list->next; regex_find(head, regex, show_password); entry_free(list); } void show_current_db_path() { char *path = NULL; path = read_active_database_path(); if(!path) { fprintf(stderr, "No decrypted database exist.\n"); } else { fprintf(stdout, "%s\n", path); free(path); } } void set_use_db(const char *path) { if(has_active_database()) { fprintf(stdout, "Type password to encrypt existing active database.\n"); if(!encrypt_database()) return; } if(is_file_encrypted(path)) { fprintf(stdout, "Decrypt %s.\n", path); if(!decrypt_database(path)) return; } write_active_database_path(path); } void show_latest_entries(int show_password, int auto_encrypt, int count) { list_all(show_password, auto_encrypt, count); } ylva-1.7/src/cmd_ui.h000066400000000000000000000014501411710156500145350ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #ifndef __CMD_UI_H #define __CMD_UI_H void init_database(const char *path, int force, int auto_encrypt); bool add_new_entry(int auto_encrypt); bool edit_entry(int id, int auto_encrypt); bool remove_entry(int id, int auto_encrypt); bool copy_entry(int id); void list_by_id(int id, int show_password, int auto_encrypt, int as_qrcode); void list_all(int show_password, int auto_encrypt, int latest_count); void find(const char *search, int show_password, int auto_encrypt); void find_regex(const char *regex, int show_password); void show_current_db_path(); void set_use_db(const char *path); void show_latest_entries(int show_password, int auto_encrypt, int count); bool decrypt_database(const char *path); bool encrypt_database(); #endif ylva-1.7/src/crypto.c000066400000000000000000000273201411710156500146140ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include "crypto.h" #include "utils.h" //Our magic number that's written into the //encrypted file. Used to determine if the file //is encrypted. static const int MAGIC_HEADER = 0x33497546; //Function generates random data from /dev/urandom //Parameter size is how much random data caller //wants to generate. Caller must free the return value. //Returns the data or NULL on failure. static char *generate_random_data(int size) { char *data = NULL; FILE *frnd = NULL; data = tmalloc(size * sizeof(char)); frnd = fopen("/dev/urandom", "r"); if(!frnd) { fprintf(stderr, "Cannot open /dev/urandom\n"); free(data); return NULL; } fread(data, size, 1, frnd); fclose(frnd); return data; } //Generate key from passphrase. If oldsalt is NULL, new salt is created. //ok is set to true on success, false on failure static Key_t generate_key(const char *passphrase, char *old_salt, bool *ok) { char *salt = NULL; int iterations = 200000; Key_t key = { {0} }; int success; char *resultbytes[KEY_SIZE]; if(old_salt == NULL) salt = generate_random_data(SALT_SIZE); else { salt = tmalloc(SALT_SIZE); memmove(salt, old_salt, SALT_SIZE); } if(!salt) { *ok = false; return key; } success = PKCS5_PBKDF2_HMAC(passphrase, strlen(passphrase), (unsigned char*)salt, SALT_SIZE, iterations, EVP_sha256(), KEY_SIZE, (unsigned char*)resultbytes); if(success == 0) { free(salt); *ok = false; return key; } memmove(key.data, resultbytes, KEY_SIZE); memmove(key.salt, salt, SALT_SIZE); free(salt); *ok = true; return key; } //Function appends ext to the orig string. //Returns new string on success, NULL on failure. //Caller must free the return value. static char *get_output_filename(const char *orig, const char *ext) { char *path = NULL; size_t len; len = strlen(orig) + strlen(ext) + 1; path = tmalloc(len * sizeof(char)); strcpy(path, orig); strcat(path, ext); return path; } static bool encrypt_decrypt(unsigned char *data_in, int data_in_len, FILE *out, unsigned char *key, unsigned char *iv, int is_encrypt) { EVP_CIPHER_CTX *ctx; unsigned char *out_buffer = NULL; int output_len = 0; int output_len_final = 0; int cipher_block_size; ctx = EVP_CIPHER_CTX_new(); if(EVP_CipherInit(ctx, EVP_aes_256_ctr(), key, iv, is_encrypt) != 1) { fprintf(stderr, "Unable to initialize AES.\n"); return false; } cipher_block_size = EVP_CIPHER_CTX_block_size(ctx); out_buffer = tmalloc(data_in_len +(cipher_block_size)); if(EVP_CipherUpdate(ctx, out_buffer, &output_len, data_in, data_in_len) != 1) { fprintf(stderr, "Unable to process data.\n"); free(out_buffer); EVP_CIPHER_CTX_free(ctx); return false; } if(EVP_CipherFinal(ctx, out_buffer + output_len, &output_len_final) != 1) { fprintf(stderr, "Unable to finalize.\n"); free(out_buffer); EVP_CIPHER_CTX_free(ctx); return false; } fwrite(out_buffer, sizeof(unsigned char), output_len + output_len_final, out); free(out_buffer); EVP_CIPHER_CTX_free(ctx); return true; } static unsigned char *hmac_data(const void *key, int key_len, unsigned char *data, int data_len, unsigned char *result, int *res_len) { return HMAC(EVP_sha512(), key, key_len, data, data_len, result, (unsigned int *)res_len); } //Calculates hmac from the content of fp and writes the hash //into end of the file. Caller must close fp. static bool calculate_and_write_hmac(FILE *fp, const void *key) { unsigned char *hmac_sha512 = NULL; char *cipher_buffer = NULL; int len = 0; int cipherlen; fseek(fp, 0, SEEK_END); cipherlen = ftell(fp); //Cursor back to beginning so we can read the data for hmac fseek(fp, 0, SEEK_SET); cipher_buffer = tmalloc(cipherlen * sizeof(char)); fread(cipher_buffer, sizeof(char), cipherlen, fp); hmac_sha512 = tmalloc(HMAC_SHA512_SIZE); hmac_data(key, KEY_SIZE, (unsigned char *)cipher_buffer,cipherlen, (unsigned char *)hmac_sha512, &len); fseek(fp, 0, SEEK_END); fwrite(hmac_sha512, 1, HMAC_SHA512_SIZE, fp); free(cipher_buffer); free(hmac_sha512); return true; } //Function assumes that fp cursor is in the right place //to read the stored hmac from the file. static bool read_and_verify_hmac(const char *path, char *hmac, const void *key) { char *new_hmac = tmalloc(HMAC_SHA512_SIZE); char *buffer; int len; int offset; int result; FILE *fp = NULL; bool retval = false; fp = fopen(path, "r"); fseek(fp, 0, SEEK_END); len = ftell(fp); //calculate the actual hmacced data size offset = len - HMAC_SHA512_SIZE; fseek(fp, 0, SEEK_SET); buffer = tmalloc(offset * sizeof(char)); //read whole file until hmac fread(buffer, sizeof(char), offset, fp); hmac_data(key, KEY_SIZE, (unsigned char*)buffer, offset, (unsigned char*)new_hmac, &result); if(CRYPTO_memcmp(hmac, new_hmac, HMAC_SHA512_SIZE) == 0) retval = true; free(new_hmac); free(buffer); fclose(fp); return retval; } //This function really just checks is the file //written using Ylva. bool is_file_encrypted(const char *path) { FILE *fp = NULL; int data; fp = fopen(path, "r"); if(!fp) { fprintf(stderr, "Failed to open file.\n"); return false; } fseek(fp, 0, SEEK_END); int len = ftell(fp); int offset = sizeof(int) + IV_SIZE + SALT_SIZE + HMAC_SHA512_SIZE; rewind(fp); fseek(fp, len - offset, SEEK_CUR); //Read our magic header fread((void*)&data, sizeof(MAGIC_HEADER), 1, fp); fclose(fp); if(data != MAGIC_HEADER) return false; return true; } bool encrypt_file(const char *passphrase, const char *path) { bool ok; char *iv = NULL; FILE *plain = NULL; FILE *cipher_fp = NULL; char *output_filename = NULL; char *plain_data = NULL; if(is_file_encrypted(path)) { fprintf(stderr, "File is already encrypted.\n"); return false; } Key_t key = generate_key(passphrase, NULL, &ok); if(!ok) { fprintf(stderr, "Key derivation failed.\n"); return false; } iv = generate_random_data(IV_SIZE); if(!iv) { fprintf(stderr, "Initialization vector generation failed.\n"); return false; } plain = fopen(path, "r"); if(!plain) { fprintf(stderr, "Unable to open %s\n", path); free(iv); return false; } fseek(plain, 0, SEEK_END); size_t plain_len = ftell(plain); fseek(plain, 0, SEEK_SET); plain_data = tmalloc(plain_len * sizeof(char)); fread(plain_data, sizeof(char), plain_len, plain); fclose(plain); output_filename = get_output_filename(path, ".ylva"); if(!output_filename) { fprintf(stderr, "Unable to create output filename.\n"); free(iv); free(plain_data); return false; } cipher_fp = fopen(output_filename, "w"); if(!cipher_fp) { fprintf(stderr, "Unable to open %s for writing.\n", output_filename); free(iv); free(output_filename); free(plain_data); return false; } //perform the actual encryption encrypt_decrypt((unsigned char*)plain_data, plain_len, cipher_fp, (unsigned char *)key.data, (unsigned char *)iv, YLVA_MODE_ENCRYPT); //write iv etc. into the end of the file fwrite((void*)&MAGIC_HEADER, sizeof(MAGIC_HEADER), 1, cipher_fp); fwrite(iv, 1, IV_SIZE, cipher_fp); fwrite(key.salt, 1, SALT_SIZE, cipher_fp); //Close the file pointer, to sync the data, before reading it again //for the hmac calculation fclose(cipher_fp); //Open the file again for reading and writing cipher_fp = fopen(output_filename, "r+"); if(!calculate_and_write_hmac(cipher_fp, key.data)) { free(iv); free(output_filename); free(plain_data); fclose(cipher_fp); return false; } //Finally remove the plain file if(remove(path) != 0) fprintf(stderr, "WARNING: Error deleting plain file %s.", path); //And rename our ciphered file back to the original name rename(output_filename, path); free(output_filename); free(plain_data); free(iv); fclose(cipher_fp); return true; } bool decrypt_file(const char *passphrase, const char *path) { bool ok; char *iv = NULL; char *salt = NULL; FILE *plain = NULL; FILE *cipher = NULL; char *output_filename = NULL; char *cipher_data = NULL; char *hmac; if(!is_file_encrypted(path)) { fprintf(stderr, "File is already decrypted or malformed?\n"); return false; } iv = tmalloc(IV_SIZE); salt = tmalloc(SALT_SIZE); cipher = fopen(path, "r"); if(!cipher) { fprintf(stderr, "Unable to open %s for reading.\n", path); free(iv); free(salt); return false; } fseek(cipher, 0, SEEK_END); int len = ftell(cipher); int offset = len - (sizeof(int) + IV_SIZE + SALT_SIZE + HMAC_SHA512_SIZE); rewind(cipher); hmac = tmalloc(HMAC_SHA512_SIZE); fseek(cipher, offset, SEEK_CUR); //Skip the magic header fseek(cipher, sizeof(int), SEEK_CUR); //iv, salt fread(iv, IV_SIZE, 1, cipher); fread(salt, SALT_SIZE, 1, cipher); fread(hmac, HMAC_SHA512_SIZE, 1, cipher); Key_t key = generate_key(passphrase, salt, &ok); if(!ok) { fprintf(stderr, "Key derivation failed.\n"); free(iv); free(salt); free(hmac); fclose(cipher); return false; } fclose(cipher); if(!read_and_verify_hmac(path, hmac, key.data)) { fprintf(stderr, "Invalid password or tampered data. Aborted.\n"); free(iv); free(salt); free(hmac); return false; } cipher = fopen(path, "r"); cipher_data = tmalloc(offset * sizeof(char)); //read all data, skip header, salt, iv fread(cipher_data, sizeof(char), offset, cipher); fclose(cipher); output_filename = get_output_filename(path, ".plain"); if(!output_filename) { fprintf(stderr, "Unable to create output filename.\n"); free(iv); free(salt); free(cipher_data); free(hmac); return false; } plain = fopen(output_filename, "w"); if(!plain) { fprintf(stderr, "Unable to open %s for writing.\n", output_filename); free(iv); free(salt); free(output_filename); free(cipher_data); free(hmac); return false; } encrypt_decrypt((unsigned char*)cipher_data, offset, plain, (unsigned char *)key.data, (unsigned char *)iv, YLVA_MODE_DECRYPT); //Finally remove the cipher file if(remove(path) != 0) { fprintf(stderr, "WARNING: Error deleting file %s.", path); } //And rename our ciphered file back to the original name rename(output_filename, path); set_file_owner_rw(path); free(output_filename); free(iv); free(salt); fclose(plain); free(cipher_data); free(hmac); return true; } ylva-1.7/src/crypto.h000066400000000000000000000010361411710156500146150ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #ifndef __CRYPTO_H #define __CRYPTO_H #define KEY_SIZE (32) //256 bits #define IV_SIZE (16) //128 bits #define SALT_SIZE (64) //512 bits #define HMAC_SHA512_SIZE (64) #define YLVA_MODE_DECRYPT (0) #define YLVA_MODE_ENCRYPT (1) typedef struct Key { char data[32]; char salt[64]; } Key_t; bool encrypt_file(const char *passphrase, const char *path); bool decrypt_file(const char *passphrase, const char *path); bool is_file_encrypted(const char *path); #endif ylva-1.7/src/db.c000066400000000000000000000304631411710156500136630ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #define _XOPEN_SOURCE 700 #include #include #include #include #include #include "entry.h" #include "db.h" #include "utils.h" /* sqlite callbacks */ static int cb_check_integrity(void *notused, int argc, char **argv, char **column_name); static int cb_get_by_id(void *entry, int argc, char **argv, char **column_name); static int cb_list_all(void *entry, int argc, char **argv, char **column_name); static int cb_find(void *entry, int argc, char **argv, char **column_name); /*Run integrity check for the database to detect *malformed and corrupted databases. Returns true *if everything is ok, false if something is wrong. */ static bool db_check_integrity(const char *path) { sqlite3 *db; char *err = NULL; int retval; char *sql; retval = sqlite3_open(path, &db); if(retval) { fprintf(stderr, "Can't initialize: %s\n", sqlite3_errmsg(db)); return false; } sql = "pragma integrity_check;"; retval = sqlite3_exec(db, sql, cb_check_integrity, 0, &err); if(retval != SQLITE_OK) { fprintf(stderr, "SQL error: %s\n", err); sqlite3_free(err); sqlite3_close(db); return false; } sqlite3_close(db); return true; } bool db_init_new(const char *path) { sqlite3 *db; char *err = NULL; int rc = sqlite3_open(path, &db); if(rc != SQLITE_OK) { fprintf(stderr, "Failed to initialize database: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return false; } char *query = "create table entries" "(id integer primary key, title text, user text, url text," "password text, notes text," "timestamp date default (datetime('now','localtime')));"; rc = sqlite3_exec(db, query, 0, 0, &err); if(rc != SQLITE_OK) { fprintf(stderr, "Error: %s\n", err); sqlite3_free(err); sqlite3_close(db); return false; } sqlite3_close(db); set_file_owner_rw(path); return true; } bool db_insert_entry(Entry_t *entry) { sqlite3 *db; char *err = NULL; char *path = NULL; path = read_active_database_path(); if(!path) { fprintf(stderr, "Error getting database path\n"); return false; } if(!db_check_integrity(path)) { fprintf(stderr, "Corrupted database. Abort.\n"); free(path); return 0; } int rc = sqlite3_open(path, &db); if(rc != SQLITE_OK) { fprintf(stderr, "Failed to initialize database: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); free(path); return false; } char *query = sqlite3_mprintf("insert into entries(title, user, url, password, notes)" "values('%q','%q','%q','%q','%q')", entry->title, entry->user, entry->url, entry->password, entry->notes); rc = sqlite3_exec(db, query, NULL, 0, &err); if(rc != SQLITE_OK) { fprintf(stderr, "Error: %s\n", err); sqlite3_free(err); sqlite3_free(query); sqlite3_close(db); free(path); return false; } sqlite3_free(query); sqlite3_close(db); free(path); return true; } bool db_update_entry(int id, Entry_t *new_entry) { sqlite3 *db; char *err = NULL; char *path = NULL; path = read_active_database_path(); if(!path) { fprintf(stderr, "Error getting database path\n"); return false; } if(!db_check_integrity(path)) { fprintf(stderr, "Corrupted database. Abort.\n"); free(path); return 0; } int rc = sqlite3_open(path, &db); if(rc != SQLITE_OK) { fprintf(stderr, "Failed to initialize database: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); free(path); return false; } char *query = sqlite3_mprintf("update entries set title='%q'," "user='%q'," "url='%q'," "password='%q'," "notes='%q',timestamp=datetime('now','localtime') where id=%d;", new_entry->title, new_entry->user, new_entry->url, new_entry->password, new_entry->notes,id); rc = sqlite3_exec(db, query, NULL, 0, &err); if(rc != SQLITE_OK) { fprintf(stderr, "Error: %s\n", err); sqlite3_free(err); sqlite3_free(query); sqlite3_close(db); free(path); return false; } sqlite3_free(query); sqlite3_close(db); free(path); return true; } /*Get entry which has the wanted id. * Caller must free the return value. */ Entry_t *db_get_entry_by_id(int id) { char *path = NULL; sqlite3 *db; int rc; char *query; char *err = NULL; Entry_t *entry = NULL; path = read_active_database_path(); if(!path) { fprintf(stderr, "Error getting database path\n"); return NULL; } if(!db_check_integrity(path)) { fprintf(stderr, "Corrupted database. Abort.\n"); free(path); return NULL; } rc = sqlite3_open(path, &db); if(rc != SQLITE_OK) { fprintf(stderr, "Error %s\n", sqlite3_errmsg(db)); free(path); return NULL; } entry = entry_new_empty(); query = sqlite3_mprintf("select id,title,user,url,password,notes," "timestamp from entries where id=%d;", id); /* Set id to minus one by default. If query finds data * we set the id back to the original one in the callback. * We can uses this to easily check if we have valid data in the structure. */ entry->id = -1; rc = sqlite3_exec(db, query, cb_get_by_id, entry, &err); if(rc != SQLITE_OK) { fprintf(stderr, "Error: %s\n", err); sqlite3_free(err); sqlite3_free(query); free(path); return NULL; } sqlite3_free(query); sqlite3_close(db); free(path); return entry; } /* Returns true on success, false on failure. * Parameter changes is set to true if entry with given * id was found and deleted. */ bool db_delete_entry(int id, bool *changes) { char *path = NULL; sqlite3 *db; int rc; char *query; char *err = NULL; int count; path = read_active_database_path(); if(!path) { fprintf(stderr, "Error getting database path\n"); return false; } if(!db_check_integrity(path)) { fprintf(stderr, "Corrupted database. Abort.\n"); free(path); return false; } rc = sqlite3_open(path, &db); if(rc != SQLITE_OK) { fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); free(path); return false; } query = sqlite3_mprintf("delete from entries where id=%d;", id); rc = sqlite3_exec(db, query, NULL, 0, &err); if(rc != SQLITE_OK) { fprintf(stderr, "Error: %s\n", err); sqlite3_free(err); sqlite3_free(query); sqlite3_close(db); free(path); return false; } count = sqlite3_changes(db); if(count > 0) *changes = true; sqlite3_free(query); sqlite3_close(db); free(path); return true; } /* Get latest count of entries pointed by count_latest. * -1 to get everything. -2 to get everything ordered by date */ Entry_t *db_get_list(int count_latest) { char *path = NULL; char *err = NULL; sqlite3 *db; char *query = NULL; if(count_latest < 0 && count_latest != -1 && count_latest != -2) { fprintf(stderr, "Invalid parameter \n"); return NULL; } path = read_active_database_path(); if(!path) { fprintf(stderr, "Error getting database path\n"); return NULL; } if(!db_check_integrity(path)) { fprintf(stderr, "Corrupted database. Abort.\n"); free(path); return NULL; } int rc = sqlite3_open(path, &db); if(rc != SQLITE_OK) { fprintf(stderr, "Error: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); free(path); return NULL; } /* Fill our list with dummy data */ Entry_t *entry = entry_new("dummy", "dummy", "dummy", "dummy", "dummy"); /* Get all data or a defined count */ if(count_latest == -1) query = "select * from entries;"; else if(count_latest == -2) query = sqlite3_mprintf("select * from entries order by datetime(timestamp) desc"); else query = sqlite3_mprintf("select * from entries order by datetime(timestamp) desc limit %d", count_latest); rc = sqlite3_exec(db, query, cb_list_all, entry, &err); if(rc != SQLITE_OK) { fprintf(stderr, "Error: %s\n", err); sqlite3_free(err); if(count_latest != -1 && query != NULL) sqlite3_free(query); sqlite3_close(db); free(path); return NULL; } sqlite3_close(db); free(path); return entry; } Entry_t *db_find(const char *search) { char *path = NULL; char *err = NULL; sqlite3 *db; path = read_active_database_path(); if(!path) { fprintf(stderr, "Error getting database path\n"); return NULL; } if(!db_check_integrity(path)) { fprintf(stderr, "Corrupted database. Abort.\n"); free(path); return NULL; } int rc = sqlite3_open(path, &db); if(rc != SQLITE_OK) { fprintf(stderr, "Error: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); free(path); return NULL; } /* Fill our list with dummy data */ Entry_t *entry = entry_new("dummy", "dummy", "dummy", "dummy", "dummy"); /* Search the same search term from each column we're might be interested in. */ char *query = sqlite3_mprintf("select * from entries where title like '%%%q%%' " "or user like '%%%q%%' " "or url like '%%%q%%' " "or notes like '%%%q%%';", search, search, search, search); rc = sqlite3_exec(db, query, cb_find, entry, &err); if(rc != SQLITE_OK) { fprintf(stderr, "Error: %s\n", err); sqlite3_free(err); sqlite3_free(query); sqlite3_close(db); free(path); return NULL; } sqlite3_free(query); sqlite3_close(db); free(path); return entry; } static int cb_check_integrity(void *notused, int argc, char **argv, char **column_name) { for(int i = 0; i < argc; i++) { if(strcmp(column_name[i], "integrity_check") == 0) { char *result = argv[i]; if(strcmp(result, "ok") != 0) return 1; } } return 0; } static int cb_list_all(void *entry, int argc, char **argv, char **column_name) { Entry_t *one_entry = entry_add(entry, argv[1], argv[2], argv[3], argv[4], argv[5]); one_entry->id = atoi(argv[0]); one_entry->stamp = strdup(argv[6]); return 0; } static int cb_find(void *entry, int argc, char **argv, char **column_name) { Entry_t *one_entry = entry_add(entry, argv[1], argv[2], argv[3], argv[4], argv[5]); one_entry->id = atoi(argv[0]); one_entry->stamp = strdup(argv[6]); return 0; } static int cb_get_by_id(void *entry, int argc, char **argv, char **column_name) { /*Let's not allow NULLs*/ if (argv[0] == NULL) { return 1; } if (argv[1] == NULL) { return 1; } if (argv[2] == NULL) { return 1; } if (argv[3] == NULL) { return 1; } if (argv[4] == NULL) { return 1; } if (argv[5] == NULL) { return 1; } if (argv[6] == NULL) { return 1; } ((Entry_t *)entry)->id = atoi(argv[0]); ((Entry_t *)entry)->title = strdup(argv[1]); ((Entry_t *)entry)->user = strdup(argv[2]); ((Entry_t *)entry)->url = strdup(argv[3]); ((Entry_t *)entry)->password = strdup(argv[4]); ((Entry_t *)entry)->notes = strdup(argv[5]); ((Entry_t *)entry)->stamp = strdup(argv[6]); ((Entry_t *)entry)->next = NULL; return 0; } ylva-1.7/src/db.h000066400000000000000000000006061411710156500136640ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #ifndef __DB_H #define __DB_H bool db_init_new(const char *path); bool db_insert_entry(Entry_t *entry); bool db_update_entry(int id, Entry_t *new_entry); bool db_delete_entry(int id, bool *changes); Entry_t *db_get_entry_by_id(int id); Entry_t *db_get_list(int count_latest); Entry_t *db_find(const char *search); #endif ylva-1.7/src/entry.c000066400000000000000000000036611411710156500144370ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #define _XOPEN_SOURCE 700 #include #include #include #include "entry.h" #include "utils.h" /* Allocate and return a new entry containing data. Called must free the return value. */ Entry_t *entry_new(const char *title, const char *user, const char *url, const char *password, const char *notes) { Entry_t *new = NULL; new = tmalloc(sizeof(struct _entry)); new->title = strdup(title); new->user = strdup(user); new->url = strdup(url); new->password = strdup(password); new->notes = strdup(notes); new->stamp = NULL; new->next = NULL; return new; } Entry_t* entry_new_empty() { Entry_t* new = NULL; new = tmalloc(sizeof(struct _entry)); return new; } /* Add new entry to the end of the list. Returns the currently added entry */ Entry_t *entry_add(Entry_t *head, const char *title, const char *user, const char *url, const char *password, const char *notes) { if(head == NULL) { return head = entry_new(title, user, url, password, notes); } Entry_t *cur = head; /* Walk until we're in the end of the list */ while(cur->next != NULL) cur = cur->next; Entry_t *new = entry_new(title, user, url, password, notes); cur->next = new; return cur->next; } Entry_t *entry_dup(Entry_t *entry) { Entry_t *new; new = entry_new(entry->title, entry->user, entry->url, entry->password, entry->notes); return new; } void entry_free(Entry_t *entry) { Entry_t *tmp; while(entry != NULL) { tmp = entry->next; free(entry->title); free(entry->user); free(entry->url); free(entry->password); free(entry->notes); if(entry->stamp) free(entry->stamp); free(entry); entry = tmp; } } ylva-1.7/src/entry.h000066400000000000000000000012401411710156500144330ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #ifndef __ENTRY_H #define __ENTRY_H typedef struct _entry { int id; char *title; char *user; char *url; char *password; char *notes; char *stamp; struct _entry *next; } Entry_t; Entry_t *entry_new(const char *title, const char *user, const char *url, const char *password, const char *notes); Entry_t* entry_new_empty(); Entry_t *entry_add(Entry_t *head, const char *title, const char *user, const char *url, const char *password, const char *notes); Entry_t *entry_dup(Entry_t *entry); void entry_free(Entry_t *entry); #endif ylva-1.7/src/pwd-gen.c000066400000000000000000000035311411710156500146330ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include #include #include "utils.h" /*Generates random number between 0 and max. *Function should generate uniform distribution. */ static unsigned int rand_between(unsigned int min, unsigned int max) { uint32_t r; const unsigned int range = 1 + max - min; const unsigned int buckets = RAND_MAX / range; const unsigned int limit = buckets * range; /*Create equal size buckets all in a row, then fire randomly towards *the buckets until you land in one of them. All buckets are equally *likely. If you land off the end of the line of buckets, try again. */ do { if(RAND_bytes((unsigned char *)&r, sizeof r) == 0) { ERR_print_errors_fp(stderr); abort(); } } while (r >= limit); return min + (r / buckets); } /* Simply generate secure password * and output it to the stdout. Uses OpenSSL RAND_bytes. * * Caller must free the return value. */ char *generate_password(int length) { if(length < 1 || length > RAND_MAX) return NULL; char *pass = NULL; char *alpha = "abcdefghijklmnopqrstuvwxyz" \ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "0123456789?)(/%#!?)="; unsigned int max; unsigned int number; RAND_poll(); if(RAND_status() != 1) fprintf(stdout, "Warning, random number generator not seeded.\n"); max = strlen(alpha) - 1; pass = tmalloc((length + 1) * sizeof(char)); for(int j = 0; j < length; j++) { number = rand_between(0, max); pass[j] = alpha[number]; } pass[length] = '\0'; fprintf(stdout, "%s\n", pass); return pass; } ylva-1.7/src/pwd-gen.h000066400000000000000000000002311411710156500146320ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #ifndef __PWD_GEN_H #define __PWD_GEN_H char *generate_password(int length); #endif ylva-1.7/src/qr.c000066400000000000000000000035431411710156500137170ustar00rootroot00000000000000/* * Copyright (C) 2019-2021 Niko Rosvall */ #define _XOPEN_SOURCE 700 #include #include #include #include #include #include "qr.h" #include "utils.h" // Prints the given QR Code to the console. static void printQr(const uint8_t qrcode[]) { printf(QR_FG_COLOR QR_BG_COLOR); int size = qrcodegen_getSize(qrcode); int border = 4; printf("\n"); for (int y = -border; y < size + border; y++) { for (int x = -border; x < size + border; x++) { fputs((qrcodegen_getModule(qrcode, x, y) ? "\u2585\u2585" : " "), stdout); } printf("\n"); } printf(COLOR_DEFAULT); printf("\n"); } void print_entry_as_qr(Entry_t *entry) { enum qrcodegen_Ecc error_level = qrcodegen_Ecc_LOW; uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX]; uint8_t tmp_buffer[qrcodegen_BUFFER_LEN_MAX]; char *data = NULL; size_t data_len = strlen(entry->title) + strlen(entry->user) + strlen(entry->url) + strlen(entry->password) + strlen(entry->notes); //Allocate space for the entry data plus five newline characters and for the \0 data_len += 6; data = tmalloc((data_len) * sizeof(char)); if( (snprintf(data, data_len, "%s\n%s\n%s\n%s\n%s\n", entry->title, entry->user, entry->url, entry->password, entry->notes) ) < 0 ) { fprintf(stderr, "Unable to combine entry data.\n"); free(data); return; } bool ok = qrcodegen_encodeText(data, tmp_buffer, qrcode, error_level, qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true); if (ok) printQr(qrcode); else fprintf(stderr, "Unable to generate QR code.\n"); free(data); } ylva-1.7/src/qr.h000066400000000000000000000004061411710156500137170ustar00rootroot00000000000000/* * Copyright (C) 2019-2021 Niko Rosvall */ #ifndef __QR_H #define __QR_H #include "entry.h" //fonts color #define QR_FG_COLOR "\33[30m" //background color #define QR_BG_COLOR "\33[107m" void print_entry_as_qr(Entry_t *entry); #endif ylva-1.7/src/regexfind.c000066400000000000000000000022771411710156500152530ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #define _XOPEN_SOURCE 700 /* Make only POSIX.2 regexp functions available */ #define _POSIX_C_SOURCE 200112L #include #include #include "entry.h" #include "utils.h" #include "regexfind.h" void regex_find(Entry_t *head, const char *search, int show_password) { regex_t regex; int retval; if(regcomp(®ex, search, REG_NOSUB) != 0) { fprintf(stderr, "Invalid regular expression.\n"); return; } while(head != NULL) { if((retval = regexec(®ex, head->title, 0, NULL, 0)) == 0) print_entry(head, show_password, 0); else if((retval = regexec(®ex, head->user, 0, NULL, 0)) == 0) print_entry(head, show_password, 0); else if((retval = regexec(®ex, head->url, 0, NULL, 0)) == 0) print_entry(head, show_password, 0); else if((retval = regexec(®ex, head->notes, 0, NULL, 0)) == 0) print_entry(head, show_password, 0); else if((retval = regexec(®ex, head->stamp, 0, NULL, 0)) == 0) print_entry(head, show_password, 0); head = head->next; } regfree(®ex); } ylva-1.7/src/regexfind.h000066400000000000000000000003061411710156500152470ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #ifndef __REGEXFIND_H #define __REGEXFIND_H void regex_find(Entry_t *head, const char *search, int show_password); #endif ylva-1.7/src/utils.c000066400000000000000000000113651411710156500144360ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #define _XOPEN_SOURCE 700 #include #include #include #include #include #include "entry.h" #include "utils.h" #include "crypto.h" #include "qr.h" /* Function returns NULL if the environment variable YLVA_DEFAULT_USERNAME is not set. */ char* get_default_username() { char* username = getenv("YLVA_DEFAULT_USERNAME"); return username; } static char *get_output_color() { char *color = getenv("YLVA_COLOR"); if(color == NULL) return COLOR_DEFAULT; if(strcmp(color, "BLUE") == 0) return "\x1B[34m"; else if(strcmp(color, "RED") == 0) return "\x1B[31m"; else if(strcmp(color, "GREEN") == 0) return "\x1B[32m"; else if(strcmp(color, "YELLOW") == 0) return "\x1B[33m"; else if(strcmp(color, "MAGENTA") == 0) return "\x1B[35m"; else if(strcmp(color, "CYAN") == 0) return "\x1B[36m"; else if(strcmp(color, "WHITE") == 0) return "\x1B[37m"; else return COLOR_DEFAULT; /* Handle empty variable too */ } bool print_entry(Entry_t *entry, int show_password, int as_qrcode) { char *color = get_output_color(); fprintf(stdout, "=====================================================================\n"); if(as_qrcode == 1) { print_entry_as_qr(entry); } else { /* Set the color */ fprintf(stdout, "%s", color); fprintf(stdout, "ID: %d\n", entry->id); fprintf(stdout, "Title: %s\n", entry->title); fprintf(stdout, "User: %s\n", entry->user); fprintf(stdout, "Url: %s\n", entry->url); if(show_password == 1) fprintf(stdout, "Password: %s\n", entry->password); else fprintf(stdout, "Password: **********\n"); fprintf(stdout, "Notes: %s\n", entry->notes); fprintf(stdout, "Modified: %s\n", entry->stamp); /* Reset the color */ fprintf(stdout, "%s", COLOR_DEFAULT); } fprintf(stdout, "=====================================================================\n"); return 0; } bool file_exists(const char *path) { struct stat buf; if(stat(path, &buf) != 0) return false; return true; } /* Function checks that we have a valid path * in our ylva.open_db file and if the database is not * encrypted. */ bool has_active_database() { char *path = NULL; path = get_open_db_path_holder_filepath(); if(!path) return false; struct stat buf; if(stat(path, &buf) != 0) { free(path); return false; } //If the database is encrypted, it's not active so return false if(is_file_encrypted(path)) { free(path); return false; } free(path); return true; } /* Returns the path of ~/.ylva.open_db file. * Caller must free the return value */ char *get_open_db_path_holder_filepath() { char *home = NULL; char *path = NULL; home = getenv("HOME"); if(!home) return NULL; /* /home/user/.ylva.open_db */ path = tmalloc(sizeof(char) * (strlen(home) + 15)); strcpy(path, home); strcat(path, "/.ylva.open_db"); return path; } /* Reads and returns the path of currently decrypted * database. Caller must free the return value */ char *read_active_database_path() { FILE *fp = NULL; char *path = NULL; char *opendbholderpath = NULL; size_t len; path = get_open_db_path_holder_filepath(); if(!path) return NULL; fp = fopen(path, "r"); if(!fp) { free(path); return NULL; } /* We only need the first line from the file */ if(getline(&opendbholderpath, &len, fp) < 0) { if(opendbholderpath) free(opendbholderpath); fclose(fp); free(path); return NULL; } fclose(fp); free(path); return opendbholderpath; } void write_active_database_path(const char *db_path) { FILE *fp = NULL; char *path = NULL; path = get_open_db_path_holder_filepath(); if(!path) return; fp = fopen(path, "w"); if(!fp) { fprintf(stderr, "Error creating ~/.ylva.open_db file\n"); free(path); return; } fprintf(fp, "%s", db_path); fclose(fp); free(path); } //Simple malloc wrapper to prevent enormous error //checking every where in the code void *tmalloc(size_t size) { void *data = NULL; data = malloc(size); if(data == NULL) { fprintf(stderr, "Malloc failed. Abort.\n"); abort(); } return data; } void set_file_owner_rw(const char *path) { if(chmod(path, S_IRUSR | S_IWUSR) != 0) fprintf(stderr, "WARNING: Error changing permissions of file %s\n", path); } ylva-1.7/src/utils.h000066400000000000000000000010461411710156500144360ustar00rootroot00000000000000/* * Copyright (C) 2019-2021 Niko Rosvall */ #ifndef __UTILS_H #define __UTILS_H #include #include "entry.h" #define COLOR_DEFAULT "\x1B[0m" bool print_entry(Entry_t *entry, int show_password, int as_qrcode); char *get_open_db_path_holder_filepath(); void write_active_database_path(const char *db_path); char *read_active_database_path(); bool has_active_database(); void *tmalloc(size_t size); void set_file_owner_rw(const char *path); bool file_exists(const char *path); char* get_default_username(); #endif ylva-1.7/src/ylva.1000066400000000000000000000057021411710156500141650ustar00rootroot00000000000000.\" Manpage for Ylva. .\" Any errors or typos, contact niko@byteptr.com. .TH man 1 "11 Sep 2021" "1.7" "ylva man page" .SH NAME ylva \- command line password manager .SH SYNOPSIS ylva [FLAGS] .SH DESCRIPTION Ylva is a command line password manager program for Unix-like operating systems. .SH OPTIONS .IP "-i, --init " Initialize new database .IP "-E, --encrypt" Encrypt current database .IP "-D, --decrypt " Decrypt database .IP "-c, --copy " Copy an entry to a new entry .IP "-a, --add" Add new entry .IP "-p, --show-db-path" Show current database path .IP "-u, --use-db " Switch using another database .IP "-r, --remove " Remove entry pointed by id .IP "-f, --find " Search for entries .IP "-F, --regex " Search for entries with regular expressions .IP "-e, --edit " Edit entry pointed by id .IP "-l, --list-entry " List entry pointed by id .IP "-t, --show-latest [count]" Show latest entries, parameter count is optional. .IP "-A, --list-all" Show database statuses .IP "-v, --version" Show program version .IP "-g, --gen-password " Generate password .IP "-q, --quick " This is the same as running --show-passwords -f .IP "-h, --help" Show short help and exit .SH FLAGS .IP "--auto-encrypt" Automatically encrypt after exit .IP "--show-passwords" Show passwords in listings .IP "--show-qrcode" Show data as QR code in --list-entry .IP "--force" --force only works with --init option .SH EXAMPLES Create a new database: ylva --init "/path/to/file.db" .PP Open and decrypt database: ylva --decrypt "/path/to/existing/file.db" .PP Close and encrypt database: ylva --encrypt Ylva knows what database is currently active and encrypts it. Encrypting will ask you to type a master passphrase which is used for encryption. .PP Add an entry to an open database: ylva --add .PP If you want to export all entries to a file: ylva --show-passwords -A > file.txt .PP To show latest 10 entries: ylva --show-latest 10 .PP To show all entries ordered by date ylva --show-latest .SH COLORS Ylva supports colored output. To use colors, set an environment variable YLVA_COLOR with one of the following value: BLUE, RED, GREEN, YELLOW, MAGENTA, CYAN or WHITE. .SH DEFAULT USERNAME Ylva supports settings default username for entries. To enable it, set an environment variable YLVA_DEFAULT_USERNAME value to the wanted default. If you want Ylva to default to an empty username define YLVA_DEFAULT_USERNAME, but leave it empty. .SH NOTES Ylva does not have a concept of "change the master password". When you encrypt an open database using --encrypt you can type a master password. This password is then required to decrypt the database. You can change the master password every time when you encrypt the database, if you want to. .SH FILES .I $HOME/.ylva.lock .SH AUTHORS Written by Niko Rosvall. .SH COPYRIGHT Copyright (C) 2019-2021 Niko Rosvall .PP Released under MIT license. ylva-1.7/src/ylva.c000066400000000000000000000145711411710156500142530ustar00rootroot00000000000000/* * Copyright (C) 2019-2020 Niko Rosvall */ #include #include #include #include #include #include "cmd_ui.h" #include "entry.h" #include "db.h" #include "utils.h" #include "pwd-gen.h" #include "crypto.h" static int show_password = 0; static int force = 0; static int auto_encrypt = 0; static int show_as_qrcode = 0; static double v = 1.7; static void version() { printf("Ylva version %.1f\n", v); } static void usage() { #define HELP "\ SYNOPSIS\n\ \n\ ylva [flags] [options]\n\ \n\ OPTIONS\n\ \n\ -i --init Initialize new database\n\ -E --encrypt Encrypt the current password database\n\ -D --decrypt Decrypt password database\n\ -a --add Add new entry\n\ -c --copy Copy an entry\n\ -r --remove Remove entry pointed by id\n\ -p --show-db-path Show current database path\n\ -u --use-db Switch using another database\n\ -f --find Search entries\n\ -F --regex Search entries with regular expressions\n\ -e --edit Edit entry pointed by id\n\ -l --list-entry List entry pointed by id\n\ -t --show-latest [count] Show latest entries, count is optional\n\ -A --list-all List all entries\n\ -h --help Show short help and exit. This page\n\ -g --gen-password Generate password\n\ -q --quick This is the same as running\n\ --show-passwords -f\n\ \n\ -v --version Show version number of program\n\ \n\ FLAGS\n\ \n\ --auto-encrypt Automatically encrypt after exit\n\ --show-passwords Show passwords in listings\n\ --show-qrcode Show data as QR code in --list-entry\n\ --force Ignore everything and force operation\n\ --force only works with --init option\n\ \n\ For more information and examples see man ylva(1).\n\ \n\ AUTHORS\n\ Copyright (C) 2019-2021 Niko Rosvall \n\ " printf(HELP); } int main(int argc, char *argv[]) { int c; if(argc == 1) { usage(); return 0; } while(true) { static struct option long_options[] = { {"init", required_argument, 0, 'i'}, {"encrypt", no_argument, 0, 'E'}, {"decrypt", required_argument, 0, 'D'}, {"use-db", required_argument, 0, 'u'}, {"add", no_argument, 0, 'a'}, {"copy", required_argument, 0, 'c'}, {"remove", required_argument, 0, 'r'}, {"find", required_argument, 0, 'f'}, {"regex", required_argument, 0, 'F'}, {"edit", required_argument, 0, 'e'}, {"list-all", no_argument, 0, 'A'}, {"list-entry", required_argument, 0, 'l'}, {"gen-password", required_argument, 0, 'g'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, {"show-db-path", no_argument, 0, 'p'}, {"show-latest", no_argument, 0, 't'}, {"quick", required_argument, 0, 'q'}, {"auto-encrypt", no_argument, &auto_encrypt, 1 }, {"show-passwords", no_argument, &show_password, 1 }, {"show-qrcode", no_argument, &show_as_qrcode, 1 }, {"force", no_argument, &force, 1 }, {0, 0, 0, 0} }; int option_index = 0; c = getopt_long(argc, argv, "i:ED:u:ac:r:f:F:e:Al:g:hvptq:", long_options, &option_index); if(c == -1) break; switch(c) { case 0: /* Handle flags here automatically */ break; case 'i': init_database(optarg, force, auto_encrypt); break; case 'E': //encrypt encrypt_database(); break; case 'D': //decrypt decrypt_database(optarg); break; case 'u': set_use_db(optarg); case 'a': add_new_entry(auto_encrypt); break; case 'c': copy_entry(atoi(optarg)); break; case 'r': remove_entry(atoi(optarg), auto_encrypt); break; case 'f': find(optarg, show_password, auto_encrypt); break; case 'F': find_regex(optarg, show_password); break; case 'e': edit_entry(atoi(optarg), auto_encrypt); break; case 'A': list_all(show_password, auto_encrypt, -1); break; case 'l': list_by_id(atoi(optarg), show_password, auto_encrypt, show_as_qrcode); break; case 'g': { char *pass = generate_password(atoi(optarg)); if(pass != NULL) free(pass); break; } case 'h': usage(); break; case 'v': version(); break; case 'p': show_current_db_path(); break; case 't': { int count = -2; if(argv[optind]) { count = atoi(argv[optind]); } show_latest_entries(show_password, auto_encrypt, count); break; } case 'q': show_password = 1; find(optarg, show_password, auto_encrypt); break; case '?': usage(); break; } } return 0; } ylva-1.7/ylva.png000066400000000000000000000766011411710156500140300ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYs/`/`:tEXtSoftwarewww.inkscape.org<|IDATxxUٿ(ʧ*HޑtXzo[dZzLʤLNz*+ "=fl2y˼ulwN~{|11 \P%_#'S+/@LOOvaaaga$X\#@ D.w'>k333} / p's@?!_ $b~~Z k #r%ĵP_MuI߈[q "z/1`%DM!XD ?`GĄ E@Ud5оG_ (%.g'< `I&pa(1,Cs^'> akx\kqįd 5p!` }C+ C՘kqP27QGjqOCg71 T3c>{%%`IpBCW(9Jyߟ)!{NiX1W@ 9O3%x≋a0G]AĄJ?l ``I&bAe)p?T,I#9? ß!u|NkVcIQ:ͅ?/#|V#9j% x8O*[aP{I(>7T7sV T,z ?GP`IPei,90ѿaU? !F $Q|"#vjX9B ó>{$ @-<P4sK_9x>OQ?.IyLP$"q/>SQt"H1ğuq|#?/Klh$FX`l<ޛBAĵ=}~bn666&x@q`<==*u,ijlaa1^Љ@scd xgpy/g sK)U]jfg䖀]YR9?U?L[65@pTa;;7&wxl`hXn 'y iG!g:wфW J: TASr> x89!/ V᭍)qWIzL!z'XL_hodsssrw;925s{)=^)aSrJ= Sd(ѣ^ |]O^/E{)?D2PrJuv3=G#HwEQrk/-m$a0۲Z(M~$1@L$G2F)^O1 z y ]1!J ["?,i}/f֏N ʺF$p 0x(e޾`^ßSLs31кUhHSBBe'&\I?,CFļW1F g9VL@gj  /LnX k @ٲU!Z:ľc0V 'ML844N8F&dC*Kx "'&&X^[u),}c@_"foSp|Rc v˫jTQclV`QkbAA 0HESew˜hU-> _dB# 0TKݽ}8Z13-?AL zjT/ޡpzzFUfggt `=?Ǚ[(VvtG{0DO` f?,;XbxdD{c*=[֎NI_tJ_Y]8`)@kllnӜp :ݣ˫ļEBCդ{X'8c,J|(&QӤd90 ZzY8A^.`L*Uh2C yGaArkZ8}IԑK@'im1ڮ441 Z9(44/E Դcv%0 !fgk^J=<Њ j ^TڿNMM4vbh>91xhEzss=}b>4.'!B4@+!ǏIJʪܼ@x 1fuJ1FOOO~Zhzu%R>h_f?kQ[$1aW/4ju'a 3W@0.fJ\qP {u'چf1?-?FL"g KTA0 jr#4gL-1H1Z84*؂]Ј[4&tpZ}]B`c vxYHȵu-ACCc|-:Oßm*3k!k]=QP#mv Q\!Tܺ@{*Xӄ\Nt}Y2ӿ,0J|xo|JPuW^ƘB]aoj P.g,nH^0]98 $%~AKpx* !l1p@p`[omZ[v@?(# u mhhXe8 qPT @`0$sss^Qvc'wncHΕ)1 u_5Ls{ c*cB?`((rW P^U=r ? gj+@p=P.T06!j2p@ȵD%av\b(,x7&,  bB*j^H`Q fs$!6/,MEC0^?xq,bkRjz/%eB Wxꩧ>%tw{A.g|bR]6W0v 'EB?DN~@+ZF NvbBvP(2 l%yŖn>P?46 *]Q@Blff֐~lvn.XHB ༟aOeX6?!ޗ*9O@Ȁw3pFFB`4@m,a@L"Gj-$چ}05,U[ܾIFOQ^G7c|C&8;-zS4\%S05_' p*5]58 XqP l~~!66>3#'ԃ #M1x9@DW^yg8( UW^a%`iU}#{ʞwoGW'1 AbG)(x5L#ȣaC&0x+d!׮Qس_YNʽ/|WbIAa[P+ /"j9Xn]* /1Bׯ`HqX2Ls,<&.?T鿭tFwws`n@T7qPiO,,,0k@ 3bqjۚs˱7 iC5~ے)cc(7{PD8Zv,U>`fR3UI)<Щpߟ(%^Rc/Ǒ'(* /=^Z%  wE uv_a+b󧙳Mɰnq#N.v{vp 2ۅW.gbr*k8>19X^duim鎞!Oc -oM=B|EDEވ5+iPc{`݂@_߼R #{ OG3F@\PY: .z*ʹJ  ~ Er:>1_踀Jt@\hm}Oa wWcAT!0EKX`vnulغu%W (-W a KYI˴Ĵ: % p WS%Hu +\cu"W]܎ (-Bªaɩ阯)@C[SsP !#P,TAPPD#X } B '"W+! PZ@sA_gOtffAhV Pz[ %ezy:" JWTMx@u-v#!` +[-DG& ($* x΀ 1_ךF;@ K*o < (-? AZbم`BM]}!ū!j \ TAA_<b#;@:boCyd@ixEP+`,yI`;@ũk: A)m6:rIٖ!(UyXvezz&#!s8  o |"#DzQ26>ZM#ܣ(g J p@`,;95pH`ӇpZZ %"RpD- !pH k8BE\,(%.9sIغ=UP8A^܉,(% @O=j$x$,pV "REH8uvCetLI]©-u ad@)H&w!J"MB1d@)HN.@uff V R z YPJR @=RaGRNFx+ N9En f~ Dd@)Nn| @) d z|T6I"R`N @ai t @gw>  YPJV:!kQECI0Y`bbe9k  d@)p ck)8!!)"R+ J0GN> R2D FS Pp),d@)(@7@։ Y;@RFF$ ;A/O7@ >>C#``p(u!#.ںETH@PY]/*9>;@R*5SP_;+" J G0~FOw_W7~}BkT (riHL-,E#״YPJH`r;9cv,bS ^״Y#8B_²76#!P꩐d/z" :tQVY$g.Ca{IgB"VW"RRS01Iph_g$Ӈ`PDAAKT7::@VJ%ޓthN"Ÿo'yfu !5)I"QPAIz7# J &ݕ~_UC='?$@ă e\c0s\ÇEA宨6lLNMK\$l\: .$$I ᥫ-F{vw)Z]ua? a+^cDkw3$4j )ݽ~IvveSRJ@CS!#],(%W km7\7vH܊ (RRx* ' Q)! J %;ut*133ؼ-$~2 m(!E\El5L sWzkI 7Ej\ӑEE:Uhx %eUʇR?%o#&Z,(%' {e y+@CT#@c`s^}+YP;+l0>1J*Qhr'  ϯ%e ~/!#dh`x qA/4܆Fnق}j&q&!4->q''| P+4&5[ݏǓ;}i.]=808${A GEV𯪮g3?/bM1!u-C#,WcM }[A`0->Y7gY [>ô[e}A-l{ꃿ]e~/G8CtŶ 684$p::ٞzE[Ab׭Ŋ^x,*At])0Eu^dGP] }^ (O~E#QvFnO%3G־KI."_*XXX 9 `=}6Ya?p:`tt4A54L[v\`T4!{ j]q Ɩ],/v#x!`2,|%`W)a?_u5[=}bñ'k+> w߯ 90۲x$o : 44@`Le<- !9**' K=]!A b/ `~nj~He9s Tխ ULYJ 7K:~́ME/d nSKső ؔ:|Щ ompI?9L𬪮_n)hss ~Nwo3]W IYUc-mijE C@x]1fTҾ5 %@`4xz lEmhg=́m*IvOLyzTC?4+(A& RB򛂂2fgW-% s jp]ͅ~O&UK~c9UAr_ RHJcS BkKaϵ d🞙a%r(ņGF"YPchx @}]F~d @oiL *d[w@@Tz ~~‹ ֩\)0w)DC'?TGLħ55o??oiAU?f $VS׀ ǂ@}ZD$hB6A:1 @zͅAiQZ ѱqVXZPIyo_0>>~2ji0qbiprzɰ y-HX`@fVM?@ىt}_"rr˫AͼU-5u@ - )'UoM*aq53# #ɬUX($ZĹ% T KUC, `URLTef- I'UoSKJo-@@ ߬7.:NGA}4??bY\ٲ=Uw+)-mknHd M[kbS,;{+HW =54*&ѼοQpE;ڳX< MJHNu6! R4Y:<@[k| ]5M;LU[XvCPJjlP.vnbd@HMɕ洛TP,x z`PbB)fo 2 KI㟤`ya )`p+ @=ާ$굽LlDs~+!8gggGrQ@H5YG`5X~&B ؍%Xgw/sW2݅Y6&$1+54ј SYD̃/i:`=9YwOsWT!Vٹd r+b^pR0E| $٫)?"=h p͇lΜ2??근 Qm}ܯ%dArO<&VV.G˄bw}M\sM0}(.\>%FXBh?/`2~A|_ asW odL@X?儀ߠ8&`@@e<@wM\fFL@V?q_fL@ɇĽmz 1@">JޖI=L@k iB/&u K :^&v >Ά#Ϡ_'$N($wL@3߄(x^$oAo1@a^$'7YcJ!ܯ 7TF5@MRAY&1i@}HdoL@P9o@ ch&k h! L@?$߁I lq^urj LVO19@'sRSfoWAqy,˕f1lغmIJc2 3K2YXrfK1X {{%R _T@/"bdu,`B˱PPw35FOm#+)( \S@$x79YU=6 +( {ᚂ"|ߊ~n6ҪK5_!p~βr c hZTi"z1p"O䡿τTdKav&Z(7Y3xr˪0|y^  ω+Vh\莨ܫX/|M.oOe֜|ł9W^5! M! 0],ܤm?0 E YzmLHBc '&cahbv20oh_ o{Ua(Ǒ_wǿ:( /憽ϾqPSמ-psZ bC@3U/t!˃+ƻ_`씆?`goL{zCQqB /}M{n}m:v \ξjf @ ?y%eŸ_w]7N?!(?gGo鄈8jQs5{PPPd.\{e B$B@@oV hҖcߩ&.Z$w\$6a[=(Y}.d4 G] mWN,Þ-IuH bi咕tE uʗ"vD%'Fl*$_ėg-M|#Ϸ#߿h6jE݅4FLVG?R<5Ҷ兀={_;3XS1cvѺWG,2 >˗j!?V?O]pa) NUM{I}*(/g4_^8O{ƈ?7- t >w}|-{ o'X=(4o![sLA|FyH7!6o]ɟw<[龮OoD-+ ^^28o@W*`iWb koG@G@ E bߗ&W:?Ȗ-Cݰ%f f^+N{|h͟! W]Kݻȏzgűߣ> xhHͷK 0ľ @~#R8d`W=:+w/'d s:O0W ((($L(Clvp^}I@o~|%= 'K_v#9ß=Ì4(&&c *e<%[ "oX5FBpuDf?+~n)p%9Gc0q4|Sp$efKhkKQx9VE-|/@NI bu?ϦJRx9Z?|oOK uχı=9q Yk "I8}\s;Q}4C+W|{LK]ώ9kţ!R2 )`:~4@2E.gvw=G32cތW+RLY8c[wձn Ÿ7Dyg\tKO ҳ! <WȰ?CH*%Ro G#*$Ona NxC qkov٧<]X",h(+P QXd>I57E #?y?+o '}yAm;3=[#| 2+pQo U׷ϹmITUsR-61x80p =о^>xnJb԰[c?? wZ|n],d1DS\%*h![=K|m??Q5}_:ڳs4.Z2r3"GN>olfc҆~EVQh8hUf9^@>k_x-np",x w9KLI7z3W-ogq|=By"?EN2F bE h9?a9J9)5G7#dIW߽eT "!&H0Zڙ2F*K팥#r! 5A|ި XKlK|Q \s8K?ueV;r @8lsU#eӈ)_ß$ /4IwDH U[ kPTef{p٥׭\d%Hg^bxƹoL`|ttMBw&ϐ~LV߹LR-2(~Y>~3o`!Gے{t3K/&hCDDP3g\z5b cw᭩3b?;($sKRk ^ :}xx;)F)ŸGGKm#317%t ~NvC߷nc"{5*&Af5w<{Ibf% ;}N#Q?n{'{Z`myE(kPTW VXh'TiP;.^9( |DHw|,?F4((ٕpK inicL[z MP E]<}#6huHȉhnjEGI.o PT %;Z tR`˶IV飿wR1?KjSx۽~Q156ԝ#FGU#{N>@aR21Rpzz{٦DmqtQ*g,F&uEfx1§🛟GG!w,Y%ccޕЀ_^O+ -_Tw?wF ^C[m*b,a2@U'܂"#Jo/ _l <_@!SŰ6tF `䕔ATBՁp8V`aJcc/??-\2G(!{댮^U F=0LhvuR8|q\ p$ME=w!L_gv[zQb *acB=J;T+[ᶔ J/ mQ|E E1Qu1jGvՆ?׋*PZU1 4+M-Nyȝ_ Q"g1?#?oS `mK=Dkj Y8ϬIp h\{*DnA ?}͔ß7Qsv `T !*c[z&> ʪkU-+ Q]U¿9_:M3rs KT/ m(.$&1PWjtsK.I@*1!PRXßOßSVUԵ#תոӤiv/Znbb"Xto 8?/D? cO#ftl\NwHHIGk#`8}}~,r2`I-}?b},?pף{%~eZ} p{ku9&8YPTzkSkT,}A /ҿqs[ xaaA\8! bGG`||\S35F8%X\1.BtvR:s a}FGT)u 7D|b?5p`fvA ^ǣ^'JKv~FU @ɂgp NgwfQ]7"cP @x$*Wz,y4Pp%T@mq@ QQON!W ˕([+TY"GPNv\ģ>< @/@$WM7!QX.w5@:f Z0zT"##(̸ baLL5!UC+(@[:ZIkY#`(eniɏ'==|荭8Oa?Yx`:cqd@`TSJBll)mJLF`C@wФ-|6 7¦?# @hSW:8<2¶8C6'RXZlo2xjXvaaǛQ'L+sR/Q[E:>UxbNLM.f?3cȓ,2ƷvǗ+$G8ǏJo-J@Yu}/sq;2&[; JP OWk#H21YƟFDzkp mL2YXV^!@" K[p$a@˘,5.0C(g H[@VؖɊ˫X\/yQa_IR`{ܜBu,0>Dgw 54C㯾TVkJUkn ^'p`0Y$º 0nɰڙ@XXXTu׶1_57?uvc@dac]UTZ'd ֶvAٔݜG1 1>͜R31䐀z~uS+cC@pu"OO`lAf% ]U76Oj?L줠O`n6n% =j)C;zsta dT{[W YQ?1 R+%qepUU(&yrÿ7li(?Z1KLnGë {A \ʓR]wvv3p{:1!&!vUV_'krFH>~)1IpGR E-\arbc%˕æ ϺvY?8,;C#̌Mngήy[:|A!AqW:׼%)E_mB  EL pvvd-.?c1?×6c Z==}~*j%{lHYbJZB Bx]9l#<#pSR>MZclfV_ OΜ|5F5:AI7CwVǭ-V{:&FuGCv~!yg;MǺl&#_!2x?@lٞ*s9 ;{zYCKTײ\'T5[XrzQP-==6r ٜv~. MG}~6~zFaz}.> *j5.0_w/kneno-)H Dwtt,ՎBME=& }. !G( ?Hw|W@@4G+111l| {i\2.Hp)(qXq*q__w `2=^2`kW &#l~=hhjACVS_!v.=ի @O }]7K'Py"g^Ll`e9u+&ج[xyE;oc_+ ?9ߞk7#ݪ7,{IX,? W^#@^@4^ vчU.kqNnaB_{ 7| S]׀,,0G.=f7!wQP\75"p?+͙f`&N+Ai\P c翏nN4X) ~&+=q+ f/#ЅC7%wK+?pS; @')c{vU҅އ`A.H 0(J?QFggϨ%xFߺ[LV_osYOqЦmipi5LNNA ÿ -{/T:}KmŞ`Ⱦ41%L7P$ht (vC 2|w7!%?חI>oU;[DJ6?!]6oO7dy--0Mc-mywy~ј۟xJ~rO|EDՑs ?-MФ`,ˑ0[5ئmIkHZ-hH$wK\)84Yfۑ]0NvRيJ`xlKHICH(HFD-S߅+ 4_ .t8Y?BHONAa# H`: cc,dF@LJWzFlss ?P0]fA=۳ :'!9-ƈ}2ms|I& H 9_IjqS5*@Uu-@ߙ@VJ h8>qy?\y룽KM0 5^sZZ %nr GD`8> sq,gnX. b4蔍 I_QE+σ׃|'&<%Sjλ47IHI?,dBMS9/ġB+2NsxNI%bϰڏ9&s)`$յu"Oh=wi.?pBA,.:jJ0>>)~?xl1xW߉qxQ%eXgy45E@%*QTJ4O%~#/>FKe9s 2GآŹ<59Q %Cv ~/~e]qo o+`?K֯4uMU Y#}KM}^ _sod,^?@g46P K4OqJՏ-u5_+*TobӶR2qOKTzkt+S100[/Dj =Ksx-~O /)Bjn%ʅ脭I 6ɑ2Ciw1̰I>! ߘS)&[T 0X> U@Rȑϳ$oY&qp1h& s'͜ ј=.l\]lhjRO.mݞ=>9=)!(m܂R)h8ߍ.ukLefIB9' a9lnnNWg@Ff5e4oCR!P^V#˰ڳ"U_(o !WH0 ehmhԤxk!%DPTZb'm#-}~ @s[;@Wy5ͭm ?|gZÿAM ƄJo?FQ  QnU<蠎]` 4mbXT*1@[vj VFA\9H6[v7ܩM c||\u੨BP㱀TjNR3-]?L36 !`kR¿mܺ!!)H1YN&? "6??*-(hT$(iF(<3pPA%e`uT%%r2VIJU3f.}F*_ciBr N5[E~ T@[5!qE2ur;ā zU m>FvTQ뿮)B@Z0bۉU/Y\U C4Bcs?22`UPMGT\ZHJ48l2߸O06XI?KacλLf|+)BIJTZ ^1%bxa& Oa04iJuRoGǭ߯3m 􋶏񵰰p0+J @P9S!s AO#@!%*v MO_Cᯓ@_@8-=;RPRe7P߂b@NNAxA-'T(JHHN;4s]_'j);vZ_Ye+:{>!W0 PBxBq{* !=\gNSLKEq )#<UvJls!%xe:+ķ~[|SSנDM|)޿~?O$?ExKŠ3IY%Q%f+).(\fs\$UO5Y+S2n9O>dv`r}J@ҿ޿EC ]U$ ¯ؠ>ȴ* 8n2;,yw率mcJOt!{C)O?2ljA=z̤vmy3a7qQ6Y-!! WR۫5@-ְUl$ 6ڎm:vK;W !K{t{}:Rب8~a_#EU=`){t&f2*9@-JwEnN*A~g%o)J>—ūBo`v5:  Y12o֙?Cxr m;Pʪr @Is o^YV*F4|g-uO~>U ~~ζ^7L𷓀=(-۶}* MQ$邟rO W9u ?J- ( e/ș c|PFpӶ`6$)E ̎, ~u}p/V zVjhjn;@PŤ9`VX"$ exV" 3V_%l}?Џ*ǎ?8,NUTޔO St p+sPit% !slGW^s*v{VI-+kPlԟ=~&y H~Pe.:nӚb~6E"_K?Nmo>* ˥\"p/O$ VL[AH'Jxk ֛$+@؟ >-0"*ڽ@al"`ߺNN1  h UOp;M0}﫷;ǁÇnTpa ? #@fSn=GJ@$6.SU N aw? KUϜ$(J@0=g.ޟ>/r'șUM8^{ºE ذ@!,[R ܃}I#@b D "R c}+Fs?HDX'ڍ6nJj%Wzsih xHT  eRg~ds.N888/՝CQ$xoF"%` K-"JѠ+kz|36璛q P/*"HHDzRB('$琄,AId"0{:']@f'sBy cHhT!=,@_$D&ϚGf@ R` H¾#PHz f֛VzT@k0?w^/KSIX\ &̘C9a$&)UAz:)¿ @TAHb);&C%nzo9ާ5Zvjp<B[^|,R5ؔtgR @g熒LYvI!AgdE֋@B7= v`0y gF}R6 _{jiK{olIZn M ! 9*5ihh`_] ^ _I} -Ӥ.$ZagR`Uk$3ص/e$q0cRx&m%?    .u[@O`38;_a+G_exf^ڟ*B #x J qOGQdvH$ 3GɌp2g^ 䙻Lsx2g2gD(Ϙ1cc'ac'O㧒''N#&SfSg+f>MR2r U+V?03` O`tS,ۗ&6 f/)b f3 ;"Vh WϏ's#bHW\,e .Y>n 9)2 Kg5` "D 8P#W;@oE@Fބ+YlvloW^]# i$4gs#ba63y> {C)9 )1Ղ_O{Z`>9C x?U6[7-cY (?4;sɾҐewyF{@^-1<&ww v]Tlr^vvDx$(In&zu$W?.*`J?}Bd􆀁 ɼ 8"~ЧdЧ>Atmdzp HlR聗&y`{C1{T pZ7O댃܆A:k,#`ATAWYiY`#C" ju \zx{y 455*FJ-_0y<*nVO/cP8[ .1Z0CA.FlI= ; Z0窨 Ԉ πX(ZLR_??;GJނBNFJeRhs 5Cb2OrF|Z'Q?=gSbVjoV%Fɞ ߱I|j&vgn)XvL`c*G xv'z,H q$aQA6W&zgֲKi@H`l v ѣ@N mպ,.1 @37$fSdWU~X~/g$(OKLqqرA:D+J81a*IʠZ444 ` #-=(?4vɀa. zŗR92'5 ogi B}ZEz}6t.\mƎë/O%zbqKr\5@Mf_(<6f2MhU^޳WH_F7%`-,>0艝@/=^$T 7 5P~7]%Y'?^x"=c|6 ͩZ1!D瀇 ?Tv Aé t3TݬaSGJQ>utgLһ# "6hnnzmoW JL'`05($gHzsq? ܊DԮ e朄pO@簾$jtM|H"R @`%iSxdzoX  \X5(h20rv' P1;`HV^+TO$ʚK_*Ꭾg4D£c]f_ұ"-;  x;/';_Ezp@RI#Ȕɬ$8"ĥdQ'$"&<6lt' xk Hk(|}u͆XT8q|%CW`\U1WI&q?/A7;kcV`_p?K?/Xv:"'?u~/aWX|\(9Οu]D'{tLBdAg96'77]rWNGH@ 'I-jF>]\kaLbuߖgV,?b6ZBdΖ1{Qi}lX+y睾'N(G[9 Ut)mWTL @Zv~&KNp9r}~yP}Qdf7_(SN"`,\?NF/ָHw hq00m*bfq\y2Ϩ\)pOs}>aQ+cS\=w¾=%FS ZA"~(2S9QoƀǏW@_UT ohp56f Dapp @f*V N`4 Q kx%b2*` ` ܎ 3KlF~@Pu+v?Z]X܈{G ŕwG9*507~}g]]3HIQ> k &I 1Z [!,L)Ww 374H٬|U{f XlJ@~60@f {@9 9"Hlh4C`Wʄ7e/] K 2t.o[`< ͶcZ,HADf o(r gG AYz LC_LVG" HQ`b k AAAAPAQM6gIENDB`