├── tests ├── password └── test1-write-read.sh ├── .env ├── pureelib ├── __init__.py ├── plumbing │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── test_format.py │ ├── show.py │ ├── unpack.py │ ├── destroy.py │ ├── format.py │ ├── common.py │ └── subspecs.py └── porcelain │ ├── __init__.py │ ├── __main__.py │ ├── help.py │ ├── unmap.py │ ├── destroy.py │ ├── info.py │ ├── puree_main.py │ ├── format.py │ └── map.py ├── LICENSE ├── requirements.txt ├── puree ├── Makefile ├── .gitignore ├── setup.py ├── README.md └── docs ├── puree.1 └── puree.1.html /tests/password: -------------------------------------------------------------------------------- 1 | asdf -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=. 2 | -------------------------------------------------------------------------------- /pureelib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Public Domain 2 | -------------------------------------------------------------------------------- /pureelib/plumbing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pureelib/porcelain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pureelib/plumbing/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pysodium==0.7.5 2 | argon2-cffi==20.1.0 3 | -------------------------------------------------------------------------------- /pureelib/porcelain/__main__.py: -------------------------------------------------------------------------------- 1 | import puree_main 2 | puree_main.main() 3 | -------------------------------------------------------------------------------- /puree: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python3 -c 'import pureelib.porcelain.puree_main as x; x.main()' $* 3 | -------------------------------------------------------------------------------- /pureelib/porcelain/help.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | def asked_for_help(args): 4 | if len(args)>=1: 5 | return ('--help' in args) or (len(args)>=1 and args[0]=='help') or (len(args)>=2 and args[1]=='help') 6 | 7 | def show_help(): 8 | subprocess.call(['man', 'puree']) 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | install-deps: 3 | sudo apt install python3 python3-pip python3-setuptools libsodium23 || sudo dnf install python3-pip python3-setuptools libsodium 4 | install: 5 | sudo python3 -m pip install puree 6 | wheel: 7 | rm -fr dist/ build/ 8 | python3 setup.py sdist bdist_wheel 9 | test: 10 | bash tests/test1-write-read.sh 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Tests 2 | tests/test-results 3 | 4 | # Python 5 | *.swp 6 | *.pyc 7 | dist/ 8 | build/ 9 | puree.egg-info/ 10 | 11 | # VSCode 12 | .vscode/ 13 | !.vscode/settings.json 14 | !.vscode/tasks.json 15 | !.vscode/launch.json 16 | !.vscode/extensions.json 17 | *.code-workspace 18 | 19 | # Local History for Visual Studio Code 20 | .history/ 21 | 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="puree", 8 | version="1.0.2", 9 | author="Jay Sullivan", 10 | author_email="jay@identity.pub", 11 | description="PUREE: Password-based Uniform-Random-Equivalent Encryption", 12 | url="https://puree.cc", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | packages=setuptools.find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), 16 | scripts=['puree'], 17 | data_files = [('man/man1', ['docs/puree.1'])], 18 | classifiers=[ 19 | "Programming Language :: Python :: 3.6", 20 | "License :: Public Domain", 21 | "Operating System :: POSIX :: Linux", 22 | ], 23 | python_requires='>=3.6', 24 | install_requires=[ 25 | 'pytest>=5.4.3', 'pytest<6.0.0', 26 | 'pysodium>=0.7.5', 'pysodium<1.0.0', 27 | 'argon2-cffi>=20.1.0', 'argon2-cffi<21.0.0' 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /pureelib/porcelain/unmap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import getopt, sys 3 | import binascii 4 | import struct 5 | import array 6 | import subprocess 7 | 8 | import pureelib.plumbing.format as plumbing_format 9 | import pureelib.plumbing.subspecs as plumbing_subspecs 10 | import pureelib.plumbing.common as plumbing_common 11 | import pureelib.plumbing.show as plumbing_show 12 | 13 | # Explain command-line options to user 14 | def explain_options(): 15 | print( 16 | '''usage: puree unmap [-v] [-p ] 17 | 18 | For more information, type 'puree help'.''') 19 | sys.exit(2) 20 | 21 | def puree_unmap(argv): 22 | 23 | # Parse options 24 | try: 25 | options_tuples, arguments = getopt.gnu_getopt(argv[2:], ":v", []) 26 | options = dict(options_tuples) 27 | except getopt.error as err: 28 | explain_options() 29 | 30 | if(len(arguments)<1): 31 | explain_options() 32 | 33 | # Put options in variables 34 | plaindevice = arguments[0] 35 | verbose = (options.get('-v') != None) 36 | 37 | # Unmap the device 38 | exit_code = subprocess.call(['dmsetup','remove', plaindevice]) 39 | 40 | # Print results 41 | if(exit_code == 0): 42 | print("Successfully unmapped '" + plaindevice + "'.") 43 | else: 44 | plumbing_common.eprint("Failed to unmap device.") 45 | -------------------------------------------------------------------------------- /pureelib/porcelain/destroy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import getopt 4 | import binascii 5 | import struct 6 | import array 7 | import getpass 8 | 9 | import pureelib.plumbing.destroy as plumbing_destroy 10 | 11 | # If no cipher was provided, explain command-line options to the user 12 | def explain_options(): 13 | print( 14 | '''usage: puree destroy [-v] [-f] [-q] 15 | 16 | For more information, type 'puree help'.''') 17 | sys.exit(2) 18 | 19 | def puree_destroy(argv): 20 | 21 | # Parse options 22 | try: 23 | options_tuples, arguments = getopt.gnu_getopt(argv[2:], "vfq") 24 | options = dict(options_tuples) 25 | except getopt.error as err: 26 | explain_options() 27 | 28 | # Get required arguments 29 | if len(arguments) < 1: 30 | explain_options() 31 | device = arguments[0] 32 | 33 | # Put options in variables 34 | v = (options.get('-v') != None) 35 | f = (options.get('-f') != None) 36 | q = (options.get('-q') != None) 37 | 38 | # Prompt user for confirmation 39 | if(not f): 40 | print(f"WARNING: This will DESTROY ALL DATA on the device '{device}'.") 41 | result = input("To proceed, type destroy in all caps: ") 42 | if(result != "DESTROY"): 43 | print("Aborting.") 44 | sys.exit(2) 45 | 46 | # Commence destruction 47 | plumbing_destroy.puree_destroy(device, v, q) 48 | 49 | # Done 50 | print("") 51 | print("Done. All data on the disk has been destroyed.") 52 | -------------------------------------------------------------------------------- /pureelib/porcelain/info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import getopt, sys 3 | import binascii 4 | import struct 5 | import array 6 | 7 | import pureelib.plumbing.format as plumbing_format 8 | import pureelib.plumbing.subspecs as plumbing_subspecs 9 | import pureelib.plumbing.common as plumbing_common 10 | import pureelib.plumbing.show as plumbing_show 11 | import pureelib.plumbing.unpack as punpack 12 | 13 | # Explain command-line options to user 14 | def explain_options(): 15 | print( 16 | '''usage: puree info [-v] [-p ] 17 | 18 | For more information, type 'puree help'.''') 19 | sys.exit(2) 20 | 21 | def puree_info(argv): 22 | 23 | # Parse options 24 | try: 25 | options_tuples, arguments = getopt.gnu_getopt(argv[2:], "p:v", []) 26 | options = dict(options_tuples) 27 | except getopt.error as err: 28 | print ("puree: " + str(err) + "\n") 29 | explain_options() 30 | 31 | # Validate number of arguments 32 | if(len(arguments)<1): 33 | explain_options() 34 | 35 | # Put options in variables 36 | device = arguments[0] 37 | password_file = options.get("-p") 38 | verbose = (options.get("-v")!=None) 39 | 40 | # Read password from file, or prompt for password 41 | password = plumbing_common.prompt_for_password_or_read_from_file(password_file,"Password: ",None) 42 | 43 | # Now, read the device header 44 | with open(device, 'rb') as f: 45 | headers = punpack.puree_unpack(f,password) 46 | 47 | # Now, show header information 48 | print("The disk contains the following header information:") 49 | print("") 50 | plumbing_show.puree_show(headers,verbose) 51 | -------------------------------------------------------------------------------- /pureelib/plumbing/show.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import getopt, sys 3 | import binascii 4 | import struct 5 | import array 6 | import os 7 | 8 | import pureelib.plumbing.subspecs as plumbing_subspecs 9 | import pureelib.plumbing.common as plumbing_common 10 | 11 | # Print all information about the given headers 12 | def puree_show(headers,show_sensitive_data): 13 | 14 | # Show puree_salt 15 | puree_salt = headers[0] 16 | print(" salt: " + plumbing_common.to_hex(puree_salt)) 17 | 18 | # Show box_one 19 | box_one = headers[1] 20 | subspec_name = plumbing_subspecs.subspec_id_to_name(box_one.subspec) 21 | subspec_id_and_name = plumbing_common.to_hex(box_one.subspec) + (" # " + subspec_name if subspec_name else "") 22 | print(" box_one:") 23 | if(box_one.total_slots>0): 24 | print(" subspec : " + subspec_id_and_name) 25 | print(" used_slots : " + str(box_one.used_slots)) 26 | print(" total_slots : " + str(box_one.total_slots)) 27 | else: 28 | print(" subspec: " + subspec_id_and_name) 29 | 30 | # If subspec is disk_aes256_cbc_essiv_sha256, show its details 31 | if box_one.subspec in plumbing_subspecs.all_subspec_ids(): 32 | # Show box_two 33 | box_two = headers[2] 34 | print(" box_two:") 35 | print(" logical_start_sector : " + str(box_two.logical_start_sector)) 36 | print(" num_sectors : " + str(box_two.num_sectors)) 37 | print(" key : " + (plumbing_common.to_hex(box_two.key) if show_sensitive_data else "("+str(len(box_two.key))+" bytes)")) 38 | # If subspec is not supported, give an error 39 | else: 40 | raise ValueError('Unsupported subspec (' + plumbing_common.to_hex(box_one.subspec)+")") 41 | -------------------------------------------------------------------------------- /pureelib/plumbing/tests/test_format.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import io 4 | import os 5 | import pureelib.plumbing.format as plumbing_format 6 | import pureelib.plumbing.subspecs as plumbing_subspecs 7 | import pureelib.plumbing.unpack as plumbing_unpack 8 | import pureelib.plumbing.show as plumbing_show 9 | 10 | MEBIBYTE = 1048576 # bytes 11 | SECTOR = 512 # bytes 12 | 13 | class FormatTests(TestCase): 14 | 15 | def test_format_then_show_disk(self): 16 | # Arrange a fake "3MiB disk" 17 | fake_disk_size = 3*MEBIBYTE 18 | with io.BytesIO(bytearray([0]*fake_disk_size)) as fake_disk: 19 | 20 | # Arrange a fake password 21 | password = "bpassword" 22 | 23 | # Pack the header to disk 24 | plumbing_format.puree_format(fake_disk, # device 25 | fake_disk_size, 26 | plumbing_subspecs.DiskAes256XtsPlain64.subspec_name(), # cipher 27 | None, # password_file 28 | password, # password 29 | False, # warn on password changes 30 | False, # derive key from password 31 | True # show verbose output 32 | ) 33 | 34 | # Unpack the header 35 | headers = plumbing_unpack.puree_unpack(fake_disk,password) 36 | puree_salt = headers[0] 37 | box_one = headers[1] 38 | box_two = headers[2] 39 | 40 | # Assert 41 | plumbing_show.puree_show(headers,True) 42 | self.assertTrue(len(puree_salt) == 24) 43 | self.assertTrue(box_one.subspec == plumbing_subspecs.DiskAes256XtsPlain64.subspec_id()) 44 | self.assertTrue(box_two.logical_start_sector == MEBIBYTE/SECTOR) 45 | self.assertTrue(box_two.num_sectors == MEBIBYTE/SECTOR) 46 | self.assertTrue(len(box_two.key) == 64) 47 | -------------------------------------------------------------------------------- /tests/test1-write-read.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | DIR=$(cd "$(dirname "$0")" && pwd) 4 | TESTDIR=$DIR/test-results 5 | PUREEDIR=$(cd $DIR/.. && pwd) 6 | PUREE=$PUREEDIR/puree 7 | mkdir -p $TESTDIR 8 | cd $PUREEDIR 9 | LOOPDEVICE="" 10 | echo '---- Prompt for sudo ----' 11 | sudo echo "prompt_for_sudo" > /dev/null 12 | if [ $? -ne 0 ]; then 13 | echo "Can't run test: need sudo password" 14 | exit 2 15 | fi 16 | cleanup() { 17 | if [ -b /dev/mapper/puree-test-device ]; then 18 | sudo dmsetup remove puree-test-device 2>/dev/null || true 19 | fi 20 | if [ -f $TESTDIR/LOOPDEVICE ]; then 21 | losetup -d `cat $TESTDIR/LOOPDEVICE` 2>/dev/null || true 22 | fi 23 | } 24 | cleanup 25 | 26 | echo '---- Create an empty fake disk, and a fake password ----' 27 | cat /dev/zero | head -c 3145728 > $TESTDIR/fakedisk # 3MiB 28 | echo -n "asdf" > $TESTDIR/fakepassword 29 | 30 | # ---- Mount the fake disk as a device ---- 31 | LOOPDEVICE=`sudo losetup $TESTDIR/fakedisk --find --show` 32 | echo -n $LOOPDEVICE > $TESTDIR/LOOPDEVICE 33 | sudo chown $UID $LOOPDEVICE 34 | 35 | echo '---- Format the device ----' 36 | $PUREE format -v -f $LOOPDEVICE aes256-cbc-essiv-sha256 -p $TESTDIR/fakepassword 37 | 38 | echo '---- Map the device ----' 39 | #sudo dmsetup create puree-test-device --table "0 1 crypt aes-cbc-essiv:sha256 8b7acdddef30fc2aba58fb47c227a6b5fb8e2b90c8b42d960becaf3f7b125b62 0 $LOOPDEVICE 1" 40 | sudo $PUREE map $LOOPDEVICE /dev/mapper/puree-test-device -p $TESTDIR/fakepassword 41 | sudo chown $UID /dev/mapper/puree-test-device 42 | test -b /dev/mapper/puree-test-device && cat /dev/zero | head -c 512 > /dev/mapper/puree-test-device 43 | 44 | echo '==== TEST RESULTS ====' 45 | 46 | echo '---- hexdump /dev/mapper/puree-test-device ----' 47 | hexdump /dev/mapper/puree-test-device 48 | echo '---- hexdump $LOOPDEVICE ----' 49 | hexdump $LOOPDEVICE 50 | 51 | echo '---- SUCCESS ----' 52 | echo '==== END OF TEST RESULTS ====' 53 | cleanup 54 | 55 | -------------------------------------------------------------------------------- /pureelib/porcelain/puree_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import getopt, sys 3 | import pysodium 4 | import binascii 5 | import struct 6 | import array 7 | import signal 8 | import sys 9 | import argon2 10 | import getopt 11 | import subprocess 12 | 13 | import pureelib.plumbing.subspecs as plumbing_subspecs 14 | import pureelib.plumbing.common as plumbing_common 15 | 16 | import pureelib.porcelain.help as porcelain_help 17 | import pureelib.porcelain.format as porcelain_format 18 | import pureelib.porcelain.info as porcelain_info 19 | import pureelib.porcelain.map as porcelain_map 20 | import pureelib.porcelain.unmap as porcelain_unmap 21 | import pureelib.porcelain.destroy as porcelain_destroy 22 | 23 | # If no cipher was provided, explain command-line options to the user 24 | def explain_options(): 25 | print( 26 | '''usage: 27 | puree format [-v] [-p ] [-f] ... 28 | puree map [-v] [-p ] 29 | puree unmap [-v] [-p ] 30 | puree info [-v] [-p ] 31 | puree destroy [-v] [-a] [-f] 32 | 33 | For more information, type 'puree help'.''') 34 | sys.exit(2) 35 | 36 | def signal_handler(sig, frame): 37 | print("") 38 | sys.exit(0) 39 | 40 | def main(): 41 | 42 | signal.signal(signal.SIGINT, signal_handler) 43 | 44 | # Initialize Pysodium randomness 45 | pysodium.sodium_init() 46 | 47 | # See if we're in verbose mode 48 | v = False 49 | if "-v" in sys.argv[2:]: 50 | v = True 51 | 52 | try: 53 | # Pass control over to sub-porcelain 54 | if(len(sys.argv)<=1): 55 | explain_options() 56 | # Show Version 57 | elif(sys.argv[1] == '--version' or sys.argv[1] == '-version' or sys.argv[1] == 'version'): 58 | print("1.0.2") 59 | # Show Man Page 60 | elif(porcelain_help.asked_for_help(sys.argv[1:])): 61 | porcelain_help.show_help() 62 | return 0 63 | # Invoke Porcelain 64 | elif(sys.argv[1] == 'format'): 65 | porcelain_format.puree_format(sys.argv) 66 | elif(sys.argv[1] == 'info'): 67 | porcelain_info.puree_info(sys.argv) 68 | elif(sys.argv[1] == 'map'): 69 | porcelain_map.puree_map(sys.argv) 70 | elif(sys.argv[1] == 'unmap'): 71 | porcelain_unmap.puree_unmap(sys.argv) 72 | elif(sys.argv[1] == 'destroy'): 73 | porcelain_destroy.puree_destroy(sys.argv) 74 | else: 75 | explain_options() 76 | 77 | except ValueError as e: 78 | if(v): 79 | raise e 80 | print(e) 81 | sys.exit(2) 82 | except Exception as e: 83 | if(v): 84 | raise e 85 | print(e) 86 | sys.exit(1) 87 | 88 | if __name__ == '__main__': 89 | try: 90 | main() 91 | except ValueError as e: 92 | print(e) 93 | -------------------------------------------------------------------------------- /pureelib/plumbing/unpack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import getopt, sys 3 | import binascii 4 | import struct 5 | import array 6 | import os 7 | 8 | import pureelib.plumbing.subspecs as plumbing_subspecs 9 | import pureelib.plumbing.common as plumbing_common 10 | 11 | # TODO: Support password slots 12 | # TODO: Support anti-forensic region 13 | # TODO: Support subvolumes 14 | 15 | # Given device d and password p, extract, decrypt and return header objects 16 | def puree_unpack(device,password): 17 | 18 | # Read first sector from the disk 19 | device.seek(0) 20 | prefetch_size = 512 # TODO: With anti-forensic region, should pre-fetch 1MiB 21 | root_sector = device.read(prefetch_size) 22 | if(len(root_sector)<512): 23 | raise RuntimeError("Can't read header: device is smaller than than 512 bytes.") 24 | 25 | # Read salt 26 | offset_salt_start = 0 27 | offset_salt_end = 24 28 | salt = root_sector[offset_salt_start:offset_salt_end] 29 | 30 | # Calculate box_key 31 | pwhash_parameters = plumbing_common.calculate_pwhash_parameters(password) 32 | pwhash = plumbing_common.calculate_pwhash(salt,password,pwhash_parameters) 33 | box_key = pwhash 34 | 35 | # Read, decrypt, and unpack box_one 36 | offset_box1_start = offset_salt_end 37 | offset_box1_end = offset_box1_start + plumbing_subspecs.PureeBoxOne.get_ciphertext_size() 38 | box_one_enc = root_sector[offset_box1_start:offset_box1_end] 39 | box_one_packed = plumbing_common.decrypt_box(box_key,1,box_one_enc) 40 | box_one = plumbing_subspecs.PureeBoxOne.unpack(box_one_packed) 41 | 42 | # Verify subspec 43 | if box_one.subspec not in plumbing_subspecs.all_subspec_ids(): 44 | raise ValueError('Unsupported subspec: '+plumbing_common.to_hex(box_one.subspec)) 45 | 46 | # Unpack box2, with highly-coupled assumptions about which subspecs we support 47 | offset_box2_start = offset_box1_end 48 | offset_box2_end = offset_box2_start + 16 + box_one.len_of_box_2 49 | box_two_enc = root_sector[offset_box2_start:offset_box2_end] 50 | box_two_packed = plumbing_common.decrypt_box(box_key,2,box_two_enc) 51 | if box_one.subspec == plumbing_subspecs.DiskAes256CbcEssivSha256BoxTwo.subspec_id(): 52 | box_two = plumbing_subspecs.DiskAes256CbcEssivSha256BoxTwo.unpack(box_two_packed) 53 | elif box_one.subspec == plumbing_subspecs.DiskAes256XtsPlain64.subspec_id(): 54 | box_two = plumbing_subspecs.DiskAes256XtsPlain64.unpack(box_two_packed) 55 | elif box_one.subspec == plumbing_subspecs.DiskAes128CbcEssivSha256BoxTwo.subspec_id(): 56 | box_two = plumbing_subspecs.DiskAes128CbcEssivSha256BoxTwo.unpack(box_two_packed) 57 | elif box_one.subspec == plumbing_subspecs.DiskAes128XtsPlain64.subspec_id(): 58 | box_two = plumbing_subspecs.DiskAes128XtsPlain64.unpack(box_two_packed) 59 | else: 60 | raise ValueError('Unsupported subspec: '+plumbing_common.to_hex(box_one.subspec)) 61 | 62 | # Return the salt, box_one, box_two 63 | return [salt,box_one,box_two] 64 | 65 | -------------------------------------------------------------------------------- /pureelib/plumbing/destroy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import getopt, sys 3 | import binascii 4 | import struct 5 | import array 6 | import os 7 | import pysodium 8 | 9 | import pureelib.plumbing.subspecs as plumbing_subspecs 10 | import pureelib.plumbing.common as plumbing_common 11 | 12 | # Print iterations progress 13 | def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█', printEnd = "\r"): 14 | """ 15 | Call in a loop to create terminal progress bar 16 | @params: 17 | iteration - Required : current iteration (Int) 18 | total - Required : total iterations (Int) 19 | prefix - Optional : prefix string (Str) 20 | suffix - Optional : suffix string (Str) 21 | decimals - Optional : positive number of decimals in percent complete (Int) 22 | length - Optional : character length of bar (Int) 23 | fill - Optional : bar fill character (Str) 24 | printEnd - Optional : end character (e.g. "\r", "\r\n") (Str) 25 | """ 26 | percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) 27 | filledLength = int(length * iteration // total) 28 | bar = fill * filledLength + '-' * (length - filledLength) 29 | print('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix), end = printEnd) 30 | # Print New Line on Complete 31 | if iteration == total: 32 | print() 33 | 34 | def blockdev_size(path): 35 | """Return device size in bytes. 36 | """ 37 | with open(path, 'rb') as f: 38 | return f.seek(0, 2) or f.tell() 39 | 40 | # Format a disk wih the given data and specified subspec 41 | def puree_destroy(d, # device 42 | v, # show verbose output 43 | q # just destroy first and last MiB 44 | ): 45 | 46 | # Calculate how many multiples of 1GiB (and after that, remaining 1MiB) that are on the disk 47 | device_bytes = blockdev_size(d) 48 | if device_bytes%1048576!=0: 49 | raise RuntimeError("Refusing to destroy device: device size isn't a multiple of 1MiB, which is irregular.") 50 | device_mibs = device_bytes//1048576 51 | device_gibs_total = device_mibs//1024 52 | device_gibs_remaining_mibs = device_mibs%1024 53 | 54 | # Quick destroy 55 | if(q): 56 | with open(d, 'wb') as f: 57 | # Destroy first 1MiB 58 | f.seek(0) 59 | f.write(pysodium.randombytes(1048576)) 60 | # Destroy last 1MiB 61 | f.seek(device_bytes-1048576) 62 | f.write(pysodium.randombytes(1048576)) 63 | # Destroy ENTIRE DISK 64 | else: 65 | with open(d, 'wb') as f: 66 | f.seek(0) 67 | for g in range(0,device_gibs_total): 68 | for m in range(0,1024): 69 | f.write(pysodium.randombytes(1048576)) 70 | printProgressBar(g, device_gibs_total, prefix = 'Progress:', suffix = 'Complete', length = 50) 71 | for m in range(0,device_gibs_remaining_mibs): 72 | f.write(pysodium.randombytes(1048576)) 73 | printProgressBar(device_gibs_total, device_gibs_total, prefix = 'Progress:', suffix = 'Complete', length = 50) 74 | -------------------------------------------------------------------------------- /pureelib/porcelain/format.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import getopt 4 | import binascii 5 | import struct 6 | import array 7 | import getpass 8 | 9 | import pureelib.plumbing.format as plumbing_format 10 | import pureelib.plumbing.subspecs as plumbing_subspecs 11 | import pureelib.plumbing.common as plumbing_common 12 | import pureelib.plumbing.show as plumbing_show 13 | 14 | # If no cipher was provided, explain command-line options to the user 15 | def explain_options(): 16 | print( 17 | '''usage: puree format [-v] [-p ] [-f] ... 18 | 19 | For more information, type 'puree help'.''') 20 | sys.exit(2) 21 | 22 | def puree_format(argv): 23 | 24 | # List of valid subspecs 25 | subspecs = plumbing_subspecs.all_subspec_names() 26 | 27 | # Parse options 28 | try: 29 | options_tuples, arguments = getopt.gnu_getopt(argv[2:], "c:p:Wfv") 30 | options = dict(options_tuples) 31 | except getopt.error as err: 32 | print ("puree: " + str(err) + "\n") 33 | explain_options() 34 | 35 | # Prompt for cipher if not provided as argument 36 | if len(arguments)==0: 37 | explain_options() 38 | # If they didn't supply a subspec, we'll help them out 39 | elif len(arguments) == 1: 40 | device = arguments[0] 41 | print("Valid subspecs: ") 42 | subspecs_map = sorted({(v,k) for v, k in enumerate(subspecs)}, key=lambda x:x[0]) 43 | for subspec in subspecs_map: 44 | print(f" {str(subspec[0]+1)} {subspec[1]}") 45 | entry = input("Choose a subspec: ") 46 | subspec = None 47 | for s in subspecs_map: 48 | if(str(s[0]+1)==entry or s[1]==entry): 49 | subspec = s[1] 50 | if(subspec not in subspecs): 51 | raise ValueError(f"Not a valid subspec: '{entry}'") 52 | # Get required arguments 53 | elif len(arguments) >= 2: 54 | device = arguments[0] 55 | subspec = arguments[1] 56 | 57 | # Put options in variables 58 | password_file = options.get('-p') 59 | h = (options.get('-h') != None) 60 | y = (options.get('-f') != None) 61 | v = (options.get('-v') != None) 62 | 63 | # Read password from file, or prompt for password 64 | password = plumbing_common.prompt_for_password_or_read_from_file(password_file) 65 | 66 | # Assert that all required options were provided 67 | if not subspec or not device or (not password_file and not password): 68 | explain_options() 69 | 70 | # Prompt user for confirmation 71 | print("You have chosen to format device '"+device+"' with the following options:") 72 | print("") 73 | print(" subspec: " + subspec) 74 | if(password_file): 75 | print(" Password File: " + password_file) 76 | if y: 77 | print(" Not prompting for confirmation.") 78 | if(not y): 79 | print("") 80 | print(f"WARNING: This will DESTROY data on the device '{device}'.") 81 | result = input("To proceed, type yes in all caps: ") 82 | if(result != "YES"): 83 | print("Aborting.") 84 | sys.exit(2) 85 | 86 | # Format the disk 87 | with open(device,'r+b') as f: 88 | headers = plumbing_format.puree_format(f,None,subspec,password_file,password,False,h,v) 89 | 90 | # If -v was chosen, show all the variables 91 | if(v): 92 | print("") 93 | print("Done. The disk was successfully formatted with the following header information:") 94 | print("") 95 | plumbing_show.puree_show(headers,v) 96 | else: 97 | print("") 98 | print("Done. The disk was successfully formatted.") 99 | -------------------------------------------------------------------------------- /pureelib/plumbing/format.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import getopt, sys 3 | import binascii 4 | import struct 5 | import array 6 | import os 7 | import pysodium 8 | 9 | import pureelib.plumbing.subspecs as plumbing_subspecs 10 | import pureelib.plumbing.common as plumbing_common 11 | 12 | # TODO: Support password slots 13 | # TODO: Support subvolumes 14 | # TODO: Support anti-forensic region 15 | # TODO: Write end-of-disk shadow header 16 | 17 | # Format a disk wih the given data and specified subspec 18 | def puree_format(d, # device, 19 | blockdev_size, # device size 20 | c, # cipher 21 | p, # password_file 22 | password, # password 23 | w, # warn on password changes 24 | h, # derive key from password 25 | v # show verbose output 26 | ): 27 | mebibyte = 1048576 # bytes 28 | 29 | # Sanity checks 30 | if(blockdev_size == None): 31 | blockdev_size = get_blockdev_size(d) 32 | if(blockdev_size<=2*mebibyte): 33 | raise RuntimeError("Block device is too small (<=2MiB bytes) to format.") 34 | if(blockdev_size%512!=0): 35 | raise RuntimeError("Block device is not a multiple of 512, which is too weird to continue.") 36 | 37 | # Verify that the subspec is valid 38 | if plumbing_subspecs.subspec_name_to_id(c) == None: 39 | raise ValueError('Unsupported subspec: '+c) 40 | 41 | # Initialize Salt 42 | salt = pysodium.randombytes(24) 43 | 44 | # Read the password from the password file 45 | if(password == None): 46 | password = plumbing_common.read_password_from_file(p) 47 | 48 | # Calculate box_key 49 | # TODO: Support slots, so box_key can be found from multiple passwords 50 | # For now, box_key == blake2b(salt||password) 51 | pwhash_parameters = plumbing_common.calculate_pwhash_parameters(password) 52 | pwhash = plumbing_common.calculate_pwhash(salt,password,pwhash_parameters) 53 | box_key = pwhash 54 | 55 | # Initialize Box Two (With highly-coupled assumptions about which subspecs we support) 56 | key_size = 0 57 | if c == plumbing_subspecs.DiskAes256XtsPlain64.subspec_name(): 58 | box_two = plumbing_subspecs.DiskAes256XtsPlain64() 59 | key_size = 64 60 | elif c == plumbing_subspecs.DiskAes256CbcEssivSha256BoxTwo.subspec_name(): 61 | box_two = plumbing_subspecs.DiskAes256CbcEssivSha256BoxTwo() 62 | key_size = 32 63 | elif c == plumbing_subspecs.DiskAes128XtsPlain64.subspec_name(): 64 | box_two = plumbing_subspecs.DiskAes128XtsPlain64() 65 | key_size = 32 66 | elif c == plumbing_subspecs.DiskAes128CbcEssivSha256BoxTwo.subspec_name(): 67 | box_two = plumbing_subspecs.DiskAes128CbcEssivSha256BoxTwo() 68 | key_size = 16 69 | else: 70 | raise ValueError('Unsupported subspec: '+c) 71 | box_two.logical_start_sector = (1*mebibyte)//512 72 | box_two.num_sectors = (blockdev_size-2*mebibyte)//512 73 | box_two.key = pysodium.randombytes(key_size) 74 | 75 | # Initialize Box One 76 | box_one = plumbing_subspecs.PureeBoxOne() 77 | box_one.subspec = plumbing_subspecs.subspec_name_to_id(c) 78 | if(box_one.subspec == None): 79 | raise ValueError('Unsupported subspec: '+c) 80 | box_one.total_slots = 0 81 | box_one.used_slots = 0 82 | box_one.len_of_box_2 = box_two.get_inner_size() 83 | 84 | # Encrypt Box One 85 | box_one_enc = plumbing_common.encrypt_box(box_key,1,box_one.pack()) 86 | 87 | # Encrypt Box Two 88 | box_two_enc = plumbing_common.encrypt_box(box_key,2,box_two.pack()) 89 | 90 | # Now, write to the disk 91 | d.seek(0) 92 | combined_header = salt + box_one_enc + box_two_enc 93 | mib = 1048576 94 | filler = pysodium.randombytes(mib - len(combined_header)) 95 | d.write(combined_header+filler) 96 | 97 | # Return all headers 98 | return [salt,box_one,box_two] 99 | 100 | def get_blockdev_size(device): 101 | """Return device size in bytes. 102 | """ 103 | return device.seek(0, 2) or device.tell() 104 | -------------------------------------------------------------------------------- /pureelib/porcelain/map.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import getopt, sys 3 | import binascii 4 | import struct 5 | import array 6 | import subprocess 7 | 8 | import pureelib.plumbing.format as plumbing_format 9 | import pureelib.plumbing.subspecs as plumbing_subspecs 10 | import pureelib.plumbing.common as plumbing_common 11 | import pureelib.plumbing.show as plumbing_show 12 | import pureelib.plumbing.unpack as punpack 13 | import pureelib.porcelain.help as porcelain_help 14 | 15 | # Explain command-line options to user 16 | def explain_options(): 17 | print( 18 | '''usage: puree map [-v] [-p ] 19 | 20 | For more information, type 'puree help'.''') 21 | sys.exit(2) 22 | 23 | def puree_map(argv): 24 | 25 | # Validate root 26 | 27 | # Parse options 28 | try: 29 | options_tuples, arguments = getopt.gnu_getopt(argv[2:], "dnp:v", []) 30 | options = dict(options_tuples) 31 | except getopt.error as err: 32 | explain_options() 33 | if len(arguments)<2: 34 | explain_options() 35 | 36 | # Put options in variables 37 | device = arguments[0] 38 | plaindevice = arguments[1] 39 | password_file = options.get('-p') 40 | verbose = (options.get('-v') != None) 41 | 42 | # Validate and strip '/dev/mapper' from plaindevice 43 | if not plaindevice.startswith('/dev/mapper/'): 44 | raise ValueError("puree: error: must begin with '/dev/mapper/...'") 45 | plaindevice = plaindevice[len('/dev/mapper/'):] 46 | if len(plaindevice)==0: 47 | raise ValueError("puree: error: must be of the form '/dev/mapper/...'") 48 | 49 | # If password_file was not supplied, read the password from it 50 | password = plumbing_common.prompt_for_password_or_read_from_file(password_file, "Password: ",None) 51 | 52 | # Read the device header 53 | with open(device, 'rb') as f: 54 | headers = punpack.puree_unpack(f,password) 55 | salt = headers[0] 56 | box1 = headers[1] 57 | box2 = headers[2] 58 | 59 | # Show header information 60 | if verbose: 61 | print("The disk contains the following header information:") 62 | print("") 63 | plumbing_show.puree_show(headers,verbose) 64 | 65 | # Assert that the subspec is supported 66 | subspec_id = box1.subspec 67 | subspec_name = plumbing_subspecs.subspec_id_to_name(subspec_id) 68 | if subspec_name == None: 69 | raise ValueError("Unsupported subspec: " + plumbing_common.to_hex(box1.subspec)) 70 | 71 | # Determine the dm-crypt cipher name 72 | cipher_name = None 73 | if subspec_id == plumbing_subspecs.DiskAes256XtsPlain64.subspec_id(): 74 | cipher_name = 'aes-xts-plain64' 75 | elif subspec_id == plumbing_subspecs.DiskAes128XtsPlain64.subspec_id(): 76 | cipher_name = 'aes-xts-plain64' 77 | elif subspec_id == plumbing_subspecs.DiskAes256CbcEssivSha256BoxTwo.subspec_id(): 78 | cipher_name = 'aes-cbc-essiv:sha256' 79 | elif subspec_id == plumbing_subspecs.DiskAes128CbcEssivSha256BoxTwo.subspec_id(): 80 | cipher_name = 'aes-cbc-essiv:sha256' 81 | else: 82 | raise ValueError("Unsupported subspec: " + plumbing_common.to_hex(box1.subspec)) 83 | 84 | # Create a "dmsetup table" string which contains all the parameters 85 | logical_start_sector = str(box2.logical_start_sector) 86 | num_sectors = str(box2.num_sectors) 87 | key = plumbing_common.to_hex(box2.key) 88 | table = ('0 %s crypt %s %s 0 %s %s' \ 89 | % (num_sectors,cipher_name,key,device,logical_start_sector)) 90 | 91 | # Map the device 92 | completed_process = subprocess.run(['dmsetup','create', str(plaindevice)], \ 93 | input=table, encoding='utf-8', capture_output=True) 94 | 95 | # If verbose mode, show the output from dmsetup 96 | if(verbose): 97 | sys.stdout.write(completed_process.stdout) 98 | sys.stderr.write(completed_process.stderr) 99 | 100 | # Show device name 101 | if(completed_process.returncode == 0): 102 | print("Device mapped as '/dev/mapper/" + plaindevice + "'.") 103 | else: 104 | plumbing_common.eprint("puree: error: failed to map device") 105 | -------------------------------------------------------------------------------- /pureelib/plumbing/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import getopt, sys 3 | import binascii 4 | import struct 5 | import array 6 | import os 7 | import getpass 8 | import ctypes 9 | import hashlib 10 | import pysodium 11 | import argon2 12 | 13 | # Common helper functions 14 | 15 | def eprint(*args, **kwargs): 16 | print(*args, file=sys.stderr, **kwargs) 17 | 18 | def to_hex(ba): 19 | return binascii.hexlify(bytearray(ba)).decode() 20 | 21 | def from_hex(s): 22 | return bytearray.fromhex(s) 23 | 24 | def u8_to_be(n): 25 | return struct.pack('>B', n) 26 | 27 | def u8_from_be(a): 28 | return struct.unpack('>B', a)[0] 29 | 30 | def u16_to_be(n): 31 | return struct.pack('>H', n) 32 | 33 | def u16_from_be(a): 34 | return struct.unpack('>H', a)[0] 35 | 36 | def u32_to_be(n): 37 | return struct.pack('>I', n) 38 | 39 | def u32_from_be(a): 40 | return struct.unpack('>I', a)[0] 41 | 42 | def u64_to_be(n): 43 | return struct.pack('>Q', n) 44 | 45 | def u64_from_be(a): 46 | return struct.unpack('>Q', a)[0] 47 | 48 | def __check(code): 49 | if code != 0: 50 | raise ValueError 51 | 52 | # ctypes imports 53 | 54 | def bytearray_to_char_array(ba): 55 | return (ctypes.c_char * len(ba)).from_buffer(bytearray(ba)) 56 | 57 | # Common boxing/unboxing functions 58 | 59 | def encrypt_box(box_key,box_number,plaintext_header_packed): 60 | r = pysodium.crypto_aead_chacha20poly1305_encrypt(bytearray_to_char_array(plaintext_header_packed),None,u64_to_be(box_number),box_key) 61 | return r 62 | 63 | def decrypt_box(box_key,header_number,ciphertext_header_packed): 64 | try: 65 | return pysodium.crypto_aead_chacha20poly1305_decrypt(bytearray_to_char_array(ciphertext_header_packed),None,u64_to_be(header_number),box_key) 66 | except ValueError: 67 | raise ValueError('Failed to decrypt disk. Either (A) the device is not formatted with PUREE, (B) the password is invalid, or (C) the disk has been corrupted.') 68 | 69 | # Common Password Functions 70 | 71 | def read_password_from_file(p): 72 | # Read the password from the password file 73 | password = None 74 | with open(p, 'r') as f: 75 | password = f.read() 76 | return password 77 | 78 | # Functions for deriving pwhash 79 | 80 | parameter_char_lookup = { # TODO: Start with alpha 81 | 'a': {'p': 0, 'm': 0, 't': 0}, 82 | 'b': {'p': 1, 'm': 16, 't': 1}, 83 | 'c': {'p': 1, 'm': 18, 't': 1}, 84 | 'd': {'p': 4, 'm': 18, 't': 4}, 85 | 'e': {'p': 1, 'm': 20, 't': 1}, 86 | 'f': {'p': 4, 'm': 20, 't': 4}, 87 | 'g': {'p': 1, 'm': 22, 't': 1}, 88 | 'h': {'p': 4, 'm': 22, 't': 4}, 89 | 'i': {'p': 1, 'm': 24, 't': 1}, 90 | 'j': {'p': 4, 'm': 24, 't': 4}, 91 | } 92 | 93 | def prompt_for_password(prompt,prompt_confirm): 94 | if sys.stdin.isatty(): 95 | password1 = getpass.getpass(prompt) 96 | if(prompt_confirm): 97 | password2 = getpass.getpass("Choose a password (again to confirm): ") 98 | if(password1 != password2): 99 | raise ValueError("puree: error: passwords don't match") 100 | password_verify(password1) 101 | else: 102 | raise ValueError("puree: error: password was not supplied via tty or command-line argument") 103 | return password1 104 | 105 | def prompt_for_password_or_read_from_file(password_file, prompt="Choose a password: ", prompt_confirm="Choose a password (again to confirm): "): 106 | if(password_file): 107 | password = read_password_from_file(password_file) 108 | else: 109 | password = prompt_for_password(prompt,prompt_confirm) 110 | return password 111 | 112 | 113 | def password_verify(password): 114 | if len(password)==0 or not parameter_char_lookup.get(password[0]): 115 | raise ValueError("puree: error: password's first digit must be a parameter char ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', or 'i')") 116 | 117 | def calculate_pwhash_parameters(password): 118 | c = password[0] if len(password)>0 else "-" 119 | t = parameter_char_lookup 120 | password_verify(password) 121 | return [t[c]['p'],t[c]['m'],t[c]['t']] 122 | 123 | 124 | def calculate_pwhash(salt,password,pwhash_parameters): 125 | pwhash = None 126 | t = pwhash_parameters[0] 127 | m = 2**(pwhash_parameters[1]) 128 | p = pwhash_parameters[2] 129 | if(t==0): 130 | pwhash = hashlib.blake2b(salt+password.encode("utf-8"), digest_size=32).digest() 131 | else: 132 | pwhash = argon2.low_level.hash_secret_raw( 133 | password.encode('utf-8'), salt, 134 | time_cost=t, memory_cost=m, parallelism=p, hash_len=32, 135 | type=argon2.low_level.Type.ID 136 | ) 137 | return pwhash 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PUREE 2 | ## Password-based Uniform-Random-Equivalent Encryption 3 | 4 | ### What is PUREE? 5 | 6 | PUREE is a disk encryption header format and `puree` is a command-line tool suite which, together, allow you to password-protect disk devices, using full-disk encryption, in such a way such that the entire disk is indistinguishable from random data. Notably, this occurs without the need to store any associated data on a separate disk. The on-disk format is simple, secure, and extensible, while the command-line tool is convenient and simple to use. 7 | 8 | ### What are the prerequisites? 9 | 10 | While the PUREE disk format is platform-independent, the `puree` command-line tool currently only supports Linux. 11 | 12 | In addition, the following system libraries must be installed: 13 | 14 | sudo apt install python3 python3-pip python3-setuptools libsodium23 # Debian, Ubuntu 15 | sudo dnf install python3 python3-pip python3-setuptools libsodium # Red Hat, Fedora 16 | 17 | ### How do I install `puree`? 18 | Install `puree` with: 19 | 20 | sudo python3 -m pip install puree 21 | 22 | (Generally, using `sudo` to perform `pip install` is not recommended. However, because disk devices usually require root permission to access, `puree` is often invoked with `sudo`—and `sudo` tends to hide userspace-installed Python packages from the `PATH`. If you like, you may install `puree` into `~/.local` by instead running "`python -m pip install --user puree pysodium argon2-cffi`", but be aware you'll likely get an error if you invoke `puree` via `sudo`.) 23 | 24 | ### How do I use it? 25 | 26 | Let's go through the complete flow, from formatting a device with PUREE all the way to mounting it. 27 | 28 | (WARNING: By encrypting a device with PUREE, you will be wiping all data from the disk.) 29 | 30 | We'll encrypt device `/dev/sdz` with `AES-256` in `XTS` mode. 31 | 32 | First, format the disk with PUREE: 33 | 34 | sudo puree format /dev/sdz aes256-xts-plain64 35 | 36 | (You will be prompted for a password. PUREE will require you to prefix your password with a "parameter char"; see the "Choosing a parameter char" section below for an explanation.) 37 | 38 | Your device should now be encrypted. 39 | 40 | Next, you'll need to "map" your encrypted disk device to a new virtual device: 41 | 42 | sudo puree map /dev/sdz /dev/mapper/sdz 43 | 44 | (You will be prompted to enter the disk's password.) 45 | 46 | The virtual device should now be available at `/dev/mapper/sdz`; you can now treat `/dev/mapper/sdz` as you would a normal disk device. 47 | 48 | For example, to format it with a filesystem, then mount it: 49 | 50 | sudo mkfs.ext4 /dev/mapper/sdz 51 | sudo mount /dev/mapper/sdz /mnt 52 | 53 | You now have an encrypted fileystem available at `/mnt`. 54 | 55 | To unmap the device, run: 56 | 57 | sudo puree unmap /dev/mapper/sdz 58 | 59 | To prove to yourself that the disk is encrypted, try running `sudo hexdump -C /dev/sdz | less`, and you'll see something like this: 60 | 61 | ``` 62 | 00000000 3ac41e42 da074126 fb9d4c6a 01a15f56 |...B..A&..Lj.._V| 63 | 00000010 c71c6c47 3a891a07 77af909a 4efb1a8f |..lG:...w...N...| 64 | 00000020 72fc3eac 1766db1d 55d2c0cd 14a666bd |r.>..f..U.....f.| 65 | 00000030 5592d610 bbc3ad81 46eb2bf7 cec566b6 |U.......F.+...f.| 66 | 00000040 8c44df17 8868323d d175458d 4327d107 |.D...h2=.uE.C'..| 67 | 00000050 6dbf3af8 11083156 dd3bb235 83826b62 |m.:...1V.;.5..kb| 68 | 00000060 fad3a02d 48acebc5 7b79ce68 ec9e68f1 |...-H...{y.h..h.| 69 | 00000070 4c5daf93 1a2bb71f ace7f417 ca627d05 |L]...+.......b}.| 70 | 00000080 39568ce6 5ec12f58 38c056d3 d682d728 |9V..^./X8.V....(| 71 | 00000090 446df278 d823fee0 ff2f4c04 434b5f5e |Dm.x.#.../L.CK_^| 72 | 000000a0 bc425830 55c455cd b4439385 c59bf3fd |.BX0U.U..C......| 73 | 000000b0 62019305 a5f38ce9 12c0c138 76f31f1b |b..........8v...| 74 | 000000c0 8e67545a e3abf95a 2247fc0c 5c55558c |.gTZ...Z"G....U.| 75 | 000000d0 01c62344 8fbb35df 80b313da 63269760 |..#D..5.....c&.`| 76 | 000000e0 4dfbd88d d32a1179 e4038d7c 3c4412eb |M....*.y...| parallelism: 1, memory: 75MiB, iterations: 1` 101 | - `'c' => parallelism: 1, memory: 250MiB, iterations: 1` 102 | - `'d' => parallelism: 4, memory: 250MiB, iterations: 4` 103 | - `'e' => parallelism: 1, memory: 1GiB, iterations: 1` 104 | - `'f' => parallelism: 4, memory: 1GiB, iterations: 4` 105 | - `'g' => parallelism: 1, memory: 4GiB, iterations: 1` 106 | - `'h' => parallelism: 4, memory: 4GiB, iterations: 4` 107 | - `'i' => parallelism: 1, memory: 16GiB, iterations: 1` 108 | - `'j' => parallelism: 4, memory: 16GiB, iterations: 4` 109 | 110 | Also: 111 | 112 | - `'a' => simply hash the salt and password with blake2b` 113 | 114 | As CPU and RAM become cheaper, more parameter chars will be added to this table. 115 | -------------------------------------------------------------------------------- /pureelib/plumbing/subspecs.py: -------------------------------------------------------------------------------- 1 | import pureelib.plumbing.common as plumbing_common 2 | 3 | class PureeBoxOne: 4 | 5 | @classmethod 6 | def get_ciphertext_size(cls): 7 | return 16 + cls.get_plaintext_size() 8 | @classmethod 9 | def get_plaintext_size(cls): 10 | return 8+1+1+2 11 | @classmethod 12 | def get_inner_size(self): 13 | return 8+1+1+2 14 | 15 | # property: byte[8] subspec 16 | # property: byte[1] used_slots 17 | # property: byte[1] total_slots 18 | # property: byte[2] len_of_box_2 19 | @classmethod 20 | def unpack(cls,packed): 21 | if len(packed) != __class__.get_plaintext_size(): 22 | raise RuntimeError("The PUREE header of this disk is corrupt.") 23 | r = PureeBoxOne() 24 | r.subspec = packed[0:8] 25 | r.used_slots = plumbing_common.u8_from_be(packed[8:9]) 26 | r.total_slots = plumbing_common.u8_from_be(packed[9:10]) 27 | r.len_of_box_2 = plumbing_common.u16_from_be(packed[10:12]) 28 | return r 29 | 30 | def pack(self): 31 | return self.subspec \ 32 | +plumbing_common.u8_to_be(self.used_slots) \ 33 | +plumbing_common.u8_to_be(self.total_slots) \ 34 | +plumbing_common.u16_to_be(self.len_of_box_2) 35 | 36 | class DiskAes256CbcEssivSha256BoxTwo: 37 | 38 | @classmethod 39 | def subspec_name(clas): 40 | return 'aes256-cbc-essiv-sha256' 41 | @classmethod 42 | def subspec_id(cls): 43 | return bytearray.fromhex('9abf8b191e4a84a4') 44 | @classmethod 45 | def get_inner_size(self): 46 | return 32+8+8 47 | 48 | # property: byte[32] key 49 | # property: byte[8] logical_start_sector 50 | # property: byte[8] num_sectors 51 | @classmethod 52 | def unpack(cls,packed): 53 | if(len(packed) != cls.get_inner_size()): 54 | raise RuntimeError("The PUREE header of this disk is corrupt.") 55 | r = DiskAes256CbcEssivSha256BoxTwo() 56 | r.key = packed[0:32] 57 | r.logical_start_sector = plumbing_common.u64_from_be(packed[32:40]) 58 | r.num_sectors = plumbing_common.u64_from_be(packed[40:48]) 59 | return r 60 | def pack(self): 61 | return self.key \ 62 | + plumbing_common.u64_to_be(self.logical_start_sector) \ 63 | + plumbing_common.u64_to_be(self.num_sectors) 64 | 65 | class DiskAes256XtsPlain64: 66 | 67 | @classmethod 68 | def subspec_name(clas): 69 | return 'aes256-xts-plain64' 70 | @classmethod 71 | def subspec_id(cls): 72 | return bytearray.fromhex('cf43556cf0b3ebb7') 73 | @classmethod 74 | def get_inner_size(self): 75 | return 64+8+8 76 | 77 | # property: byte[64] key 78 | # property: byte[8] logical_start_sector 79 | # property: byte[8] num_sectors 80 | @classmethod 81 | def unpack(cls,packed): 82 | if(len(packed) != cls.get_inner_size()): 83 | raise RuntimeError("The PUREE header of this disk is corrupt.") 84 | r = DiskAes256XtsPlain64() 85 | r.key = packed[0:64] 86 | r.logical_start_sector = plumbing_common.u64_from_be(packed[64:72]) 87 | r.num_sectors = plumbing_common.u64_from_be(packed[72:80]) 88 | return r 89 | def pack(self): 90 | return self.key \ 91 | + plumbing_common.u64_to_be(self.logical_start_sector) \ 92 | + plumbing_common.u64_to_be(self.num_sectors) 93 | 94 | class DiskAes128CbcEssivSha256BoxTwo: 95 | 96 | @classmethod 97 | def subspec_name(clas): 98 | return 'aes128-cbc-essiv-sha256' 99 | @classmethod 100 | def subspec_id(cls): 101 | return bytearray.fromhex('f83789a7bf8f0e43') 102 | @classmethod 103 | def get_inner_size(self): 104 | return 16+8+8 105 | 106 | # property: byte[16] key 107 | # property: byte[8] logical_start_sector 108 | # property: byte[8] num_sectors 109 | @classmethod 110 | def unpack(cls,packed): 111 | if(len(packed) != cls.get_inner_size()): 112 | raise RuntimeError("The PUREE header of this disk is corrupt.") 113 | r = DiskAes128CbcEssivSha256BoxTwo() 114 | r.key = packed[0:16] 115 | r.logical_start_sector = plumbing_common.u64_from_be(packed[16:24]) 116 | r.num_sectors = plumbing_common.u64_from_be(packed[24:32]) 117 | return r 118 | def pack(self): 119 | return self.key \ 120 | + plumbing_common.u64_to_be(self.logical_start_sector) \ 121 | + plumbing_common.u64_to_be(self.num_sectors) 122 | 123 | class DiskAes128XtsPlain64: 124 | 125 | @classmethod 126 | def subspec_name(clas): 127 | return 'aes128-xts-plain64' 128 | @classmethod 129 | def subspec_id(cls): 130 | return bytearray.fromhex('a9d4d04dfaf36314') 131 | @classmethod 132 | def get_inner_size(self): 133 | return 32+8+8 134 | 135 | # property: byte[32] key 136 | # property: byte[8] logical_start_sector 137 | # property: byte[8] num_sectors 138 | @classmethod 139 | def unpack(cls,packed): 140 | if(len(packed) != cls.get_inner_size()): 141 | raise RuntimeError("The PUREE header of this disk is corrupt.") 142 | r = DiskAes128XtsPlain64() 143 | r.key = packed[0:32] 144 | r.logical_start_sector = plumbing_common.u64_from_be(packed[32:40]) 145 | r.num_sectors = plumbing_common.u64_from_be(packed[40:48]) 146 | return r 147 | def pack(self): 148 | return self.key \ 149 | + plumbing_common.u64_to_be(self.logical_start_sector) \ 150 | + plumbing_common.u64_to_be(self.num_sectors) 151 | 152 | # List all valid subspec names 153 | def all_subspec_names(): 154 | return [DiskAes256XtsPlain64.subspec_name(), 155 | DiskAes256CbcEssivSha256BoxTwo.subspec_name(), 156 | DiskAes128XtsPlain64.subspec_name(), 157 | DiskAes128CbcEssivSha256BoxTwo.subspec_name()] 158 | 159 | # List all valid subspec ids 160 | def all_subspec_ids(): 161 | return [DiskAes256XtsPlain64.subspec_id(), 162 | DiskAes256CbcEssivSha256BoxTwo.subspec_id(), 163 | DiskAes128XtsPlain64.subspec_id(), 164 | DiskAes128CbcEssivSha256BoxTwo.subspec_id()] 165 | 166 | # Find the subspec_id given the subspec_name 167 | def subspec_name_to_id(name): 168 | if name == DiskAes256XtsPlain64.subspec_name(): 169 | return DiskAes256XtsPlain64.subspec_id() 170 | elif name == DiskAes256CbcEssivSha256BoxTwo.subspec_name(): 171 | return DiskAes256CbcEssivSha256BoxTwo.subspec_id() 172 | elif name == DiskAes128XtsPlain64.subspec_name(): 173 | return DiskAes128XtsPlain64.subspec_id() 174 | elif name == DiskAes128CbcEssivSha256BoxTwo.subspec_name(): 175 | return DiskAes128CbcEssivSha256BoxTwo.subspec_id() 176 | return None 177 | 178 | # Find the subspec_name via subspec_id 179 | def subspec_id_to_name(idd): 180 | if idd == DiskAes256XtsPlain64.subspec_id(): 181 | return DiskAes256XtsPlain64.subspec_name() 182 | elif idd == DiskAes256CbcEssivSha256BoxTwo.subspec_id(): 183 | return DiskAes256CbcEssivSha256BoxTwo.subspec_name() 184 | elif idd == DiskAes128XtsPlain64.subspec_id(): 185 | return DiskAes128XtsPlain64.subspec_name() 186 | elif idd == DiskAes128CbcEssivSha256BoxTwo.subspec_id(): 187 | return DiskAes128CbcEssivSha256BoxTwo.subspec_name() 188 | return None -------------------------------------------------------------------------------- /docs/puree.1: -------------------------------------------------------------------------------- 1 | .TH puree 1 "June 2020" "puree-v1.0.0" "puree" 2 | 3 | .ad l 4 | .SH NAME 5 | .PP 6 | \fBpuree\fP - encrypt devices with PUREE (the full-disk encryption format) 7 | 8 | .SH SYNOPSIS 9 | .PP 10 | 11 | \fBpuree info\fP [-v] [-p] <\fIpassword_file\fP>] [-f] <\fIcipherdevice\fP> 12 | 13 | \fBpuree format\fP [-v] [-p] <\fIpassword_file\fP>] [-f] <\fIcipherdevice\fP> <\fIsubspec\fP> 14 | 15 | \fBpuree map\fP [-v] [-p] <\fIpassword_file\fP>] [-f] <\fIcipherdevice\fP> <\fIplaindevice\fP> 16 | 17 | \fBpuree unmap\fP [-v] <\fIplaindevice\fP> 18 | 19 | \fBpuree destroy\fP [-v] [-f] [-q] <\fIdevice\fP> 20 | 21 | .SH DESCRIPTION 22 | .PP 23 | 24 | The \fBpuree\fP tool suite allows you to password-protect disk devices (using \fBdm-crypt\fP and the PUREE header format) in such a way that the entire disk is indistinguishable from random data. Notably this occurs without requiring you to store any associated data on a separate disk (in comparison, most disk encryption formats require the user to store a separate "detached header" somewhere to accomplish this). 25 | 26 | The full lifecycle of encrypting a disk with PUREE is as follows: 27 | 28 | .RS 4 29 | 1) First format the disk with '\fBpuree format\fP' (keeping in mind this will \fBdestroy\fP existing data from the disk). 30 | 31 | 2) If you'd like, you can now use the \fBpuree info\fP command to verify that the disk is formatted correctly, and that you still have the correct password to the disk. 32 | 33 | 3) Next, map the disk to a virtual device with '\fBpuree map\fP'. This virtual device (which will be located under \fB/dev/mapper/\fP) can then be treated as if it were a new disk. Do whatever you'd like with it now, such as as format it with a filesystem, or mount it. 34 | 35 | 4) When you're done, use \fBpuree unmap\fP to unmap the virtual device. After doing this, the disk "locked", and will not be unlocked until you call \fBpuree map\fP again. 36 | 37 | 5) If you'd like to destroy all data on a disk, use \fBpuree destroy\fP. 38 | .RE 39 | 40 | .SH OPTIONS 41 | 42 | -v 43 | .RS 4 44 | Show verbose output. 45 | .RE 46 | 47 | -p 48 | .RS 4 49 | Instead of prompting for a password, read it from the specified file. 50 | .RE 51 | 52 | -f 53 | .RS 4 54 | Don't ask for confirmation before writing to disk. (WARNING: Use at your own risk!) 55 | .RE 56 | 57 | <\fIdevice\fP> 58 | .RS 4 59 | A block device (i.e, disk, partition, or loop device). 60 | .RE 61 | 62 | <\fIcipherdevice\fP> 63 | .RS 4 64 | A block device (i.e, disk, partition, or loop device) encrypted, or to be encrypted, with PUREE. 65 | .RE 66 | 67 | <\fIplaindevice\fP> 68 | .RS 4 69 | A virtual block device (i.e., device-mapper mapping), or target path to create a virtual block device. 70 | .RE 71 | 72 | <\fIsubspec\fP> 73 | .RS 4 74 | Use \fIsubspec\fP as the a sub-specification (i.e., the disk format and cryptosystem to use). See section \fBSUB-SPECIFICATIONS\fP for more details. 75 | .RE 76 | 77 | .SH COMMANDS 78 | .PP 79 | 80 | \fBpuree format\fP [\fB-p\fP <\fIpassword_file\fP>] [-f] [-v] <\fIcipherdevice\fP> <\fIsubspec\fP> 81 | 82 | .RS 4 83 | Formats the block device \fIcipherdevice\fP with the PUREE disk encryption format using sub-specification \fIsubspec\fP. See section \fBSUB-SPECIFICATIONS\fP for more details. 84 | 85 | .RE 86 | 87 | \fBpuree info\fP [\fB-p\fP <\fIpassword_file\fP>] [-f] [-v] <\fIcipherdevice\fP> 88 | 89 | .RS 4 90 | Shows detailed PUREE header information for block device \fIcipherdevice\fP; the device must have already been formatted with the PUREE disk encryption format. 91 | .RE 92 | 93 | \fBpuree map\fP [\fB-p\fP <\fIpassword_file\fP>] [-f] [-v] <\fIcipherdevice\fP> <\fIplaindevice\fP> 94 | 95 | .RS 4 96 | Creates a new virtual block device, \fIplaindevice\fP, for which all writes (or reads) will be transparently encrypted (or decrypted) then written to (or read from) \fIcipherdevice\fP. The \fIcipherdevice\fP must have already been formatted with \fBpuree format\fP, and a password must be provided to unlock it when it is mapped. Internally, this command leverages the dm-crypt device-mapper target using whatever parameters were used when the \fIcipherdevice\fP was formatted. 97 | .RE 98 | 99 | \fBpuree unmap\fP [-v] <\fIplaindevice\fP> 100 | 101 | .RS 4 102 | Unmaps the virtual block device \fIplaindevice\fP, thereby making the device's plaintext data unavailable until it is mapped again. 103 | .RE 104 | 105 | \fBpuree destroy\fP [-f] [-v] [-q] <\fIdevice\fP> 106 | 107 | .RS 4 108 | Destroys all data on the specified \fBdevice\fP by writing random data. \fBWARNING: This will wipe ALL data on the disk\fP. If \fB-q\fP is supplied, then only the first 1MiB of data and the last 1MiB of data on the disk will destroyed (which, for a cipherdevice previously encrypted with PUREE, will effectively wipe the entire disk). 109 | 110 | .SH EXAMPLES 111 | .PP 112 | 113 | To format, map, and mount a device, you may perform the series of commands listed below, in order. 114 | 115 | To encrypt device \fB/dev/sdz\fP with \fBAES-256-CBC-ESSIV-SHA256\fP: 116 | 117 | .RS 4 118 | sudo puree format /dev/sdz aes256-cbc-essiv-sha256 119 | .RE 120 | 121 | (You will be prompted for a password. PUREE will require you to prefix your password with a "parameter character"; see "PARAMETER CHARACTERS" section below for an explanation.) 122 | 123 | Your device should now be encrypted. 124 | 125 | Next, you'll need to "map" your encrypted disk device (we'll assume \fB/dev/sdz\fP) to a virtual device : 126 | 127 | .RS 4 128 | sudo puree map /dev/sdz /dev/mapper/sdz 129 | .RE 130 | 131 | (You will be prompted to re-enter the password you chose earlier.) 132 | 133 | Your virtual device should now be available at \fB/dev/mapper/sdz\fP. You can now treat \fB/dev/mapper/sdz\fP as you would a normal disk device, and its data will be transparently encrypted/decrypted. 134 | 135 | For example, to format it with a filesystem: 136 | 137 | .RS 4 138 | sudo mkfs.ext4 /dev/mapper/sdz 139 | .br 140 | sudo mount /dev/mapper/sdz /mnt 141 | .RE 142 | 143 | You now have an filesystem mounted to \fB/mnt\fP. You can treat it like a normal filesystem, and its data will be transparently encrypted/decrypted. 144 | 145 | When you're done using the device, unmap it with: 146 | 147 | .RS 4 148 | sudo puree unmap /dev/mapper/sdz 149 | .RE 150 | 151 | .PP 152 | To prove to yourself that the disk is encrypted, try running `sudo hexdump -C /dev/sdz | less`, and you'll see something like this: 153 | 154 | .nf 155 | .eo 156 | 00000000 3ac41e42 da074126 fb9d4c6a 01a15f56 |...B..A&..Lj.._V| 157 | 00000010 c71c6c47 3a891a07 77af909a 4efb1a8f |..lG:...w...N...| 158 | 00000020 72fc3eac 1766db1d 55d2c0cd 14a666bd |r.>..f..U.....f.| 159 | 00000030 5592d610 bbc3ad81 46eb2bf7 cec566b6 |U.......F.+...f.| 160 | 00000040 8c44df17 8868323d d175458d 4327d107 |.D...h2=.uE.C'..| 161 | 00000050 6dbf3af8 11083156 dd3bb235 83826b62 |m.:...1V.;.5..kb| 162 | 00000060 fad3a02d 48acebc5 7b79ce68 ec9e68f1 |...-H...{y.h..h.| 163 | 00000070 4c5daf93 1a2bb71f ace7f417 ca627d05 |L]...+.......b}.| 164 | 00000080 39568ce6 5ec12f58 38c056d3 d682d728 |9V..^./X8.V....(| 165 | 00000090 446df278 d823fee0 ff2f4c04 434b5f5e |Dm.x.#.../L.CK_^| 166 | 000000a0 bc425830 55c455cd b4439385 c59bf3fd |.BX0U.U..C......| 167 | 000000b0 62019305 a5f38ce9 12c0c138 76f31f1b |b..........8v...| 168 | 000000c0 8e67545a e3abf95a 2247fc0c 5c55558c |.gTZ...Z"G....U.| 169 | 000000d0 01c62344 8fbb35df 80b313da 63269760 |..#D..5.....c&.`| 170 | 000000e0 4dfbd88d d32a1179 e4038d7c 3c4412eb |M....*.y...| parallelism: 1, memory: 75MiB, iterations: 1 194 | \[char39]c' => parallelism: 1, memory: 250MiB, iterations: 1 195 | \[char39]d' => parallelism: 4, memory: 250MiB, iterations: 4 196 | \[char39]e' => parallelism: 1, memory: 1GiB, iterations: 1 197 | \[char39]f' => parallelism: 4, memory: 1GiB, iterations: 4 198 | \[char39]g' => parallelism: 1, memory: 4GiB, iterations: 1 199 | \[char39]h' => parallelism: 4, memory: 4GiB, iterations: 4 200 | \[char39]i' => parallelism: 1, memory: 16GiB, iterations: 1 201 | \[char39]j' => parallelism: 4, memory: 16GiB, iterations: 4 202 | \[char46].. 203 | .fi 204 | .RE 205 | 206 | Or, if 'a' is chosen as the parameter character, the password will be derived simply by hashing the password (along with a salt) using the \fBblake2b\fP hash function. 207 | 208 | As CPU and RAM become cheaper, more parameter characters will be added to this table. 209 | 210 | .SH SUB-SPECIFICATIONS 211 | 212 | Currently, the following subspecs are supported: 213 | 214 | - aes256-xts-plain64 215 | 216 | .RS 4 217 | Encrypt each sector of the disk with AES-256 in XTS mode. 218 | .RE 219 | 220 | - aes256-cbc-essiv-sha256 221 | 222 | .RS 4 223 | Encrypt each sector of the disk with AES-256 in CBC-ESSIV mode, using SHA-256 as the hash function. 224 | .RE 225 | 226 | - aes128-xts-plain64 227 | 228 | .RS 4 229 | Encrypt each sector of the disk with AES-128 in XTS mode. 230 | .RE 231 | 232 | - aes128-cbc-essiv-sha256 233 | 234 | .RS 4 235 | Encrypt each sector of the disk with AES-128 in CBC-ESSIV mode, using SHA-256 as the hash function. 236 | .RE 237 | 238 | .SH EXIT CODE 239 | .PP 240 | If \fBpuree\fP was successful, it will exit with code 0. 241 | .br 242 | If \fBpuree\fP encounters an error, it will exit with code 1. 243 | .br 244 | If invalid arguments are passed to \fBpuree\fP, it will exit with code 2. 245 | 246 | .SH WEBSITE 247 | For more information, see . 248 | 249 | .SH AUTHOR 250 | Jay Sullivan 251 | 252 | -------------------------------------------------------------------------------- /docs/puree.1.html: -------------------------------------------------------------------------------- 1 | Manpage of puree 2 | 3 |

puree(1)

4 | 5 |

6 | 7 |   8 |

NAME

9 | 10 |

11 | 12 | puree - encrypt devices with PUREE (the full-disk encryption format) 13 |

14 |   15 |

SYNOPSIS

16 | 17 |

18 | 19 |

20 | puree info [-v] [-p] <password_file>] [-f] <cipherdevice> 21 |

22 | puree format [-v] [-p] <password_file>] [-f] <cipherdevice> <subspec> 23 |

24 | puree map [-v] [-p] <password_file>] [-f] <cipherdevice> <plaindevice> 25 |

26 | puree unmap [-v] <plaindevice> 27 |

28 | puree destroy [-v] [-f] [-q] <device> 29 |

30 |   31 |

DESCRIPTION

32 | 33 |

34 | 35 |

36 | The puree tool suite allows you to password-protect disk devices (using dm-crypt and the PUREE header format) in such a way that the entire disk is indistinguishable from random data. Notably this occurs without requiring you to store any associated data on a separate disk (in comparison, most disk encryption formats require the user to store a separate "detached header" somewhere to accomplish this). 37 |

38 | The full lifecycle of encrypting a disk with PUREE is as follows: 39 |

40 |

41 | 1) First format the disk with 'puree format' (keeping in mind this will destroy existing data from the disk). 42 |

43 | 2) If you'd like, you can now use the puree info command to verify that the disk is formatted correctly, and that you still have the correct password to the disk. 44 |

45 | 3) Next, map the disk to a virtual device with 'puree map'. This virtual device (which will be located under /dev/mapper/) can then be treated as if it were a new disk. Do whatever you'd like with it now, such as as format it with a filesystem, or mount it. 46 |

47 | 4) When you're done, use puree unmap to unmap the virtual device. After doing this, the disk "locked", and will not be unlocked until you call puree map again. 48 |

49 | 5) If you'd like to destroy all data on a disk, use puree destroy. 50 |

51 | 52 |

53 |   54 |

OPTIONS

55 | 56 |

57 | -v 58 |

59 | Show verbose output. 60 |
61 | 62 |

63 | -p <password_file> 64 |

65 | Instead of prompting for a password, read it from the specified file. 66 |
67 | 68 |

69 | -f 70 |

71 | Don't ask for confirmation before writing to disk. (WARNING: Use at your own risk!) 72 |
73 | 74 |

75 | <device> 76 |

77 | A block device (i.e, disk, partition, or loop device). 78 |
79 | 80 |

81 | <cipherdevice> 82 |

83 | A block device (i.e, disk, partition, or loop device) encrypted, or to be encrypted, with PUREE. 84 |
85 | 86 |

87 | <plaindevice> 88 |

89 | A virtual block device (i.e., device-mapper mapping), or target path to create a virtual block device. 90 |
91 | 92 |

93 | <subspec> 94 |

95 | Use subspec as the a sub-specification (i.e., the disk format and cryptosystem to use). See section SUB-SPECIFICATIONS for more details. 96 |
97 | 98 |

99 |   100 |

COMMANDS

101 | 102 |

103 | 104 |

105 | puree format [-p <password_file>] [-f] [-v] <cipherdevice> <subspec> 106 |

107 |

108 | Formats the block device cipherdevice with the PUREE disk encryption format using sub-specification subspec. See section SUB-SPECIFICATIONS for more details. 109 |

110 |

111 | 112 |

113 | puree info [-p <password_file>] [-f] [-v] <cipherdevice> 114 |

115 |

116 | Shows detailed PUREE header information for block device cipherdevice; the device must have already been formatted with the PUREE disk encryption format. 117 |
118 | 119 |

120 | puree map [-p <password_file>] [-f] [-v] <cipherdevice> <plaindevice> 121 |

122 |

123 | Creates a new virtual block device, plaindevice, for which all writes (or reads) will be transparently encrypted (or decrypted) then written to (or read from) cipherdevice. The cipherdevice must have already been formatted with puree format, and a password must be provided to unlock it when it is mapped. Internally, this command leverages the dm-crypt device-mapper target using whatever parameters were used when the cipherdevice was formatted. 124 |
125 | 126 |

127 | puree unmap [-v] <plaindevice> 128 |

129 |

130 | Unmaps the virtual block device plaindevice, thereby making the device's plaintext data unavailable until it is mapped again. 131 |
132 | 133 |

134 | puree destroy [-f] [-v] [-q] <device> 135 |

136 |

137 | Destroys all data on the specified device by writing random data. WARNING: This will wipe ALL data on the disk. If -q is supplied, then only the first 1MiB of data and the last 1MiB of data on the disk will destroyed (which, for a cipherdevice previously encrypted with PUREE, will effectively wipe the entire disk). 138 |

139 |

140 |   141 |

EXAMPLES

142 | 143 |

144 | 145 |

146 | To format, map, and mount a device, you may perform the series of commands listed below, in order. 147 |

148 | To encrypt device /dev/sdz with AES-256-CBC-ESSIV-SHA256: 149 |

150 |

151 | sudo puree format /dev/sdz aes256-cbc-essiv-sha256 152 |
153 | 154 |

155 | (You will be prompted for a password. PUREE will require you to prefix your password with a "parameter character"; see "PARAMETER CHARACTERS" section below for an explanation.) 156 |

157 | Your device should now be encrypted. 158 |

159 | Next, you'll need to "map" your encrypted disk device (we'll assume /dev/sdz) to a virtual device : 160 |

161 |

162 | sudo puree map /dev/sdz /dev/mapper/sdz 163 |
164 | 165 |

166 | (You will be prompted to re-enter the password you chose earlier.) 167 |

168 | Your virtual device should now be available at /dev/mapper/sdz. You can now treat /dev/mapper/sdz as you would a normal disk device, and its data will be transparently encrypted/decrypted. 169 |

170 | For example, to format it with a filesystem: 171 |

172 |

173 | sudo mkfs.ext4 /dev/mapper/sdz 174 |
175 | 176 | sudo mount /dev/mapper/sdz /mnt 177 |
178 | 179 |

180 | You now have an filesystem mounted to /mnt. You can treat it like a normal filesystem, and its data will be transparently encrypted/decrypted. 181 |

182 | When you're done using the device, unmap it with: 183 |

184 |

185 | sudo puree unmap /dev/mapper/sdz 186 |
187 | 188 |

189 |

190 | 191 | To prove to yourself that the disk is encrypted, try running `sudo hexdump -C /dev/sdz | less`, and you'll see something like this: 192 |

193 |

194 | 00000000  3ac41e42 da074126 fb9d4c6a 01a15f56  |...B..A&..Lj.._V|
195 | 00000010  c71c6c47 3a891a07 77af909a 4efb1a8f  |..lG:...w...N...|
196 | 00000020  72fc3eac 1766db1d 55d2c0cd 14a666bd  |r.>..f..U.....f.|
197 | 00000030  5592d610 bbc3ad81 46eb2bf7 cec566b6  |U.......F.+...f.|
198 | 00000040  8c44df17 8868323d d175458d 4327d107  |.D...h2=.uE.C'..|
199 | 00000050  6dbf3af8 11083156 dd3bb235 83826b62  |m.:...1V.;.5..kb|
200 | 00000060  fad3a02d 48acebc5 7b79ce68 ec9e68f1  |...-H...{y.h..h.|
201 | 00000070  4c5daf93 1a2bb71f ace7f417 ca627d05  |L]...+.......b}.|
202 | 00000080  39568ce6 5ec12f58 38c056d3 d682d728  |9V..^./X8.V....(|
203 | 00000090  446df278 d823fee0 ff2f4c04 434b5f5e  |Dm.x.#.../L.CK_^|
204 | 000000a0  bc425830 55c455cd b4439385 c59bf3fd  |.BX0U.U..C......|
205 | 000000b0  62019305 a5f38ce9 12c0c138 76f31f1b  |b..........8v...|
206 | 000000c0  8e67545a e3abf95a 2247fc0c 5c55558c  |.gTZ...Z"G....U.|
207 | 000000d0  01c62344 8fbb35df 80b313da 63269760  |..#D..5.....c&.`|
208 | 000000e0  4dfbd88d d32a1179 e4038d7c 3c4412eb  |M....*.y...|<D..|
209 | 000000f0  c856ecfe 15e5c4a5 d7f12165 628c05b8  |.V........!eb...|
210 | 00000100  6c00f7e2 dcb39dce dff67d1d e9551eaa  |l.........}..U..|
211 | 00000110  d9e24fd6 0f42b399 ed18adec 4de8912a  |..O..B......M..*|
212 | 00000120  2316e413 1712a0a7 044b96d3 154d1b2f  |#........K...M./|
213 | 00000130  67a62365 6f15d733 f4541fc7 8781bfd3  |g.#eo..3.T......|
214 | 
215 | 
216 | 217 |

218 |   219 |

PARAMETER CHARACTERS

220 | 221 |

222 | PUREE encrypts disks using a key derived from a password via the argon2id password-key derivation function. In order to calculate a derived key from a password, however, a few parameters are required: 223 |

224 | 1. Parallelism: the maximum number of parallel CPU threads 225 |

226 | 2. Memory: the amount of RAM required 227 |

228 | 3. Iterations: multiplier on amount of time required 229 |

230 | One goal of PUREE is that the disk must be indistinguishable from random. This means these parameters can not be stored on the disk. Instead, PUREE stores these parameters in the password. Every PUREE password must be prefixed with a special character, called the "parameter character". Current valid values are: 231 |

232 |

233 |
234 | 'b' => parallelism: 1,  memory: 75MiB,  iterations: 1
235 | 'c' => parallelism: 1,  memory: 250MiB, iterations: 1
236 | 'd' => parallelism: 4,  memory: 250MiB, iterations: 4
237 | 'e' => parallelism: 1,  memory: 1GiB,   iterations: 1
238 | 'f' => parallelism: 4,  memory: 1GiB,   iterations: 4
239 | 'g' => parallelism: 1,  memory: 4GiB,   iterations: 1
240 | 'h' => parallelism: 4,  memory: 4GiB,   iterations: 4
241 | 'i' => parallelism: 1,  memory: 16GiB,  iterations: 1
242 | 'j' => parallelism: 4,  memory: 16GiB,  iterations: 4
243 | ...
244 | 
245 | 246 |

247 | Or, if 'a' is chosen as the parameter character, the password will be derived simply by hashing the password (along with a salt) using the blake2b hash function. 248 |

249 | 250 |

251 | As CPU and RAM become cheaper, more parameter characters will be added to this table. 252 |

253 |   254 |

SUB-SPECIFICATIONS

255 | 256 |

257 | Currently, the following subspecs are supported: 258 |

259 | - aes256-xts-plain64 260 |

261 |

262 | Encrypt each sector of the disk with AES-256 in XTS mode. 263 |
264 | 265 |

266 | - aes256-cbc-essiv-sha256 267 |

268 |

269 | Encrypt each sector of the disk with AES-256 in CBC-ESSIV mode, using SHA-256 as the hash function. 270 |
271 | 272 |

273 | - aes128-xts-plain64 274 |

275 |

276 | Encrypt each sector of the disk with AES-128 in XTS mode. 277 |
278 | 279 |

280 | - aes128-cbc-essiv-sha256 281 |

282 |

283 | Encrypt each sector of the disk with AES-128 in CBC-ESSIV mode, using SHA-256 as the hash function. 284 |
285 | 286 |

287 |   288 |

EXIT CODE

289 | 290 |

291 | 292 | If puree was successful, it will exit with code 0. 293 |
294 | 295 | If puree encounters an error, it will exit with code 1. 296 |
297 | 298 | If invalid arguments are passed to puree, it will exit with code 2. 299 |

300 |   301 |

WEBSITE

302 | 303 | For more information, see <https://puree.cc>. 304 |

305 |   306 |

AUTHOR

307 | 308 | Jay Sullivan <jay@identity.pub> 309 |

310 |

311 | 312 | 313 | --------------------------------------------------------------------------------