Skip to content

Nullcaller/passutil

Repository files navigation

passutil (WIP)

passutil is a command-line utility that helps you store, memorize, generate and transfer passwords securely. It is built using libsodium and OpenSSL. It stores passwords in an encrypted form in a single blob, with metadata stored separately in plaintext. The two files, the password blob or 'masterfile' and the metadata file, are collectively referred to as a 'password store' or simply 'store'.


Security is not guaranteed.

Memory leaks are guaranteed and the code is spaghetti.

Data loss is not guranateed. However, data loss is highly likely. Even if data loss does not occur, password stores created at this point will be uncompatible with a future version of this utility at a later date.


If you still have doubts about whether or not secuirty is guaranteed, please take a read at section 4b of this project's license file:

4. Limitations and Disclaimers.

<...>

 b. Affirmer offers the Work as-is and makes no representations or
    warranties of any kind concerning the Work, express, implied,
    statutory or otherwise, including without limitation warranties of
    title, merchantability, fitness for a particular purpose, non
    infringement, or the absence of latent or other defects, accuracy, or
    the present or absence of errors, whether or not discoverable, all to
    the greatest extent permissible under applicable law.

Please, do believe every word it says and review the code yourself if you have any questions.

The code is written mostly with the goal of being humanly readable (though it was also mostly written in haste, so don't count on it being too good in that regard), to make checking it easier. Sometimes this results in variable names akin to first_argument_except_for_the_command_argument_except_that_weird_edgecase_where.... This is intentional.

Store Key Verification

Store key verification is not enabled by default (and is not implemented yet anyway). passutil instead relies on the user to keep track of whether or not they're entering the correct key. To aid the user in entering correct store keys, a preview with first five decrypted passwords is displayed upon entering the store key, and further confirmation of their correctness is required to proceed.

This way, if the passwords are generated with passutil, a potential attacker with access to the password store will not be able to gain access to the correct passwords using brute computational force: for a given hypothetical store key X, an attacker will have no way of knowing whether this specific key is correct.

If password data was encrypted carelessly (for example, containing delimiter characters) or contained orderly data (such as English words), an attacker could theoretically find an entropy minimum and be reasonably sure that they have correctly decrypted the store.

The password data, however, is not encrypted carelessly (nor is it ordered, if the passwords are generated with passutil, which is currently the only way to add passwords to a store). AES-256 in electronic code book mode is used, with no padding (or, rather, random padding). Initialization vectors are properly randomized. All plaintext blocks are filled fully before encryption, and placed back-to-back into the masterfile.

For all intents and purposes, without the proper store key and the metadata file, the encrypted password blob is thus as meaningful as output of a random number generator. Or, at least, that's the hope.

Password Character Encoding

Special care is also taken with regards to encoding the password characters (or, rather, decoding them).

It is more or less obvious that simply encrypting password characters as ASCII bytes would negate the benefits of using no encryption key verification, since a simple check would reveal whether or not a given encryption key is correct: decrypt password bytes, check password characters, and if any of them are weird (non-printable or unlikely to be used in a password), then the chosen encryption key is most definitely incorrect.

The chances of all decrypted password characters in all passwords being alphanumeric or special characters decrease exponentially with the total number of stored password characters. There are approximately 100 'non-weird' characters, and there are 256 possible values for a byte. The number of possible encryption keys is 2^256. Thus, we can approximate the total number of encryption keys that would fit as per our simple check as (2^256)*(100/256)^N, where N is the total number of stored password characters.

This expression is equal to 1 at N≈188.77, which means that, with ASCII encoding of password characters, there would be no benefit to absence of store key verification for stores containing more than 189 password characters in total. For stores that contain only alphanumeric password characters, there would be no benefit to absence of store key verification after 131 password characters.

passutil thus does not use any traditional character encoding to encode password characters, including ASCII.

Instead, each password character is represented with modulo N of a uint64_t, where N is the length of the password format string, which is part of password metadata. For example, N=66 for a...zA...Z0...9.

The type length (sizeof(uint64_t) is 8) is necessary to guarantee that there is no set of characters that is significantly more frequent than the set of remaining characters. Consider, for example, a byte of data. 256 div 66 = 3, 256 mod 66 = 58. Thus, if an unsigned char was used instead of a uint64_t, The first 58 characters of the format string would occur with 4/3 the frequency of the remaining 8 characters when decoding random data. Since data must be random to provide no entropy minimum for a potential attacker, as discussed, there is no other option but to encode password characters with a larger type.

Because sizeof(uint64_t) == 8, the total amount of possible values for a uint64_t is 2^64. Divided by 66, it is approximately 3*10^17, which means that the more frequent set of characters generates with (3*10^17+1)/(3*10^17)≈1 the frequency of the less frequent set, or, in other words, with approximately the same frequency.

I use the word 'approximately' here only because mathematicians would murder me if I didn't. The difference is on the order of magnitude of 10^(-15) percent (that's 14 zeroes after the decimal point). The amount of passwords you can feasibly create will never reach an amount where a potential attacker would be able to take advantage of this in any way, shape or form.

You may also want to know that, when generating passwords, passutil does not randomly generate the password characters themselves, but rather generates random bytes that will be then joined into uint64_ts to be interpreted as password characters. This choice has been made to make password generation easier and to prioritize the impossibility of store decryption instead of prioritizing true randomness of password character distribution. But, again, due to the length of uint64_t, the randomness of password character distribution suffers only a practically infinitesimal adjustment compared to a truly random distribution.

A Word on Special Characters

The general wisdom has always been and appears to remain, that using special characters in passwords is very very good. The more, the merrier!

Respectfully, I disagree.

The strength of a password is generally agreed to be characterized by the average number of passwords that a potential attacker has to guess before identifiying the correct password.

The worlds luckiest attacker could guess your 50-character password first try and gain access to everything you hold dear.

This event, however, is very unlikely. In fact, there are approximately 10^100 possible 50-character passwords, which means that the chance of accidentally guessing the correct one is beyond astronomically small.

This example brings us to an important conclusion: we want to minimize the probability for a potential attacker to guess our password correctly. To do that, we must maximize the number of passwords our password could possibly be one of instead, and we do that by either increasing password length, or by increasing the amount of symbols which we can possibly use in the password.

But once we've made up our mind about the password that we want to use, the only number that matters is the total number of possible passwords we could have chosen instead. And at that point, it does not matter whether or not we used special characters. A 5-character password with special characters is bad, a 50-character password without them is good.

But where exactly is the line between good and bad? Luckily, in this particular case, the answer can be derived mathematically.

The total amount of password our password the world's unluckiest attacker would need to go through to guess our password is approximately

N≈C^k

where C is the number of symbols each individual character could be, and k is the length of the password. By going through this many passwords, if they're all k characters in length, you will have exhausted all possible k-character combinations of the symbols that we're working with.

For alphanumeric characters, C=C_0=66. For alphanumeirc and special characters, C=C_1=99. It thus transpires that for a given password length k,

N_0≈66^k
N_1≈99^k

It is true that for a fixed character length, using special characters will always be better. But it is also true that the length of the password without special characters can always be increased, so that it matches the strength of the password with special characters:

N_0=N_1
66^k_0=99^k_1
k_0=log_66(99^k_1)
k_0=k_1*log_66(99)
k_0=k_1*[log(99)/log(66)]

But surely, surely, you will need to increase the length by a lot? You would think that, since 99 is so much bigger than 66, using special characters would be so much better, right? Right? Right? Well, we can in fact compute the exact factor of length increase:

log(99)/log(66)≈1.09677

Soo, to match the stength of a password with special characters, you need to add... 9,7% more characters? That's... 2 characters for a 20-character password? And if you just use a 20-character password without special characters, it will be the same as using... 18-character password with special characters?

I don't know about you, but I don't think this 10% extra protection is worth having your password be a bunch of mess that's even more unintelligible than random letters and numbers. It's harder to learn, harder to remember, and downright pointless.

At least, that's the case for me. As always, I encourage you to make your own judgement call. And as for passutil, it pretty much always allows you to pick your own poison.

Usage Examples

There are four 'modes of operation' the utility can be in:

  • store manipulation mode
  • password manipulation mode
  • memorization mode
  • transfer mode

The utility defaults to store manipulation mode, as you need to create a new store or load an existing one to do anything anyway.

By issuing store, password, memorize or transfer commands, respectively, in any of the four modes, the current mode can be switched. The current mode is displayed in the pseudoshell prompt:

passutil(store)>
passutil(password)>
passutil(memorize)>
passutil(transfer)>

To return to store manipulation mode in any other mode, you can also use the exit command. Using the exit command (or its alias, quit) in store manipulation mode will result in the utility attempting to close itself. It will not, however, generally do that when the currently loaded store is marked as dirty. That is, when there are any unsaved changes to the loaded store. As the utility is currently in alpha, however, this is not guaranteed to always be the case.

Commands can only be used when the utility is in the appropriate mode. For example, the select command will work in memorization mode, but in password manipulation mode it will result in nothing but an error message.

Each mode has mode configuration options or 'parameters' that can be changed via the set command or viewed via the get command. The full list of parameters that the utility recognizes at the moment, for each mode and with allowed values, is as follows:

store:
    cipher: AES-128, AES-192, AES-256
    key_verification_algorithm: (unimplemented)
    key_verification_algorithm_rounds: (unimplemented)
password:
    format: FORMAT_AZaz09, FORMAT_AZaz09_64, FORMAT_AZaz09_symb, FORMAT_AZaz09_sp, FORMAT_AZaz09_symb_sp
    length: (any unsigned integer)
    salt_length: (any unsigned integer)
memorize:
    mode: whole, by_symbol, by_nth_symbol
    symbol_number: (any unsigned integer)
    choice_count: (any unsigned integer)
    symbol_failure_mode: ignore, repeat, restart
    repeat_mode: singular, repeat
transfer:
    (none yet)

The set command works a bit differently in store manipulation mode than in any other mode. Instead of changing mode configuration options, it sets store parameters, such as the algorithm used to encrypt passwords, the algorithm used to verify store key against a hash, etc. Used in store manipulation mode, the set command will thus generally fail without a loaded store. The same is true of the get command.

Store Manipulation

Create a new store and save it:

store                    // This command switches the utility into store manipulation mode (the default mode)
init                     // This command creates a blank new store
set cipher AES-256       // This command sets the encryption algorithm used to store your passwords
get cipher               // This command will allow you to verify that the encryption algorithm parameter was changed 
unlock                   // Use the key you wish to set for the store
save-as <name>           // Replace <name> with the name of the store (unlike most parameters, <name> allows spaces)

Change key on an existing store:

store                    // This command switches the utility into store manipulation mode (the default mode)
rekey                    // Use the key you wish to set for the store

Change the encryption algorithm on an existing store:

store                    // This command switches the utility into store manipulation mode (the default mode)
set cipher AES-128       // This command sets the encryption algorithm used to store your passwords

Load an existing store:

store                    // This command switches the utility into store manipulation mode (the default mode)
load <name>              // Replace <name> with the name of the store (unlike most parameters, <name> allows spaces)

Save a store that you made changes to:

store                    // This command switches the utility into store manipulation mode (the default mode)
save                     // This command saves the store, using the path it's been loaded from or saved to previously

Password Manipulation

Generate a password of set length and format:

password                 // This command switches the utility into password manipulation mode
set length <length>      // Replace <length> with the length of the password you wish to generate
get length               // This command will allow you to verify that password length parameter was changed
set format <format>      // Replace <format> with the format of the password you wish to generate (look above for available formats)
get format               // This command will allow you to verify that password format parameter was changed
generate <description>   // Replace <description> with human-readable description of what the password is for (unlike most parameters, <description> allows spaces)

Change the length of the salt added to store key for password encryption/decryption for password generated during this session:

password                 // This command switches the utility into password manipulation mode
set salt_length <length> // Replace <length> with the salt length you wish to use
get salt_length          // This command will allow you to verify that salt length parameter was changed

Take a peek at passwords and their descriptions:

password                 // This command switches the utility into password manipulation mode
peek <start> <count>     // Replace <start> with the index from which you'd like the command to start displaying passwords and <count> with the amount of passwords you'd like displayed

(Tip: <start> and <count> are both optional positional arguments, using the peek command without any arguments will work as if you used peek 0 N, where N is a value pre-defined at compile time, see facility_peek and pseudoshell_peek_command_executer)

Print out only password descriptions:

password                 // This command switches the utility into password manipulation mode
display <start> <count>  // Replace <start> with the index from which you'd like the command to start displaying passwords and <count> with the amount of passwords you'd like displayed

(Tip: <start> and <count> are both optional positional arguments, using the display command without any arguments will work as if you used display 0 N, where N is a value pre-defined at compile time, see facility_display and pseudoshell_display_command_executer)

Print out the password with a specific identifier:

password                 // This command switches the utility into password manipulation mode
fecth <id>               // Replace <id> with the identifier of the password you'd like printed out

Remove an existing password:

password                 // This command switches the utility into password manipulation mode
remove <id>              // Replace <id> with the identifier of the password you'd like removed

Memorization

Attempt to memorize a password:

memorize                 // This command switches the utility into memorization mode
select <id>              // Replace <id> with the identifier of the password you'd like to memorize

Use Ctrl+D to get back to the utility's pseudoshell.

See mode parameters for memorization customization.

Transfer

Initiate store transfer on the send side:

transfer                 // This command switches the utility into transfer mode
send

Initiate store transfer on the receive side:

transfer                 // This command switches the utility into transfer mode
receive

The store will be locked and marked as dirty after receive is completed. You will need to save it manually and to unlock it before making any changes.

During the transfer process, you will be asked to enter the metadata file and masterfile encryption keys, then metadata file and masterfile lengths, then the AES128-encrypted file contents themselves.

Keys, as well as files, will be presented in form of data blocks of some N<≈8 symbols and with M<≈1 checksum symbols, which will aid you in entering the data correctly. When enetering file data, it is not required to press shift to print capital letter characters. You can go back a character by pressing Backspace and forward a character by pressing Tab. Pressing n will move the cursor straight to the next block, and p will move it one block back. Note that there are empty lines printed in strategic positions. This is to make it easier for your eyes to find the block you should currently be entering. These creature comforts are not currently implemented for encryption key entering process. This will change in the future.

Expect this manual file transfer to be a lengthy process. Prepare tea/coffee and an audiobook of your choosing beforehand, as well as yourself for this tedious task. Entering the first (few) page(s) will be unpleasant, but I daresay the monotony grows on you after awhile. Good luck.

Project Structure (may be slightly out of date)

File Purpose
constants/*.h Constants used throughout the code
passutil.c, passutil.h Entry point, argument parsing
util/*.c, util/*.h Various utility functions
util/crypt.c, util/crypt.h AES encryption/decryption utilizing OpenSSL
util/shuffle.c, util/shuffle.h Password "shuffling" utilities: passwords that are in RAM are stored "shuffled" when possible so as not to expose plain passwords
util/generation.c, util/generation.h Generator functions utilizing libsodium
storage.c, storage.h Store and Password structures, serializers and deserializers, parsers, generators, functions for working with storage and getting passwords in plain text
memorizer.c, memorizer.h Functions to help memorize the passwords
transfer.c, transfer.h Transfer functions
facilities.c, facilities.h Generalizations for functions available both as command-line arguments and in interactive mode
pseudoshell.c, pseudoshell.h Interactive mode pseudoshell implementation, command related abstractions, command executer implementations
program_options.c, program_options.h Program option related abstractions, program option executer implementations

Password Shuffling

Shuffling is the following procedure:

Each symbol of the password is replaced with a different symbol from a symbol pool string. Same symbols are replaced with same symbols. Two different symbols are replaced with two different symbols.

Unshuffling is the reverse of the described procedure.

Consider this example: password = ABE, shuffle key = DCFABE, shuffle key format = ABCDEF. This means that the symbols are replaced as following: A -> D, B -> C, C -> F, D -> A, E -> B and F -> E. Shuffled, the password is DCB. Unshuffled, it is ABE again.

This is a bit silly as a security measure, but I'm too lazy to rip it out now ¯_(ツ)_/¯

TODO (largely moved to issues section of this repository)

Feature-wise:

Security-wise:

  • Use OpenSSL's EVP instead of direct AES functions (https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption)
  • Use random IVs instead of all zeroes
  • Make key hashing function selection an option
  • Use some of the store key to encrypt password metadata?
  • Add salt to key before hashing and store it with store metadata
  • Make use of libsodium's security features (secure mallocs, RAM management and stuff)
  • More cipher types
  • More key verification algorithm types
  • Deny any more arguments after --interactive so that user can't be tricked into opening stores, add compile-time option to override this behavior

QoL-wise:

  • Command history so that up/down arrows actually work
  • Command short codes (e.g. so that you can use q instead of quit)
  • Capture all VT100 escape sequences (https://web.archive.org/web/20121225024852/http://www.climagic.org/mirrors/VT100_Escape_Codes.html)
  • Capture all VT100 escape sequences in pseudoshell_get_specific_hidden_character
  • Write some actual error messages for facilities
  • Capture ^C, kill/term/etc signals, offer to save on any unsaved changes
  • exit in any mode but store manipulation should probably result in going back to store manipulation mode
  • In memorization mode, incorrect key presses should result in a "Terminate memorization? (Y/n)" prompt instead of terminating straightaway
  • Allow loading stores with explicit separate metadata and master file paths (so that, for example, metadata.txt and masterf are valid metadata and master file names respectively)
  • Inject debug-related code so that tracking down bugs is less of a nightmare (use preprocessor, only compile debug functions if required)
  • Don't forget to add an actual help message

QoC-wise:

  • Reconsider the usage of unsigned char, separate bytes from chars more clearly
  • Pseudoshell command abstraction
  • Put files serving separate purposes in separate folders (how name?)
  • Better number parsing in pseudoshell (implement in util, use in pseudoshell)
  • Some table-printing functions to reduce clutter in display, peek facilities? (something along the lines of display.c, display.h probably required)
  • Separate storage constructs better so facilities don't use store->..., password->... directly
  • Find short variable and function names (e.g. cnt which should be count) and make them more human-readable
  • Prefix all functions clearly
  • Separate store-related stuff and password-related stuff into storage/store.c and storage/password.c?
  • Switch to using Cipher abstrcation instead of strings where possible

Bug-wise:

  • Track down and fix memory leaks before 1.0.0

About

A CLI-based password management utility writren in C

Resources

License

Stars

Watchers

Forks