Initial commit: Outlined most project goals in `README.md`

Fortune for readpass's current commit: Small blessing − 小吉
master
Avril 12 months ago
commit 00e716694e
Signed by: flanchan
GPG Key ID: 284488987C31F630

3
.gitignore vendored

@ -0,0 +1,3 @@
obj/
*-release
*-debug

@ -0,0 +1,121 @@
# Generic C and C++ Makefile project template
# Contains targets for `release', `debug', and `clean'.
PROJECT=readpass
AUTHOR=Avril (Flanchan) <flanchan@cumallover.me>
DESCRIPTION=Securely reads a password, asymetrically encrypts it, and then outputs it for another process that posesses the private key to decrypt and use.
VERSION=0.0.0
SRC = src
SRC_C = $(shell find $(SRC)/ -type f -name \*.c)
SRC_CXX = $(shell find -O2 $(SRC)/ -type f -name \*.cpp -or -name \*.cxx)
INCLUDE=include
# Link to these libraries dynamicalls
SHARED_LIBS=
# Link to these libraries statically
STATIC_LIBS=
override __COMMA=,
override __VERSION_SPLIT:= $(subst ., ,$(VERSION))
override __VERSION_REVISION:=$(word 3,$(__VERSION_SPLIT)) 0
override __VERSION_SPLIT:= MAJOR:$(word 1,$(__VERSION_SPLIT)) MINOR:$(word 2,$(__VERSION_SPLIT)) BUGFIX:$(word 1,$(subst r, ,$(__VERSION_REVISION))) REVISION:$(word 2,$(subst r, ,$(__VERSION_REVISION))) REVISION_STRING:$(word 3,$(__VERSION_SPLIT))
COMMON_FLAGS?= -W -Wall
COMMON_FLAGS+= -pipe -Wstrict-aliasing -fno-strict-aliasing $(addprefix -I,$(INCLUDE))
COMMON_FLAGS+= $(addprefix -D_VERSION_,$(subst :,=,$(__VERSION_SPLIT))) '-D_VERSION="$(VERSION)"'
# Target arch. Set to blank for generic
ARCH?=native
# Enable OpenMP and loop parallelisation? (dyn-links to openmp)
PARALLEL?=yes
OPT_FLAGS?= -fgraphite \
-floop-interchange -ftree-loop-distribution -floop-strip-mine -floop-block \
-fno-stack-check
ifneq ($(ARCH),)
OPT_FLAGS+= $(addprefix -march=,$(ARCH))
endif
ifeq ($(PARALLEL),yes)
OPT_FLAGS+= -fopenmp -floop-parallelize-all -ftree-parallelize-loops=4
endif
CXX_OPT_FLAGS?= $(OPT_FLAGS) -felide-constructors
CSTD?=gnu2x
CXXSTD?=gnu++23
CFLAGS += $(COMMON_FLAGS) --std=$(CSTD)
CXXFLAGS += $(COMMON_FLAGS) --std=$(CXXSTD)
LDFLAGS += $(addsuffix .a,$(addprefix -l:lib,$(STATIC_LIBS))) $(addprefix -l,$(SHARED_LIBS))
STRIP=strip
RELEASE_COMMON_FLAGS+= -fno-bounds-check
DEBUG_COMMON_FLAGS+= -ggdb -gz -fanalyzer -ftrapv -fbounds-check
ifneq ($(TARGET_SPEC_FLAGS),no)
RELEASE_CFLAGS?= -O3 -flto $(OPT_FLAGS)
RELEASE_CXXFLAGS?= -O3 -flto $(CXX_OPT_FLAGS)
RELEASE_LDFLAGS?= -Wl,-O3 -Wl,-flto -fuse-linker-plugin
DEBUG_CFLAGS?= -Og
DEBUG_CXXFLAGS?= -Og
DEBUG_LDFLAGS?=
endif
DEBUG_CFLAGS+=-DDEBUG $(DEBUG_COMMON_FLAGS)
DEBUG_CXXFLAGS+=-DDEBUG $(DEBUG_COMMON_FLAGS) -fasynchronous-unwind-tables
RELEASE_CFLAGS+=-DRELEASE $(RELEASE_COMMON_FLAGS)
RELEASE_CXXFLAGS+=-DRELEASE $(RELEASE_COMMON_FLAGS)
# Objects
OBJ_C = $(addprefix obj/c/,$(SRC_C:.c=.o))
OBJ_CXX = $(addprefix obj/cxx/,$(SRC_CXX:.cpp=.o))
OBJ = $(OBJ_C) $(OBJ_CXX)
# Phonies
.PHONY: release
release: | dirs $(PROJECT)-release
.PHONY: debug
debug: | dirs $(PROJECT)-debug
# Targets
dirs:
@mkdir -p $(addprefix obj/c{$(__COMMA)xx}/,$(shell find $(SRC)/ -type d))
obj/c/%.o: %.c
$(CC) -c $< $(CFLAGS) -o $@
obj/cxx/%.o: %.cpp
$(CXX) -c $< $(CXXFLAGS) -o $@
$(PROJECT)-release: CFLAGS+= $(RELEASE_CFLAGS)
$(PROJECT)-release: CXXFLAGS += $(RELEASE_CXXFLAGS)
$(PROJECT)-release: LDFLAGS += $(RELEASE_LDFLAGS)
$(PROJECT)-release: $(OBJ)
$(CXX) $^ $(CXXFLAGS) -o $@ $(LDFLAGS)
$(STRIP) $@
$(PROJECT)-debug: CFLAGS+= $(DEBUG_CFLAGS)
$(PROJECT)-debug: CXXFLAGS += $(DEBUG_CXXFLAGS)
$(PROJECT)-debug: LDFLAGS += $(DEBUG_LDFLAGS)
$(PROJECT)-debug: $(OBJ)
$(CXX) $^ $(CXXFLAGS) -o $@ $(LDFLAGS)
clean-rebuild:
rm -rf obj
clean: clean-rebuild
rm -f $(PROJECT)-{release,debug,pgo}

@ -0,0 +1,85 @@
# `readpass` - Securely read a user password than write it asymmetrically encrypted to an output stream
# Usage
* `readpass [OPTIONS] [<format-spec>:]<output-public-key>/-`: Read user input with default prompt and hash settings, encrypt the output with `<output-public-key>`, and the write that to `stdout` (unless overridden by an `OPTION`, TODO: See section *Options*.)
* `readpass [OPTIONS] [-c] [--format[-{public,private}] <format-spec>]... keypair [<private-keyfile>]`: Generate a public-private keypair and write the public key as suitable for use as `<output-public-key>` to `stdout`. (XXX: Should `keypair` instead be `--keypair`? It is a mode change, but the version, plugin-ingo, and help mode changes are all still `--...`)
Write the private key to either `<private-keyfile>` if supplied, or if not, to either: if `-c` is not supplied and the output was not specified to be a binary format, `stderr` (in this case, warnings will not be omitted to `stderr` at all unless the program exits with non-0 code.) If `-c` is provided, the private key will be encoded to a text format the same as the public one and outputted as the 2nd line of `stdout`, warning and error reporting to `stderr` are not suppressed for this case.
If `--format` is provided, the output will be in that specific format specifier (see *Key formatting* below.) instead of the default hex-string public key "hex:", (unless outputting to file via *OPTIONS*, in which case "raw:" binary), binary private key "raw:" (unless printing to `stdout`, in which case hex encoded private key "hex:" as well.)
**NOTE**: The `:` parts of the format specifier for `--format[-...]` are not required and are ignored if provided in this context.
* `readpass --help`: Show all modes, formatting specifiers, restriction rules, and global as well as mode-specific `[OPTIONS]`, then exit with status 0.
* `readpass [-v] --version`: Print version, license, comptime feature flags, author, build-date, build type, auto-loaded or available plugins (*TODO*), etc then exit with code 0.
If `-v` is supplied, then all extra information about supported formats, supported hash and derivation functions, and information about auto-loaded or available plugins (which may have to be loaded to extract the information from if they are not converted to `.rpp` files.)
* `readpass [-v1n] [--oneshot] [--noload] --plugin-info [--[force-]load] [--auto] <plugin>[/]...`
Reads and displays (optionally verbose) plugin information from specified `<plugin>`s, in specified order (if a file, read the file, if a directory, use the `--plugin` plugin lookup rules specified in the *Plugins* section. The plugins may have to be loaded if they are not converted to `.rpp` format, to skip non-rpp format plugins that do have to be loaded, specify `--noload`/`-n`. (TODO: encrypted plugins will warrant extra options to specify which key/key-tree/keystore to use to decrypt them.)
To force all to be loaded regardless, use `--load`.
`--load` will fail and error-out with non-0 exit code if an encrypted plugin is specified that could not be decrypted, or if a corrupted `.rpp` plugin with a valid header fails to be loaded, despite the header information being already displayed before the exit. To skip past un-decryptable or corrupted plugins that fail to load, use `--force-load` instead. An error message will be printed for these cases and the plugin info-dump will be skipped (unless `-v` is provided, and it is the valid-header situation, in which case that header information is still printed.)
If `--auto` is applied, all plugins detected for auto-load are added to the start of the `<plugin>...` list in the order they appear in normal usage.
The plugins will be unloaded in reverse order before the program exists, after the info-dump, regardless of if the program is exiting in error.
To unload the plugins after their invididual info-dump instead, specify `--oneshot`/`-1`.
## Key formatting
See the *Example* section for prefix-encoding of hashes. Without and explicit `<format-spec>` prefix, the `<output-public-key>` will attempt to be decoded as such:
* as a hex-string encoded key
* as a base64-string encoded key
* as raw binary data (if format auto-detection is enabled and falls back to this, a warning will be omitted.)
Or, if `[<format-spec>:]-` is provided (and does not conflict with any `[OPTIONS]` flag specifying to read user password from `stdin` instead of prompting, for example,) the `stdin` buffer is read and interpreted as raw binary data unless explicit `<format-spec>` is prefixed, in which case that is used.
If *any* explicit `<format-spec>` fails to parse the input, the program will fail with a non-0 exit code and error message.
Valid format specifiers for `<format-spec>` are:
* `hex:` Interpret the string as encoded as a hex-string
* `base64:` Interpret the string as encoded as a base64-string
* `raw:` Interpret the string as the raw key bytes
* `file[/<file-format-spec>]:` Read the key from a file, with an optional format specifier for the data read from the file after a `/` but before the `:`, otherwise, it is defaulted to assume raw binary key data.
### Implicit vs. explicit (`<format-spec>`) format reduction rules.
*All* implicit decodes *may* issue warnings (if `stderr` is not used as an output stream) if data appears to be assumed incorrectly encoded, or if the distance from the string and a valid representation of it for the current format being tested is low enough. (i.e. the *amount* of error present in an encoding. `01bg` has an error distance of just 1, and will be warned as "Did you mean hex:01bf?", whereas `cafeworld` will not be warned on for the hex stage, despite `cafe` being valid, as its error disance is so large it is impossible to be just a typo.)
Base64 strings *may* ommit the ending `==` **iff** they are both: explicity specified (base64 `<format-spec>`) and the expected input size is fixed to always make them redundant. Otherwise if the latter holds but the former does not, a warning of "This looks like a trimmed base64 string, did you mean base64:...? (Note that implicit encoding deductions are not allowed to trim redundancy when the sized is fixed to cause it, you must still explicitly specify the format for this case if that was your intent.)"
It is advised to *always* use explicit format specifiers wherever possible if your input is *in any* way non-trivial.
See section *Format speficication* (TODO) for more details.
## Plugins
TODO: Plugins are dynamic objects can be loaded at runtime using a provided plugin API, they can provide `--restrict` rules, `--{post,pre}process` directives, additional available hashing and derivation functions, and additional `format-spec` explicit specifiers, implicit ordering steps, and decoding algorithms. There is probably more that can be done with them.
TODO: Enable plugin autoload? Maybe make this a compilation feature and if enabled give it a disabling flag in `[OPTIONS]`.
TODO: Explicit plugin load: `--plugin {<plugin-file>,<plugin-directory>/}`: Plugin loads happen before any other directives, the also happen *in the order* specified on the command-line.
If the argument to `--plugin` is a directory, the directory is enumerated over (default Unix file ordering) and each valid plugin (or file with any extension of: `.so.rpp`, `.rpp`) is decoded (if needed) and then loaded.
TODO: Building a plugin example. compiling one, and encoding one.
It is optional for the `.so` plugin to be converted to a valid `.rpp` plugin file just by renaming it, but it can also be encoded as such:
* `zstd` (XXX: or maybe other supported algos) compressed `.so` file.
* Optionally encrypted and/or signed. (XXX: with our own key formats? Or with `gpg` ones or something? I think with our own would be better, we don't want *required* runtime dependencies, especially not `gpg` itself...)
* prepended with an `rpp` header showing information about the plugin that can be read without loading the library itself. (such as compression, cryptographic signing of the plugin binary itself, information about the decryption needed of encrypted plugins, etc.)
## Example
```sh
$ readpass --prompt "Please enter password" \ # Initial user prompt
--restrict size 10..50 \ # Restrict on a Rust-format size range:`10..50` >=10 && <50, `10..` >=10, `..` unrestricted, `..=50` <=50 (no lower bound, can be zero chars here) Or a specific exact size (e.g: `10` == 10)
--restrict regex '(?!.*\2)(?:(?1){4})+^ \ # Restrict based on regex match (this one requires at least 4 unique characters in a row.
--restrict deny-list sha256 wordlist.txt salt \
# Deny all passwords who's sha256 hash (with user-provided `salt`) \
The salt can be one of the following: `raw:salt_string` (use the literal "salt_string". "hex:<value hex-string-encoded data>" Where the post-colon text is decoded into bytes as a hexadecimal string, e.g: "hex:cafebeef1004" is decoded to the hex-dump literal `0xcafebeef1004`. "base64:<base64 string>" Where the post-colon text is a valid base64 string, the salt used is the basae64 decoded data. "file:<filename>[/{[{=,-}],..}<bytes>]" use the data of a file as the salt, optionally only up to "<bytes>" number read, omit the `..` to read only exactly `<bytes>` number from the file, or if the range has a lower bound, the most possible number of bytes within that range; the `=` is entirely optional, but explicitly says to read the *most* bytes, if the character `-` is used instead of the `=` before the range specifier, then it will read the *least* bytes instead. "fd:<fd>[{{=,-},..}<bytes>]" specify a currently open file-descriptor to read the salt from (will be read to the end if not lstat()/mmap()able). The optional sizing restriction rules are the same as with `file:`, but the `=` is not optional and must be provided if a `-` is not (see "file:" sizing behaviour.). Essentially the same as`file:/dev/<fd>[/sizing-rules...]. ) \
Or a bare string literal, which defaults to assuming the prefix is "raw:". \
(note: a "did you mean" warning _may_ be omitted if the data string starts with a prefix "<kw>:", and the levelshtein distance between <kw> and a valid keyword is very low, so explicit "raw:" is advised over letting the implicit default behaviour potentially emit these kinds of warnings.) \
Valid options for `sha256` are: ( Salt required: `sha256`, `sha512`, `sha1`, `blake2`. \
The hashes will be attempted to be read as such from the wordlist: optionally (since hashes are of fixed size) whitespace-delimited hex strings decoded, optionally whitespace-delimited base64 strings, raw binary datawith no delimiter. A warning *may* be omitted if it is thought the file was *intended* to be read as a line/whitespace-based format when it is instead being read as a binary one. To avoid any potential program confusion, the format may be specified explicitly by prefixing `wordlist` with any of: ("hex[=]:[:]", "base64[=]:[:]", "raw[=]:[:]", specifying the format. The `=` character additionally specifies that the file is *not delimited*, i.e. the fixed-sized hash list is to be read one fixed-size encoding at a time; for "raw:", this prevents auto-decompression of detected-compressed files, and the postfix extra `:` redundantly has the same behaviour.) \
The file may be compressed with: `zstd`, `brotli`, or `gz` algorithms; if the header of the file indicates that is compressed with that format, it will first be decompressed. If the file seems compressed, but still can be validly interpreted in the specified (or inferred) format without decompression, a warning *will* be outputted. To prevent this, post-fix the ":" with another `:` on the explicit format specifier to disable automatic attempted decompression (for "raw=:", the second ":" is redundant, but either/or can still be specified.) \
Some hashes may require size constraints on the salt, if the ":" of a specifier is prefixed with the character `~` (e.g. `raw~:`, `file~:`), then if the size of the read salt is lower than what is required, a correctly-sized nonce will *instead* be generated from the read salt using as many rounds of the `blake2` hash (/*XXX: is this still secure? should we choose an embedded salt for this at compile-time, or blake2 the `salt` specifier list and use that result instead? It cannot be random, since the hashes much match*/ unsalted) on the input needed to generate a nonce of the required size (usually, only one round will be applied to generate enough data to use, when reading a specific size, if the input is too large, the rest will just be discarded, the extra hashing round is only for inputs that are too small.) \
Salt not required: `raw` (i.e, deny raw password strings delimited by newlines inside the wordlist.txt), `null`, same as raw, but delimited by the NUL character instead of newlines.
--restrict deny sha256 word salt \ # The same as above, but a single value specified on the command-line instead of a file containing a list of them, the same formatting rules for `deny-list` can be applied to "word" as can be "wordlist", and are interpreted the same, without the compression support or the `=` and `::` modofiers. The default for hashes (like `sha256`) is "hex:", and auto-detection is applied to validly encoded inputs. The default for `raw` is "raw:", since it is a raw string match and a password is likely not to contain encoded binary data, the explicit modifier can still be used for `raw` though, but by default it is interperted verbatim.
--restrict allow-list ... \ # See `deny-list`. This has the same arguments, and whitelists overrides restrictions on passwords denied by *previous* `deny[-...]` `--restrict` directives, but not subsequent ones.
--restrict allow ... \ # See `deny`. This has the same arguments. Behaves the same as `allow-list` but for a single value.
--restrict deny-all \ # Blacklist overrides all restriction exceptions places on passwords by any *subsequent* `allow[-...]` --restrict directives. (See `allow[-...]`.)
--restrict allow-all \ # Override and whitelist every *subsequent* `deny[-...]` --restrict directives. (See `deny[-...]`.)
--restrict whitelist ... \ # The same as `--restrict deny-all --restrict allow-list ...` (See restriction directive ordering rules for `allow/deny[-...]` directives. (TODO: explain them on completeness in another section of the README.md file.)
--restrict whitelist-single ... \ # The same as `--restrict deny-all --restrict allow ...`
--restrict plugin:<loaded-plugin-name> ... \ # Add a restriction rule from an externally loaded plugin (TODO: see Plugins section of README.md file)
```
Loading…
Cancel
Save