├── tests ├── __init__.py ├── test_compat.py ├── test_start.py ├── test_decorators.py ├── test_padding.py ├── test_jak.py ├── test_helpers.py ├── test_diff.py └── test_crypto.py ├── docs ├── _static │ ├── jak_crypto_description.jpg │ └── videos │ │ ├── diffmerge_short.json │ │ └── nosetup.json ├── Makefile ├── make.bat ├── supported-platforms.rst ├── index.rst ├── guide │ ├── contributor.rst │ ├── advanced.rst │ ├── commands.rst │ └── usage.rst ├── security.rst ├── changelog.rst └── conf.py ├── requirements_dev.txt ├── .travis.yml ├── jak ├── exceptions.py ├── compat.py ├── padding.py ├── __init__.py ├── start.py ├── decorators.py ├── outputs.py ├── crypto_services.py ├── aes_cipher.py ├── diff.py ├── helpers.py └── app.py ├── ship.sh ├── tox.ini ├── .gitignore ├── Vagrantfile ├── setup.py ├── README.md ├── LICENSE └── LICENSE.txt /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/jak_crypto_description.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dispel/jak/HEAD/docs/_static/jak_crypto_description.jpg -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | tox==2.5.0 2 | flake8==3.2.0 3 | mock==2.0.0 4 | pytest 5 | pytest-cov 6 | 7 | # Documentation generation 8 | sphinx 9 | sphinx_rtd_theme 10 | -------------------------------------------------------------------------------- /tests/test_compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from jak.compat import b 4 | 5 | 6 | def test_b(): 7 | assert b('a') == b'a' 8 | assert b(b'a') == b'a' 9 | assert b(u'a') == b'a' 10 | assert b(chr(222)) == b'\xde' 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | # - "pypy" 9 | 10 | install: 11 | - pip install . 12 | - pip install -r requirements_dev.txt 13 | 14 | script: py.test 15 | -------------------------------------------------------------------------------- /jak/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2018 Dispel, LLC 3 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 4 | """ 5 | 6 | 7 | class JakException(Exception): 8 | """Something obvious went wrong.""" 9 | 10 | 11 | class WrongKeyException(Exception): 12 | """The wrong key was used when trying to decrypt""" 13 | -------------------------------------------------------------------------------- /ship.sh: -------------------------------------------------------------------------------- 1 | # tags: deploy, ship, pypi 2 | 3 | # If this is your first time or it's just been a while: 4 | # https://packaging.python.org/guides/using-testpypi/ 5 | # https://packaging.python.org/tutorials/distributing-packages/#uploading-your-project-to-pypi 6 | 7 | # rm -rf dist/ 8 | # python 2 9 | # python setup.py bdist_wheel 10 | # switch to python 3 and run it again 11 | # python setup.py bdist_wheel 12 | 13 | # Test 14 | twine upload -r testpypi --config-file .pypirc dist/* 15 | 16 | # Prod 17 | twine upload --config-file .pypirc dist/* 18 | -------------------------------------------------------------------------------- /jak/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Copyright 2018 Dispel, LLC 5 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 6 | """ 7 | 8 | import six 9 | 10 | if six.PY3: 11 | import codecs 12 | 13 | def b(x): 14 | if isinstance(x, six.binary_type): 15 | return x 16 | else: 17 | return codecs.latin_1_encode(x)[0] 18 | else: 19 | def b(x): 20 | if isinstance(x, six.binary_type): 21 | return x 22 | else: 23 | return bytes(x) 24 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = jak 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /jak/padding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2018 Dispel, LLC 4 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 5 | """ 6 | 7 | import six 8 | 9 | 10 | def pad(data, bs=16): 11 | """PKCS#7 Padding. Takes a bytestring data and an optional blocksize 'bs'""" 12 | length = bs - (len(data) % bs) 13 | data += six.int2byte(length) * length 14 | return data 15 | 16 | 17 | def unpad(data): 18 | """remove PKCS#7 padding by removing as many digits as 19 | the padding indicates are padding. 20 | 21 | :data: is a bytestring. 22 | Returns the unpadded bytestring. 23 | """ 24 | 25 | # Python 3 raises the TypeError 26 | try: 27 | return data[:-ord(data[-1])] 28 | except TypeError: 29 | return data[:-data[-1]] 30 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # https://github.com/pyca/pyopenssl/blob/master/tox.ini 2 | # I wonder if we can use this to get tox working better across all the python versions. 3 | 4 | [tox] 5 | # Locally we run with just 2 for speed. 6 | # However, any kind of CI SHOULD run with all of these environments 7 | # Barring that please run with all environments before committing. 8 | envlist= 9 | py27 10 | py34 11 | py35 12 | py36 13 | pypy 14 | # pypy3 (todo) 15 | # jython (todo) 16 | # flake8 17 | 18 | [testenv] 19 | usedevelop=true 20 | commands=py.test --cov jak {posargs} 21 | deps= 22 | lowest: click==6.6 23 | lowest: pycrypto==2.6.1 24 | lowest: six==1.10.0 25 | 26 | -rrequirements_dev.txt 27 | 28 | [testenv:flake8] 29 | basepython = python2.7 30 | deps= 31 | -rrequirements.txt 32 | -rrequirements_dev.txt 33 | commands=flake8 jak tests --max-line-length=110 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=jak 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /jak/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Jak 3 | --- 4 | Jak is a tool for making it dead simple to encrypt and decrypt files. 5 | 6 | About versioning 7 | ---------------- 8 | Past, current and future versions. 9 | 10 | 0.X Troubled Toddler <-- CURRENT 11 | 1.X Young Whippersnapper 12 | 2.X Teenage Wasteland 13 | 3.X Highschool Sweetheart 14 | 4.X Wannabee Scientist 15 | 5.X Jaded Hipster 16 | 6.X Midlife Maniac 17 | 7.X Dorky Parent 18 | 8.X Rattled Retiree 19 | 9.X Cranky Old Seafarer 20 | 10.X Wizened Witch 21 | 22 | If in doubt about how version should increase see: http://semver.org/ 23 | The exception is version 1.X which is when we are saying that we are comfortable 24 | with people using it. I Don't care what incompatible API changes happen during 0.X 25 | it is NOT 1.X until we are 99.9 percent sure it is secure. 26 | 27 | Semantic Versioning Cheatsheet 28 | ------------------------------ 29 | version 1.2.3 means MAJOR = 1, MINOR = 2, PATCH = 3. 30 | MAJOR version when you make incompatible API changes, 31 | MINOR version when you add functionality in a backwards-compatible manner, and 32 | PATCH version when you make backwards-compatible bug fixes. 33 | 34 | Copyright 2018 Dispel, LLC 35 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 36 | """ 37 | 38 | __version__ = '0.14.6' 39 | __version_full__ = "Jak v{0} ({1})".format(__version__, 'Troubled Toddler') 40 | -------------------------------------------------------------------------------- /docs/supported-platforms.rst: -------------------------------------------------------------------------------- 1 | .. _support_detailed: 2 | 3 | 4 | Supported platforms 5 | =================== 6 | 7 | Python 8 | ------ 9 | 10 | jak is explicitly tested on Pythons: 11 | 12 | - 2.7 (It is probably safe to assume jak works for 2.7.7 - 2.7.X, where X > 7) 13 | - 3.4 14 | - 3.5 15 | - 3.6 16 | 17 | Works but CI fails: 18 | 19 | - `PyPy `_. (It works just fine locally but travis seems to have trouble with it, so we removed it from CI for now. The issue is that pycrypto fails to install under it, and pycrypto seems to explicitly state that they dont work with pypy. Nonetheless, we've gotten this working on our machines just fine...) 20 | 21 | Planned but not tested yet, but hopefully work: 22 | 23 | - PyPy3 24 | 25 | jak follows the `Python end of support dates `_, which in practice means that support ends on the following dates: 26 | 27 | - 3.4 (PEP 429) support ends 2019-03-16 28 | - 2.7 (PEP 373) support ends 2020-01-01 29 | - 3.5 (PEP 478) support ends 2020-09-13 30 | - 3.6 (PEP 494) support ends 2021-12-23 31 | 32 | For all you Python 2.7 lunatics out there that means when `this clock reaches zero `_ we drop 2.7 in the name of `courage `_, progress and maintaining a clean codebase. It is my understanding that dropping 2.7 may implicitly mean dropping PyPy as well, which may sway this decision, since jak is a sucker for scrappy whippersnappers. 33 | 34 | It is however likely that even without explicitly testing for it the 3.X versions will continue to work just fine even after we officially stop supporting them. 35 | 36 | 37 | OS 38 | -- 39 | 40 | We believe jak should work well on most `*nix `_ systems. But is mainly developed on Ubuntu and tested on Ubuntu and macOS. 41 | -------------------------------------------------------------------------------- /tests/test_start.py: -------------------------------------------------------------------------------- 1 | from jak import start 2 | import os 3 | 4 | 5 | def test_add_pre_commit_encrypt_hook(tmpdir): 6 | repo_hooks = tmpdir.mkdir('.git').mkdir('hooks') 7 | repo_hooks = repo_hooks.strpath 8 | start.add_pre_commit_encrypt_hook(repo_hooks[:repo_hooks.rfind('.git')]) 9 | assert os.path.exists(repo_hooks + '/pre-commit') 10 | assert os.path.exists(repo_hooks + '/jak.pre-commit.py') 11 | 12 | 13 | def test_pre_existing_pre_commit_hook(tmpdir): 14 | repo_hooks = tmpdir.mkdir('.git').mkdir('hooks').join('pre-commit') 15 | repo_hooks.write('PRE-COMMIT HOOK') 16 | repo_hooks = repo_hooks.strpath 17 | result = start.add_pre_commit_encrypt_hook(repo_hooks[:repo_hooks.rfind('.git')]) 18 | assert os.path.exists(repo_hooks[:repo_hooks.rfind('/pre-commit')] + '/pre-commit') 19 | assert 'EXISTING PRE-COMMIT HOOK' in result 20 | assert os.path.exists(repo_hooks[:repo_hooks.rfind('/pre-commit')] + '/jak.pre-commit.py') 21 | 22 | 23 | def test_add_keyfile_to_gitignore(tmpdir): 24 | gitignore = tmpdir.join('.gitignore') 25 | gitignore.write('# Simple Git Ignore') 26 | start.add_keyfile_to_gitignore(gitignore.strpath) 27 | with open(gitignore.strpath, 'r') as f: 28 | new_gitignore = f.read() 29 | assert '.jak' in new_gitignore 30 | 31 | 32 | def test_create_jakfile_error(tmpdir): 33 | jakfile = tmpdir.join("jakfile") 34 | jakfile.write('gobbledigook') 35 | result = start.create_jakfile(jakfile.dirpath().strpath + '/') 36 | assert 'Doing nothing, but feeling good' in result 37 | 38 | 39 | def test_create_jakfile(tmpdir): 40 | jakfile = tmpdir.join("jakfile") 41 | 42 | # I still want it to go in the tmpdir and not affect the actual location 43 | # without the jakfile.write it should not exist there. 44 | result = start.create_jakfile(jakfile.strpath) 45 | assert "Creating" in result 46 | assert '/jakfile' in result 47 | assert 'Done' in result 48 | 49 | # TODO 50 | # Make sure the files actually showed up and have content. 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/macos,linux,python,virtualenv,vagrant 2 | 3 | ### macOS ### 4 | *.DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | # Thumbnails 11 | ._* 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | # Directories potentially created on remote AFP share 21 | .AppleDB 22 | .AppleDesktop 23 | Network Trash Folder 24 | Temporary Items 25 | .apdisk 26 | 27 | 28 | ### Linux ### 29 | *~ 30 | 31 | # temporary files which can be created if a process still has a handle open of a deleted file 32 | .fuse_hidden* 33 | 34 | # KDE directory preferences 35 | .directory 36 | 37 | # Linux trash folder which might appear on any partition or disk 38 | .Trash-* 39 | 40 | # .nfs files are created when an open file is removed but is still being accessed 41 | .nfs* 42 | 43 | 44 | ### Python ### 45 | # Byte-compiled / optimized / DLL files 46 | __pycache__/ 47 | *.py[cod] 48 | *$py.class 49 | 50 | # C extensions 51 | *.so 52 | 53 | # Distribution / packaging 54 | .Python 55 | env/ 56 | build/ 57 | develop-eggs/ 58 | dist/ 59 | downloads/ 60 | eggs/ 61 | .eggs/ 62 | lib/ 63 | lib64/ 64 | parts/ 65 | sdist/ 66 | var/ 67 | *.egg-info/ 68 | .installed.cfg 69 | *.egg 70 | setup.cfg 71 | .pypirc 72 | 73 | # PyInstaller 74 | # Usually these files are written by a python script from a template 75 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 76 | *.manifest 77 | *.spec 78 | 79 | # Installer logs 80 | pip-log.txt 81 | pip-delete-this-directory.txt 82 | 83 | # Unit test / coverage reports 84 | htmlcov/ 85 | .tox/ 86 | .coverage 87 | .coverage.* 88 | .cache 89 | nosetests.xml 90 | coverage.xml 91 | *,cover 92 | .hypothesis/ 93 | 94 | # Translations 95 | *.mo 96 | *.pot 97 | 98 | # Sphinx documentation 99 | docs/_build/ 100 | 101 | # pyenv 102 | .python-version 103 | 104 | # virtualenv 105 | .venv/ 106 | venv/ 107 | ENV/ 108 | 109 | ### Vagrant ### 110 | .vagrant/ 111 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | $provisionScript = < 60 | 61 | 62 | Stewardship 63 | ----------- 64 | 65 | `Dispel `_ is the main steward of jaks development. But all contributions are encouraged and welcome. Please read the :ref:`contribution guide ` for more information on contributing. 66 | 67 | 68 | Table of contents 69 | ----------------- 70 | 71 | .. toctree:: 72 | :maxdepth: 1 73 | 74 | guide/usage 75 | guide/advanced 76 | guide/commands 77 | guide/contributor 78 | security 79 | supported-platforms 80 | changelog 81 | 82 | 83 | .. _support_short: 84 | 85 | Supported platforms 86 | ------------------- 87 | 88 | jak works if you have a modern Python (2.7-3.6) installed on a `*nix `_ system. 89 | 90 | :ref:`You can read about it in excrutiating detail here. ` 91 | 92 | 93 | Proposed future features and enhancements 94 | ----------------------------------------- 95 | 96 | - Make maintaining encrypted state of files optional 97 | - Avoid polluting filesystems with .jak folders? 98 | - Windows support 99 | - Easier key rotation 100 | 101 | 102 | License 103 | ------- 104 | Copyright 2016-2017 Dispel, LLC and contributors 105 | 106 | 107 | Licensed under the Apache License, Version 2.0 (the "License"); 108 | you may not use this file except in compliance with the License. 109 | You may obtain a copy of the License at 110 | 111 | http://www.apache.org/licenses/LICENSE-2.0 112 | 113 | Unless required by applicable law or agreed to in writing, software 114 | distributed under the License is distributed on an "AS IS" BASIS, 115 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 116 | See the License for the specific language governing permissions and 117 | limitations under the License. 118 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | import six 5 | from jak import helpers 6 | 7 | jakfile_content_1 = """ 8 | // Comment 1 9 | { 10 | // Comment 2 11 | "password_file": "jakpassword", 12 | // Comment 3 13 | "files_to_encrypt": [ "env", "env2" ] // Inline-Comment 4 14 | // "commented out line": 5 15 | } // Comment 5 (seriously?) 16 | // Comment 6 17 | // Comment 7 18 | """ 19 | 20 | 21 | def test_remove_comments_from_JSON(): 22 | result = helpers._remove_comments_from_JSON(jakfile_content_1) 23 | assert result == '{"password_file":"jakpassword","files_to_encrypt":["env","env2"]}' 24 | 25 | 26 | def test_read_jakfile_to_dict(tmpdir): 27 | jakfile = tmpdir.join("jakfile") 28 | jakfile.write(jakfile_content_1) 29 | assert jakfile.read() == jakfile_content_1 30 | 31 | result = helpers.read_jakfile_to_dict(jwd=jakfile.dirpath().strpath) 32 | 33 | assert isinstance(result, dict) 34 | assert 'files_to_encrypt' in result 35 | assert 'password_file' in result 36 | 37 | 38 | def test_grouper(): 39 | assert helpers.grouper('aaa', 1) == ('a', 'a', 'a') 40 | assert helpers.grouper('aaa', 5) == ('aaa', ) 41 | assert helpers.grouper('aaabbbcc', 3) == ('aaa', 'bbb', 'cc') 42 | 43 | # Raise error due to 2 not being iterable 44 | with pytest.raises(TypeError): 45 | helpers.grouper(2, 1) 46 | 47 | 48 | def test_generate_256bit_key(): 49 | key = helpers.generate_256bit_key() 50 | assert len(key) == 64 51 | assert isinstance(key, six.binary_type) 52 | 53 | 54 | def test_get_jak_working_directory(tmpdir): 55 | ''' 56 | /repo/.git/gitfile 57 | /repo/sub1/sub2/nestedfile 58 | ''' 59 | # No parent .git 60 | norepo = tmpdir.mkdir('norepo') 61 | result = helpers.get_jak_working_directory(cwd=norepo.strpath) 62 | assert result == norepo.strpath 63 | 64 | # Current has .git 65 | repo = tmpdir.mkdir('repo') 66 | gitfile = repo.mkdir('.git').join('gitfile') 67 | gitfile.write('this is a git repo') 68 | result = helpers.get_jak_working_directory(cwd=repo.strpath) 69 | assert result == repo.strpath 70 | 71 | # Parent has a .git 72 | nested = repo.mkdir('sub1').mkdir('sub2') 73 | # nested.write('I am a nested file') 74 | result = helpers.get_jak_working_directory(cwd=nested.strpath) 75 | assert '/repo' in result 76 | assert result.count('/') > 3 77 | 78 | 79 | def test_does_jwd_have_gitignore(tmpdir): 80 | repo = tmpdir.mkdir("repo_folder") 81 | git_ignore = repo.join(".gitignore") 82 | git_ignore.write("i exist") 83 | 84 | # this will pass because the .gitignore is in the CWD 85 | assert helpers.does_jwd_have_gitignore(cwd=repo.strpath) 86 | 87 | subdir = repo.mkdir('sub') 88 | # This will fail because there is no .git folder in any parent 89 | # and the CWD does not have a .gitignore 90 | assert not helpers.does_jwd_have_gitignore(cwd=subdir.strpath) 91 | 92 | repo.mkdir('.git') 93 | # This will be true because the parent now has .git and .gitignore 94 | assert helpers.does_jwd_have_gitignore(cwd=subdir.strpath) 95 | 96 | 97 | def test_create_backup_filepath(): 98 | output = helpers.create_backup_filepath(jwd='/a/b/c', filepath='/a/b/c/d/e.txt') 99 | assert output == '/a/b/c/.jak/d_e.txt_backup' 100 | 101 | # Special case, root. 102 | output = helpers.create_backup_filepath(jwd='/', filepath='/a') 103 | assert output == '/.jak/a_backup' 104 | 105 | output = helpers.create_backup_filepath(jwd='/a/b', filepath='/a/b/c') 106 | assert output == '/a/b/.jak/c_backup' 107 | 108 | output = helpers.create_backup_filepath(jwd='/a/b', filepath='/a/b/c/d/e') 109 | assert output == '/a/b/.jak/c_d_e_backup' 110 | -------------------------------------------------------------------------------- /tests/test_diff.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import jak.diff as difflib 4 | from jak.exceptions import JakException 5 | import pytest 6 | 7 | example_diff = ''' 8 | - - - Encrypted by jak - - - 9 | 10 | <<<<<<< HEAD 11 | SkFLLTAwMNdSsVOpbVZxcDCXjhXm-aGQCVwRHVjj-qYvBF3xFjKK7nI805NJ 12 | XiKXTmyWTH71FWA3Qt8aKQ8REOJQXxZdhT9djYmp-b4lFuWn3Qyp8zaV1nfE 13 | lzQwwLoSzJyKPPVYTg== 14 | ======= 15 | SkFLLTAwMMsRkZLtneHqxqqm_WX4uRjBKsPPkNeGmrv8cxJfLu71A9haYELd 16 | rLilAPevGzppR50xr1K0bn4Z88XWNp_cnU50GfD8Hy1jdiX4Wy53QJZlUPbt 17 | PL2gvlgTqLxOzupXgA== 18 | >>>>>>> f8eb651525b7403aa5ed93c251374ddef8796dee 19 | ''' 20 | 21 | 22 | def test_diff_decrypt(): 23 | local = 'SkFLLTAwMNdSsVOpbVZxcDCXjhXm-aGQCVwRHVjj-qYvBF3xFjKK7nI805NJXiKXTmyWTH71FWA3Qt8aKQ8REOJQXxZdhT9djYmp-b4lFuWn3Qyp8zaV1nfElzQwwLoSzJyKPPVYTg==' # noqa 24 | remote = 'SkFLLTAwMMsRkZLtneHqxqqm_WX4uRjBKsPPkNeGmrv8cxJfLu71A9haYELdrLilAPevGzppR50xr1K0bn4Z88XWNp_cnU50GfD8Hy1jdiX4Wy53QJZlUPbtPL2gvlgTqLxOzupXgA==' # noqa 25 | expected_local = 'SECRET' 26 | expected_remote = 'REMOTE_SECRET' 27 | 28 | (dlocal, dremote) = difflib._decrypt( 29 | key='2c596b43b406c47d67a620b890da19351c811b643698f9395ab6674cf9f6b7ca', 30 | local=local, 31 | remote=remote) 32 | 33 | assert dlocal == expected_local 34 | assert dremote == expected_remote 35 | 36 | 37 | def test_extract_merge_conflict_parts(): 38 | 39 | result = difflib._extract_merge_conflict_parts(content=example_diff) 40 | assert len(result) == 5 41 | assert result[0] == '<<<<<<< HEAD\n' 42 | expected = '''SkFLLTAwMNdSsVOpbVZxcDCXjhXm-aGQCVwRHVjj-qYvBF3xFjKK7nI805NJ 43 | XiKXTmyWTH71FWA3Qt8aKQ8REOJQXxZdhT9djYmp-b4lFuWn3Qyp8zaV1nfE 44 | lzQwwLoSzJyKPPVYTg== 45 | ''' 46 | assert result[1] == expected 47 | assert result[2] == '=======\n' 48 | expected = '''SkFLLTAwMMsRkZLtneHqxqqm_WX4uRjBKsPPkNeGmrv8cxJfLu71A9haYELd 49 | rLilAPevGzppR50xr1K0bn4Z88XWNp_cnU50GfD8Hy1jdiX4Wy53QJZlUPbt 50 | PL2gvlgTqLxOzupXgA== 51 | ''' 52 | assert result[3] == expected 53 | assert result[4] == '>>>>>>> f8eb651525b7403aa5ed93c251374ddef8796dee\n' 54 | 55 | 56 | @pytest.mark.parametrize('f,lf,rf', [ 57 | ('', '', ''), 58 | ('a', 'b', 'c'), 59 | ('env.yaml', 'env_LOCAL_1232342.yaml', 'env_REMOTE_1232342.yaml') 60 | ]) 61 | def test_smoke_vimdiff(f, lf, rf): 62 | expected = "vimdiff -f -d -c 'wincmd J' {} {} {}".format(f, lf, rf) 63 | assert expected in difflib._vimdiff(f, lf, rf) 64 | 65 | 66 | @pytest.mark.parametrize('filepath,name,local,remote', [ 67 | ('a', 'b', 'c', 'd'), 68 | ('', 'env.yaml', 'localcontent', 'remotecontent'), 69 | ('a/real/path', 'env.ext', u'localcontent', u'remotecontent') 70 | ]) 71 | def test_create_local_remote_diff_files(tmpdir, filepath, name, local, remote): 72 | # create a folder for them to put the files so we dont pollute. 73 | test_dir = tmpdir.mkdir('difftests') 74 | (local_result, remote_result) = difflib._create_local_remote_diff_files( 75 | test_dir.strpath + '/' + filepath + name, 76 | local, 77 | remote) 78 | assert filepath in remote_result and filepath in local_result 79 | 80 | with open(remote_result) as f: 81 | assert f.read() == remote 82 | 83 | with open(local_result) as f: 84 | assert f.read() == local 85 | 86 | 87 | def test_diff_decrypt_wrongkey(): 88 | local = 'SkFLLTAwMNdSsVOpbVZxcDCXjhXm-aGQCVwRHVjj-qYvBF3xFjKK7nI805NJXiKXTmyWTH71FWA3Qt8aKQ8REOJQXxZdhT9djYmp-b4lFuWn3Qyp8zaV1nfElzQwwLoSzJyKPPVYTg==' # noqa 89 | remote = 'SkFLLTAwMMsRkZLtneHqxqqm_WX4uRjBKsPPkNeGmrv8cxJfLu71A9haYELdrLilAPevGzppR50xr1K0bn4Z88XWNp_cnU50GfD8Hy1jdiX4Wy53QJZlUPbtPL2gvlgTqLxOzupXgA==' # noqa 90 | with pytest.raises(JakException): 91 | (dlocal, dremote) = difflib._decrypt( 92 | key='aaaaa1e1862c99f9211a01eebedb00ae1475a1e1862c99f9211aaaaaaaaaaaaa', 93 | local=local, 94 | remote=remote) 95 | -------------------------------------------------------------------------------- /docs/guide/contributor.rst: -------------------------------------------------------------------------------- 1 | .. _contributor: 2 | 3 | 4 | Contributor information 5 | ======================= 6 | 7 | Just like with jaks functionality we've worked hard to make developer installation consistent and easy. It should always be quick for people to contribute and the tests should help people know whether their 8 | changes work or not BEFORE they open a PR. 9 | 10 | We aim to be friendly to rookie devs, so if you are in doubt about proper operating procedure don't hesitate to reach out by creating an `issue `_, we are super friendly =). 11 | 12 | 13 | Developer machine setup 14 | ----------------------- 15 | 16 | 1. Clone this repo 17 | 2. Install the excellent [vagrant](https://www.vagrantup.com/) 18 | 3. See below. 19 | 20 | .. sourcecode:: shell 21 | 22 | # Boot up the vagrant machine 23 | # This will run for quite some time, I HIGHLY recommend looking at the Vagrantfile 24 | # for a description of what is happening. 25 | vagrant up 26 | 27 | # Enter sandman 28 | vagrant ssh 29 | 30 | # This is where the project files are mirrorer on the virtual machine 31 | cd /vagrant 32 | 33 | # Choose a virtualenv to work on (see virtualenvwrapper docs) 34 | # It is recommended you use the py27 environment for development and 35 | # then switching to py35 when you have an issue in Python 3. 36 | workon py27 37 | 38 | # Run tests for multiple Python versions (see tox.ini) 39 | tox 40 | 41 | # Or run tests in just the current environment 42 | pytest 43 | 44 | 45 | Notes of import 46 | --------------- 47 | 48 | To edit which environments the tests should be run as see the `tox.ini` file. 49 | We would prefer to be developing against Python 3 but the reality is that it is easier to dev against Python 2 and continually make sure it also works for 3. 50 | 51 | 52 | Updating documentation 53 | ---------------------- 54 | 55 | Documentation is important because it helps people understand jak. We welcome spelling, grammar, and generally any improvements to the documentation that help people understand jak and stay secure. 56 | 57 | jak uses `sphinx `_ to generate documentation. 58 | 59 | To update the docs edit the ``*.rst`` file that has the information you want to improve upon and then: 60 | 61 | .. sourcecode:: shell 62 | 63 | # from the root of jak, probably /vagrant 64 | cd docs 65 | 66 | # clean out previous version and remake the html 67 | rm -rf _build && make html 68 | 69 | Inspect the new docs by simply double clicking ``docs/_build/html/index.html`` to open it in your browser. 70 | 71 | 72 | Pull requests 73 | ------------- 74 | 75 | Once your branch or fork looks good make a PR and one of the stewards will take a look at it and give you a review (and hopefully merge it!). 76 | 77 | 78 | Alerts 79 | ------ 80 | 81 | It currently (2017-01-25) appears that tox will NOT run from Python 3.6 due to urrlib3 not existing. So run your tox from a different base Python environment for now. 82 | 83 | Also PyPy tests don't seem to run in tox either, I recommend checking out the virtualenv (``workon pypy``) and running them straight with ``pytest``. If someone could fix this, it would be much appreciated. 84 | 85 | 86 | Future versions 87 | --------------- 88 | 89 | 0 - 10 are the formative years. If we get past them (which seems frankly highly unlikely) we will start a new naming scheme. We use `semantic versioning `_ so the only time we shift the first number would be if we make backwards incompatible changes. The exception to this is 1.0 which will be assigned when Chris DiLorenzo thinks jak is (1) verified to be secure and (2) have no known bugs and (3) have decent tests for it's core functionality. 90 | 91 | .. sourcecode:: text 92 | 93 | 0.X Troubled Toddler <-- CURRENT 94 | 1.X Young Whippersnapper 95 | 2.X Teenage Wasteland 96 | 3.X Highschool Sweetheart 97 | 4.X Wannabee Scientist 98 | 5.X Jaded Hipster 99 | 6.X Midlife Maniac 100 | 7.X Dorky Parent 101 | 8.X Rattled Retiree 102 | 9.X Cranky Old Seafarer 103 | 10.X Wizened Witch 104 | -------------------------------------------------------------------------------- /jak/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Copyright 2018 Dispel, LLC 5 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 6 | """ 7 | 8 | import os 9 | from io import open 10 | from . import helpers 11 | from functools import wraps 12 | from .exceptions import JakException 13 | 14 | 15 | def _select_files_logic(**kwargs): 16 | if kwargs['all_or_filepath'] == 'all': 17 | try: 18 | filepaths = kwargs['jakfile_dict']['files_to_encrypt'] 19 | except KeyError: 20 | raise JakException("Expected key missing: 'files_to_encrypt' in jakfile.") 21 | else: 22 | filepaths = [kwargs['all_or_filepath']] 23 | 24 | files = [] 25 | for fp in filepaths: 26 | 27 | # Some OS expand this out automagically but in case one doesnt... 28 | if fp[0] == '~': 29 | fp = fp.replace('~', os.path.expanduser('~')) 30 | files.append(os.path.abspath(fp)) 31 | return files 32 | 33 | 34 | def select_files(f): 35 | """Select which files you want to act upon""" 36 | @wraps(f) 37 | def wrapper(*args, **kwargs): 38 | kwargs['files'] = _select_files_logic(**kwargs) 39 | return f(*args, **kwargs) 40 | return wrapper 41 | 42 | 43 | def attach_jwd(f): 44 | @wraps(f) 45 | def wrapper(*args, **kwargs): 46 | kwargs['jwd'] = helpers.get_jak_working_directory() 47 | return f(*args, **kwargs) 48 | return wrapper 49 | 50 | 51 | def read_jakfile(f): 52 | """Parse the jakfile and assign it to the jakfile_dict value""" 53 | @wraps(f) 54 | def wrapper(*args, **kwargs): 55 | try: 56 | kwargs['jakfile_dict'] = helpers.read_jakfile_to_dict() 57 | except IOError: 58 | kwargs['jakfile_dict'] = {} 59 | except ValueError as ve: 60 | raise JakException("Your jakfile has malformed syntax (probably).") 61 | return f(*args, **kwargs) 62 | return wrapper 63 | 64 | 65 | def select_key(f): 66 | """Let's find your key champ!""" 67 | @wraps(f) 68 | def wrapper(*args, **kwargs): 69 | kwargs['key'] = select_key_logic(key=kwargs['key'], 70 | keyfile=kwargs['keyfile'], 71 | jakfile_dict=kwargs['jakfile_dict']) 72 | result = f(*args, **kwargs) 73 | return result 74 | return wrapper 75 | 76 | 77 | def select_key_logic(key=None, keyfile=None, jakfile_dict=None): 78 | """Select a password or complain about passing too many. 79 | 80 | Pseudocode: 81 | REJECT IF NO KEYS 82 | IF CLI 83 | REJECT IF 2 KEYS FROM CLI 84 | PROCEED IF 1 KEY FROM CLI 85 | 86 | GET FROM KEYFILE 87 | 88 | Plaintext: CLI input keys override the jakfiles. Abort if 2 keys from CLI. 89 | """ 90 | 91 | # Take from them everything, give to them nothing! 92 | msg = '''Please provide a key in one of the three ways: 93 | 1. -k 94 | 2. -kf 95 | 3. "keyfile" value in your jakfile (recommended)''' 96 | try: 97 | if not key and not keyfile and 'keyfile' not in jakfile_dict: 98 | raise JakException(msg) 99 | except TypeError: 100 | raise JakException(msg) 101 | 102 | if key and keyfile: 103 | raise JakException('Please only pass me one key to avoid confusion. Aborting... ') 104 | 105 | if key: 106 | return key 107 | 108 | if keyfile: 109 | try: 110 | with open(keyfile, 'rt', encoding='utf-8') as f: 111 | key = f.read() 112 | except IOError: 113 | raise JakException("Sorry I can't find the key file: {}".format(keyfile)) 114 | else: 115 | key = key.replace('\n', '') 116 | return key 117 | 118 | # At this point they must have supplied a keyfile value in their jakfile 119 | filepath = jakfile_dict['keyfile'] 120 | try: 121 | with open(filepath, 'rt', encoding='utf-8') as f: 122 | key = f.read() 123 | except IOError: 124 | raise JakException("Sorry I can't find the key file: {}".format(filepath)) 125 | else: 126 | key = key.replace('\n', '') 127 | return key 128 | -------------------------------------------------------------------------------- /docs/guide/advanced.rst: -------------------------------------------------------------------------------- 1 | .. _advanced: 2 | 3 | Advanced usage 4 | ============== 5 | 6 | Here we answer questions not many people will have. 7 | 8 | 9 | Encryption & Security 10 | --------------------- 11 | 12 | If provided a jak generated key (complexity of the key is what determines the "number" for AES), jak will encrypt the files using AES256 which is secure. Seriously, I cannot stress this enough, using a poor password will result in poor encryption, don't do it! :ref:`Jak will generate the password for you if you ask nicely. ` 13 | 14 | 15 | 16 | .. _jak_folder_adv: 17 | 18 | What is in the hidden .jak folder? 19 | ---------------------------------- 20 | 21 | Basically, it holds the things jak doesn't feel like you should need to be looking at. 22 | 23 | One of the more important things jak recommends it holds is the :ref:`keyfile ` which holds the auto generated key that jak uses. 24 | 25 | It also holds the backups used to :ref:`maintain state ` of the encrypted files. 26 | 27 | 28 | 29 | .. _maintain_state: 30 | 31 | How does jak maintain state? 32 | ---------------------------- 33 | 34 | Or stated a different (more verbose) way: **How come the encrypted content of a file doesn't change unless the files content changes?** 35 | 36 | jak saves a copy of the encrypted files in the :ref:`.jak folder ` on decryption. On re-encryption it checks whether encrypting the contents with the backups IV creates the same encrypted content. If it is the same it simply reverts to using the previously encrypted content. This method has 2 main benefits: first, it is very simple. If given the option between a simple solution and an advanced one, you should pick the easy one. Two, nothing unencrypted is stored anywhere, the backup is of the encrypted content. The only issue (as described below) is that on encryption you end up performing 2 encryptions instead of 1 for content that has changed, which for very large files (hasn't been measured yet) may incur a time cost that is deemed unacceptable. 37 | 38 | The dev team has discussed switching to a slightly more "stupid" way of dealing with this, namely to save the modified time and simply compare that. However that would fail if the file was modified and then the changes were discarded or otherwise changed back. However that would be a constant time lookup, so it may be preferable if we find jak is used for very large files (where performing the encryption twice might become an issue). 39 | 40 | 41 | 42 | How does jak perform the diffing at a merge conflict? 43 | ----------------------------------------------------- 44 | 45 | Basically jak extracts the LOCAL and REMOTE parts of the merge conflict and decrypts them back into the same file. It then provides some options for a merge tool (or plain for decrypting and then leaving it alone) to merge with. 46 | 47 | Example conflict can looks something like this: Where the LOCAL is the top and the REMOTE is the bottom. 48 | 49 | .. sourcecode:: text 50 | 51 | <<<<<<< SOMETHING (usually HEAD) 52 | 56 | ======= 57 | 61 | >>>>>>> SOME OTHER HASH 62 | 63 | If you are a developer the `code for diffing is right here `_. 64 | 65 | :ref:`Here is information on how to perform a diff `. 66 | 67 | 68 | 69 | How does the pre-commit hook work? 70 | ---------------------------------- 71 | 72 | First and foremost, we recommend against trusting that the pre-commit hook will work, this is a failsafe and should be treated as such. 73 | 74 | The pre-commit hook embeds logic into the regular old ``.git/hooks/pre-commit`` hook that exists in all git repositories. 75 | It's functionality in pseudocode is roughly this: 76 | 77 | 1. Read the jakfile for the list of files that should be encrypted and retreive the key. 78 | 2. If it can't get a list of files or there isn't a key, do nothing. 79 | 3. If there is a list of files, compare it to the files that are currently staged. 80 | 4. If a file is in both the list and in staging, encrypt it. 81 | 5. Profit. 82 | 83 | To view the actual code you can see it in the `outputs.py `_ file. It is the ``PRE_COMMIT_ENCRYPT`` variable. 84 | -------------------------------------------------------------------------------- /docs/guide/commands.rst: -------------------------------------------------------------------------------- 1 | .. _commands: 2 | 3 | Command reference 4 | ================= 5 | 6 | Commands are given in the format ``jak ``. Some example commands: 7 | 8 | .. sourcecode:: shell 9 | 10 | jak --help 11 | jak start 12 | jak keygen 13 | jak keygen -m 14 | jak encrypt file 15 | jak encrypt file --key 64af685c12bf9f2245b851c528bdd6f41e351c8dbe614db4ea81d3486fc0ee5c 16 | jak decrypt file --keyfile secrets/jak/keyfile 17 | jak encrypt all 18 | jak stomp 19 | jak decrypt all 20 | jak shave 21 | jak diff 22 | 23 | 24 | 25 | --help 26 | ------ 27 | 28 | ``jak --help`` 29 | 30 | More information about jak or a jak command. 31 | 32 | ``jak == jak -h == jak --help`` 33 | 34 | ``jak -h == jak --help`` 35 | 36 | 37 | 38 | --version, -v 39 | ------------- 40 | 41 | ``jak -v`` 42 | 43 | Prints out the version. 44 | 45 | 46 | 47 | .. _start_cmd: 48 | 49 | start 50 | ----- 51 | 52 | ``jak start`` 53 | 54 | Initializes jak into your current working directory. 55 | 56 | **We highly recommend running this in the root of a git repository.** 57 | 58 | Specifically it will: 59 | 60 | - Add a hidden ``.jak`` directory 61 | - Add a :ref:`jakfile `. 62 | - Add a :ref:`keyfile ` (with a generated random 32 byte password in it) inside the ``.jak`` directory. 63 | - Check if it is being run in a in a git repository 64 | 65 | - IF GIT: it will ask if you want to add a pre-commit hook for auto encrypting files which are specified in the ``"file_to_encrypt"`` value in the :ref:`jakfile ` IF you should accidentally try to commit them. 66 | - IF GIT: It will add the ``.jak`` folder to the ``.gitignore``. 67 | 68 | It should give you very detailed output about what is happening. 69 | 70 | The start command is idempotent, so you can run it many times if you (for example) on second thought would like to add the git pre-commit hook. 71 | 72 | 73 | 74 | encrypt 75 | ------- 76 | 77 | ``jak encrypt `` or ``jak encrypt all``. 78 | 79 | The ``all`` command requires a :ref:`jakfile ` to exist, and will encrypt all files that are designated in the ``"files_to_encrypt"`` value. 80 | 81 | **optional arguments:** 82 | 83 | .. sourcecode:: text 84 | 85 | -k, --key 86 | jak encrypt -k 87 | 88 | -kf, --keyfile 89 | jak encrypt -kf 90 | 91 | 92 | 93 | decrypt 94 | ------- 95 | 96 | ``jak decrypt `` or ``jak decrypt all``. 97 | 98 | The ``all`` command requires a :ref:`jakfile ` to exist, and will decrypt all files that are designated in the ``"files_to_encrypt"`` value. 99 | 100 | **optional arguments:** 101 | 102 | .. sourcecode:: text 103 | 104 | -k, --key 105 | jak decrypt -k 106 | 107 | -kf, --keyfile 108 | jak decrypt -kf 109 | 110 | 111 | 112 | .. _keygen_cmd: 113 | 114 | keygen 115 | ------ 116 | 117 | Generate a 32byte key that jak will accept. Returns it to the command line. 118 | 119 | **optional arguments:** 120 | 121 | .. sourcecode:: text 122 | 123 | -m, --minimal 124 | Makes the command only return the key with no comments 125 | 126 | 127 | 128 | .. _diff_cmd: 129 | 130 | diff 131 | ---- 132 | 133 | ``jak diff `` 134 | 135 | This command will decrypt the LOCAL and REMOTE parts of a merge conflict. 136 | 137 | It will then prompt you for if you want to open the conflict in a merge tool 138 | such as vimdiff or opendiff (default on macOS) or if you simply want the decrypted content written back into the file 139 | so you can solve it yourself using your favorite text editor. 140 | 141 | **optional arguments:** 142 | 143 | .. sourcecode:: text 144 | 145 | -k, --key 146 | jak encrypt -k 147 | 148 | -kf, --keyfile 149 | jak encrypt -kf 150 | 151 | :ref:`Read more here. ` 152 | 153 | 154 | 155 | stomp 156 | ----- 157 | 158 | ``jak stomp`` 159 | 160 | Alias for ``jak encrypt all``. 161 | 162 | **Has the same options as the encrypt/decrypt commands.** 163 | 164 | 165 | 166 | shave 167 | ----- 168 | 169 | ``jak shave`` 170 | 171 | Alias for ``jak decrypt all``. 172 | 173 | **Has the same options as the encrypt/decrypt commands.** 174 | -------------------------------------------------------------------------------- /docs/security.rst: -------------------------------------------------------------------------------- 1 | .. _security: 2 | 3 | 4 | Security 5 | ======== 6 | 7 | 8 | jak aims to use well-tested computer security methods, including cryptography, to protect your information. What follows is a description of how jak uses cryptographic primitives to achieve this goal. Please report any security issues related to the architecture, design, or implementation you find as a `github issue `_ or via email to cdilorenzo@dispel.io 9 | 10 | Hopefully this image will be helpful in having you understand how the encryption and authentication works. 11 | 12 | .. image:: /_static/jak_crypto_description.jpg 13 | :alt: flow diagram of how jak encrypts plaintext. 14 | 15 | 16 | Encryption 17 | ---------- 18 | 19 | jak uses the **PyCrypto** implementation of **AES256** running in **CBC-MODE** for its encryption. What makes AES be 256 is the key space of the key you use. For 256-bit you should have a 32 byte key that is as random as possible. 1 byte is 8 bits so 256 / 8 = 32. This gives you a key space of 2^32. 20 | 21 | jak requires a 64 character hexadecimal key. It can :ref:`generate it for you. ` It should look something like this ``b30259425d7e5a8b4858f72948d7a232142c292997d6431efaa6a02d7a866b03``. To keep it readable we are actually representing the bytes as hexdigits, 2 hex digits are 1 byte of complexity. ``b3 02 59 42`` is 4 bytes. Therefore the 64 character is key 32 bytes. jak generates this key from **/dev/urandom** (``binascii.hexlify(os.urandom(32))``). 22 | 23 | CBC-MODE requires padding. jak uses **PKCS#7** padding. In plain English that means that jak pads the plaintext secret to be a multiple of the block size (defaults to 16) by adding padding where each character is a number equal to the amount of padding. The previous sentence might be tricky, so here is an example to clarify: ``pad('aaaaaaaaaaaaa') returns 'aaaaaaaaaaaaa\x03\x03\x03'``. 24 | 25 | CBC-MODE also requires an **Initialization Vector (IV)**. jak generates it using the **Fortuna (PRNG)** as implemented by **PyCrypto**. 26 | 27 | Further reading: 28 | 29 | * https://www.pycrypto.org 30 | * https://en.wikipedia.org/wiki/Advanced_Encryption_Standard 31 | * https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 32 | * https://en.wikipedia.org/wiki/Key_space_(cryptography) 33 | * https://en.wikipedia.org/wiki/Initialization_vector 34 | * `https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 `_ 35 | * https://en.wikipedia.org/wiki/Fortuna_(PRNG) 36 | 37 | 38 | HMAC 39 | ---- 40 | 41 | jak uses `Encrypt-then-MAC (EtM) `_ for authentication. The hash function is **SHA512**. 42 | 43 | .. image:: https://upload.wikimedia.org/wikipedia/commons/b/b9/Authenticated_Encryption_EtM.png 44 | :alt: picture of encrypt then MAC. 45 | 46 | The key for the HMAC is simply passed through SHA512, which is questionably necessary. The argument for passing it through SHA512 is basically that it "can‘t hurt" and "better safe than sorry". We would love to hear your opinion on this. Read more about our reasoning `here. `_ 47 | 48 | Further reading: 49 | 50 | * https://moxie.org/blog/the-cryptographic-doom-principle/ 51 | * https://en.wikipedia.org/wiki/Authenticated_encryption 52 | * https://en.wikipedia.org/wiki/SHA-2 53 | * http://crypto.stackexchange.com/a/8086 54 | 55 | 56 | .. _prng_digression: 57 | 58 | Obtaining randomness 59 | -------------------- 60 | 61 | The random values jak generates are the **key** and the **IV**. Measuring randomness is hard if not impossible and there seems to be a great deal of differing opinions about what is a good source. The TL;DR seems to be that /dev/urandom and Fortuna are sufficiently random. But please educate yourself, it's a really interesting subject. Here are some good links to get you started. 62 | 63 | * https://docs.python.org/3.5/library/os.html#os.urandom 64 | * https://docs.python.org/2.7/library/os.html#os.urandom 65 | * https://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/ 66 | * http://www.2uo.de/myths-about-urandom/ 67 | * https://github.com/dlitz/pycrypto/blob/master/lib/Crypto/Random/__init__.py 68 | 69 | 70 | Final thoughts 71 | -------------- 72 | 73 | Implementing good cryptography has many not-so-subtle opportunities for an implementer, or library, to make a mistake. This situation is not helped by the fact that the types of attacks that are available is a continually changing landscape. That is why we encourage as much openness about how jak is implemented as possible so that possible issues can be caught early on. 74 | -------------------------------------------------------------------------------- /jak/outputs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2018 Dispel, LLC 4 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 5 | """ 6 | 7 | FRESH_JAKFILE = u''' 8 | {{ 9 | 10 | // This list is for the encrypt/decrypt all commands and for the 11 | // pre-commit hook (optional) protection. 12 | "files_to_encrypt": ["path/to/file"], 13 | "keyfile": "{keyfile_path}" 14 | }}''' 15 | 16 | KEYGEN_RESPONSE = '''Here is your shiny new key. 17 | 18 | {key} 19 | 20 | Remember to keep this password secret and save it. Without it you will NOT be able 21 | to decrypt any file(s) you encrypt using it.''' 22 | 23 | PRE_COMMIT_CALL = '''#!/bin/sh 24 | # ---- Begin jak Block ---- 25 | 26 | PURPLE='\\033[1;35m' 27 | NC='\\033[0m' # No Color 28 | 29 | printf "🌰 ${PURPLE}jak: pre-commit > Encrypting files listed in jakfile.${NC}\\n" 30 | 31 | # See http://click.pocoo.org/6/python3/ for more info 32 | export LC_ALL=en_US.UTF-8 33 | export LANG=en_US.UTF-8 34 | 35 | # Not thrilled about this since it is OS specific but certain git apps 36 | # (in this instance SourceTree) couldn't find jak in the pre-commit hook. 37 | export PATH=/usr/local/bin:$PATH 38 | 39 | 40 | # Encrypt any staged files that are protected by jak 41 | python .git/hooks/jak.pre-commit.py 42 | 43 | # ---- End jak Block ---- 44 | 45 | # Place your custom pre-commit code here 46 | ''' 47 | 48 | PRE_COMMIT_EXISTS = ''' 49 | 50 | jak says: EXISTING PRE-COMMIT HOOK, I DON'T WANT TO OVERRIDE IT WILLY NILLY 51 | SEE .git/hooks/jak.pre-commit.py for further installation instructions.''' 52 | 53 | PRE_COMMIT_ENCRYPT = '''#!/usr/bin/env python 54 | # -*- coding: utf-8 -*- 55 | 56 | """ 57 | Copyright 2018 Dispel, LLC 58 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 59 | 60 | INSTALLATION 61 | The pre-commit hook is usually added with the jak start command. 62 | If you want to add it manually I would recommend running jak start in a temp. 63 | local git repository and copying the files from the .git/hooks folder. 64 | """ 65 | 66 | from __future__ import unicode_literals 67 | 68 | import subprocess 69 | from io import open 70 | import os 71 | 72 | 73 | def _remove_comments_from_JSON(raw_json): 74 | """Technically JSON does not have comments. But it is very user friendly to 75 | allow for commenting so we strip the comments out in this function. 76 | Example input: 77 | // Comment 0 78 | { 79 | // Comment 1 80 | "Ada": "Lovelace" // Comment 2 81 | // Comment 3 82 | } // Comment 4 83 | Expected output: 84 | { 85 | "Ada": "Lovelace" 86 | } 87 | """ 88 | import re 89 | tmp = re.sub(r'//.*\\n', '\\n', raw_json) 90 | tmp = "".join(tmp.replace('\\n', '').split()) 91 | return tmp 92 | 93 | 94 | def read_jakfile_to_dict(): 95 | """Read the jakfile and dump it's json comments into a dict""" 96 | with open('jakfile', 'rt') as f: 97 | import json 98 | contents_raw = f.read() 99 | 100 | sans_comments = _remove_comments_from_JSON(contents_raw) 101 | return json.loads(sans_comments) 102 | 103 | 104 | def get_staged(): 105 | output = subprocess.check_output('git --no-pager diff --cached --name-only', 106 | shell=True) 107 | output_array = output.decode('utf-8').split('\\n') 108 | names = [name for name in output_array if name] 109 | return names 110 | 111 | 112 | def try_encrypt(filename): 113 | proc = subprocess.Popen(["jak", "encrypt", filename], env=dict(os.environ)) 114 | proc.communicate() 115 | 116 | 117 | def git_add(filename): 118 | proc = subprocess.Popen(['git', 'add', filename]) 119 | proc.communicate() 120 | 121 | 122 | if __name__ == '__main__': 123 | staged_files = get_staged() 124 | files_to_encrypt = read_jakfile_to_dict()['files_to_encrypt'] 125 | for staged_file in staged_files: 126 | if staged_file in files_to_encrypt: 127 | try_encrypt(staged_file) 128 | git_add(staged_file) 129 | ''' 130 | 131 | 132 | FINAL_START_MESSAGE = '''- - - Setup complete! - - - 133 | 134 | TL;DR; 135 | 1. If this is your first rodeo please look at your ./keyfile and your ./jakfile. 136 | 2. Keep your keyfile secret at all costs. Don't commit it to any VCS, don't email it, don't put it in dropbox, definitely don't put it in google drive, etc...! 137 | 138 | {version}''' # noqa 139 | 140 | QUESTION_WANT_TO_ADD_PRE_COMMIT = ''' 141 | Do you want to add a git pre-commit hook? 142 | The hook will encrypt files listed in your jakfile 143 | each time you git commit. [y/n]''' 144 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | 4 | Changelog 5 | ========= 6 | 7 | 8 | 1.0 (Young Whippersnapper) 9 | -------------------------- 10 | 11 | Lifecycle: Not released yet. 12 | 13 | 1.0.0 Will be assigned when we have verified that the encryption is absolutely stable AND we believe the risk of us accidentally deleting peoples secrets is < 0.0001%. In practice this means better unit testing and talking to 2-3 more cryptography experts (especially outside of Dispel). Are you such an expert? Get in touch! cdilorenzo@dispel.io. 14 | 15 | 16 | 0.14.6 17 | ------ 18 | 19 | Lifecycle: 2018-10-22 - current 20 | 21 | * **[0.14.6]** BUG: Had issue when double decrypting certain files (which should be safe operation, and now it is again). `(PR#56) `_ 22 | 23 | 24 | 0.14.5 25 | ------ 26 | 27 | Lifecycle: 2018-03-08 - 2018-10-22 28 | 29 | * **[0.14.4]** BUG: SourceTree (and other linuxy apps hopefully) should now work with the pre-commit hook. `(PR#45) `_ 30 | * **[0.14.5]** ENHANCEMENT: Better message when malformed jakfile. `(PR#51) `_ 31 | 32 | * Other: 33 | * Updated 2017 to 2018 (Happy new year!?) 34 | * Removed formal support for python 3.3 (since it is at its end of life). 35 | 36 | 37 | 0.14.3 38 | ------ 39 | 40 | Lifecycle: 2017-09-02 41 | 42 | * **[0.14.2]** BUG: Files with the same name now support the backup feature (maintain their encrypted state if their unencrypted state is not edited on re-encryption) if they are in different folders. `(PR#40) `_ 43 | * **[0.14.3]** DEV: Improved one of our tests that was placing backup files where they did not belong. `(PR#42) `_ 44 | 45 | * Other: 46 | * Update the tox.ini file to run tests on Python 3.6 (which we totally support btw.) `(PR#44) `_ 47 | * Updated copyrights to the year 2017 `(PR#43) `_ 48 | 49 | 50 | 0.14.1 51 | ------ 52 | 53 | Lifecycle: 2016-02-01 - 2017-09-02 54 | 55 | * **[0.14.1]** HOTFIX: Import of bytestring compatibility function was removed during a merge, and it happened unnoticed. `(commit) `_ 56 | 57 | * Other: 58 | * DOCS: fixed static links for the terminal examples (I guess readthedocs changed something?). 59 | 60 | 61 | 0.14 62 | ---- 63 | 64 | Lifecycle: 2016-02-01 - 2016-02-06 65 | 66 | * **[0.14.0]** FEATURE: jak encrypt/decrypt commands can now accept a list of files (jak encrypt file1 ... fileN -k ). `(PR#34) `_ 67 | * **[0.13.0]** FEATURE: jak works for all type of files, not just text files. `(PR#33) `_ 68 | * **[0.12.0]** FEATURE: Add encryption versioning. This allows us to upgrade/edit the cipher and still decrypt previous ciphertexts (so they don't become undecryptable) `(PR#31) `_ 69 | 70 | 71 | 0.11 72 | ---- 73 | 74 | Lifecycle: 2017-01-23 - 2017-02-01 75 | 76 | * **[0.11.0]** FEATURE: Properly use HMAC to make sure the ciphertext has not been tampered with. `(PR#28) `_ 77 | 78 | * Other: 79 | * Upgraded the dev environment `(PR#29) `_ 80 | * :ref:`Added security section to the documentation ` 81 | 82 | Acknowledgements: 83 | 84 | * Huge thank you to @obscurerichard (Richard Bullington-McGuire / @obscurerichard on GitHub & Twitter) for figuring out that jaks authentication could be improved. 85 | 86 | 87 | 0.10 88 | ---- 89 | 90 | Lifecycle: ~2017-01. 91 | 92 | * **[0.10.0]** FEATURE: Switched to CBC mode for AES from CFB. `(PR#14) `_ 93 | * **[0.10.1]** CLEANUP: Encrypt/Decrypt file services were a mess.. `(PR#15) `_ 94 | * **[0.10.2]** ENHANCEMENT: Make keyfile location in jakfile relative instead of absolute. `(PR#22) `_ 95 | * **[0.10.3]** BUG: Wrong key should print filepath. `(PR#21) `_ 96 | * **[0.10.4]** ENHANCEMENT: Made sure jak worked well in Python 3, 3.3, 3.4 and PyPy. `(PR#19) `_ 97 | * Other: 98 | * DOCS: Add videos of terminal usage, a ton of text content and this changelog. `(PR#27) `_ 99 | 100 | 101 | 0.0 - 0.9 (Troubled Toddler) 102 | ---------------------------- 103 | 104 | Lifecycle: ~2016-11 - ~2016-12 105 | 106 | Birth. 107 | -------------------------------------------------------------------------------- /jak/crypto_services.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Copyright 2018 Dispel, LLC 5 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 6 | """ 7 | 8 | import base64 9 | import binascii 10 | from io import open 11 | from . import helpers 12 | from .compat import b 13 | from .aes_cipher import AES256Cipher 14 | from .exceptions import JakException, WrongKeyException 15 | 16 | ENCRYPTED_BY_HEADER = u'- - - Encrypted by jak - - -\n\n' 17 | 18 | 19 | def _read_file(filepath): 20 | """Helper for reading a file and making sure it has content.""" 21 | try: 22 | with open(filepath, 'rb') as f: 23 | contents = f.read() 24 | except IOError: 25 | raise JakException("Sorry I can't find the file: {}".format(filepath)) 26 | 27 | if len(contents) == 0: 28 | raise JakException('The file "{}" is empty, aborting...'.format(filepath)) 29 | 30 | return contents 31 | 32 | 33 | def _restore_from_backup(jwd, filepath, plaintext, aes256_cipher): 34 | """Return backup value (if such exists and content in file has not changed) 35 | 36 | We may want to replace this with a simpler "check last modified time" lookup 37 | that could happen in constant time instead. 38 | """ 39 | if not helpers.is_there_a_backup(jwd=jwd, filepath=filepath): 40 | return None 41 | 42 | backup_ciphertext_original = helpers.get_backup_content_for_file(jwd=jwd, filepath=filepath) 43 | 44 | previous_enc = base64.urlsafe_b64decode(b(backup_ciphertext_original)) 45 | iv = aes256_cipher.extract_iv(ciphertext=previous_enc) 46 | new_secret_w_same_iv = aes256_cipher.encrypt(plaintext=plaintext, iv=iv) 47 | 48 | if new_secret_w_same_iv == previous_enc: 49 | return backup_ciphertext_original 50 | 51 | return None 52 | 53 | 54 | def write_ciphertext_to_file(filepath, ciphertext): 55 | ciphertext = b(ciphertext) 56 | ciphertext = ciphertext.replace(b'\n', b'') 57 | encrypted_chunks = helpers.grouper(ciphertext.decode('utf-8'), 60) 58 | with open(filepath, 'w', encoding='utf-8') as f: 59 | f.write(ENCRYPTED_BY_HEADER) 60 | for encrypted_chunk in encrypted_chunks: 61 | f.write(encrypted_chunk + '\n') 62 | 63 | 64 | def encrypt_file(jwd, filepath, key, **kwargs): 65 | """Encrypts a file""" 66 | plaintext = _read_file(filepath=filepath) 67 | 68 | if b(ENCRYPTED_BY_HEADER) in plaintext: 69 | raise JakException('I already encrypted the file: "{}".'.format(filepath)) 70 | 71 | aes256_cipher = AES256Cipher(key=key) 72 | 73 | ciphertext = _restore_from_backup(jwd=jwd, 74 | filepath=filepath, 75 | plaintext=plaintext, 76 | aes256_cipher=aes256_cipher) 77 | 78 | if not ciphertext: 79 | ciphertext_ugly = aes256_cipher.encrypt(plaintext=plaintext) 80 | 81 | # Base64 is prettier 82 | ciphertext = base64.urlsafe_b64encode(ciphertext_ugly) 83 | 84 | write_ciphertext_to_file(filepath=filepath, ciphertext=ciphertext) 85 | return '{} - is now encrypted.'.format(filepath) 86 | 87 | 88 | def decrypt_file(filepath, key, jwd, **kwargs): 89 | """Decrypts a file""" 90 | contents = _read_file(filepath=filepath) 91 | 92 | if b(ENCRYPTED_BY_HEADER) not in contents: 93 | return 'The file "{}" is already decrypted, or it is missing it\'s jak header.'.format( 94 | filepath) 95 | 96 | ciphertext_no_header = contents.replace(b(ENCRYPTED_BY_HEADER), b'') 97 | 98 | # We could actually check that the first few letters are SkFL (JAK in base64) 99 | # it seems unreasonably unlikely that a plaintext would start with those 4 characters. 100 | # But the header check above should be enough. 101 | 102 | # Remove the base64 encoding which is applied to make output prettier after encryption. 103 | try: 104 | ciphertext = base64.urlsafe_b64decode(ciphertext_no_header) 105 | except (TypeError, binascii.Error): 106 | return 'The file "{}" is already decrypted, or is not in a format I recognize.'.format( 107 | filepath) 108 | 109 | # Remember the encrypted file in the .jak folder 110 | # The reason to remember is because we don't want re-encryption of files to 111 | # be different from previous ones if the content has not changed (which it would 112 | # with a new random IV). This way it works way better with VCS systems like git. 113 | helpers.backup_file_content(jwd=jwd, filepath=filepath, content=ciphertext_no_header) 114 | 115 | # Perform decryption 116 | aes256_cipher = AES256Cipher(key=key) 117 | try: 118 | decrypted_secret = aes256_cipher.decrypt(ciphertext=ciphertext) 119 | except WrongKeyException as wke: 120 | raise JakException('{} - {}'.format(filepath, wke.__str__())) 121 | 122 | with open(filepath, 'wb') as f: 123 | f.write(decrypted_secret) 124 | 125 | return '{} - is now decrypted.'.format(filepath) 126 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # jak documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Jan 9 13:37:00 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | # 'sphinx.ext.autodoc', 32 | # 'sphinx.ext.doctest', 33 | 'sphinx.ext.ifconfig', 34 | 'sphinx.ext.viewcode'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'jak' 50 | copyright = '2017, Dispel, LLC' 51 | author = 'Dispel, LLC' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | from jak import __version__, __version_full__ 57 | 58 | # The short X.Y version. 59 | version = __version__ 60 | 61 | # The full version, including alpha/beta/rc tags. 62 | release = __version_full__ 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This patterns also effect to html_static_path and html_extra_path 74 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = 'sphinx' 78 | 79 | # If true, `todo` and `todoList` produce output, else they produce nothing. 80 | todo_include_todos = False 81 | 82 | 83 | # -- Options for HTML output ---------------------------------------------- 84 | 85 | # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org 86 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 87 | 88 | if not on_rtd: # only import and set the theme if we're building docs locally 89 | import sphinx_rtd_theme 90 | html_theme = 'sphinx_rtd_theme' 91 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | # 97 | html_theme_options = { 98 | # 'show_powered_by': False, 99 | # 'github_user': 'dispel', 100 | # 'github_repo': 'jak', 101 | # 'github_banner': True 102 | } 103 | 104 | # Add any paths that contain custom static files (such as style sheets) here, 105 | # relative to this directory. They are copied after the builtin static files, 106 | # so a file named "default.css" will overwrite the builtin "default.css". 107 | html_static_path = ['_static'] 108 | 109 | 110 | # -- Options for HTMLHelp output ------------------------------------------ 111 | 112 | # Output file base name for HTML help builder. 113 | htmlhelp_basename = 'jakdoc' 114 | 115 | # -- Options for manual page output --------------------------------------- 116 | 117 | # One entry per manual page. List of tuples 118 | # (source start file, name, description, authors, manual section). 119 | man_pages = [ 120 | (master_doc, 'jak', 'jak Documentation', 121 | [author], 1) 122 | ] 123 | 124 | 125 | # -- Options for Texinfo output ------------------------------------------- 126 | 127 | # Grouping the document tree into Texinfo files. List of tuples 128 | # (source start file, target name, title, author, 129 | # dir menu entry, description, category) 130 | texinfo_documents = [ 131 | (master_doc, 'jak', 'jak Documentation', 132 | author, 'jak', 'One line description of project.', 133 | 'Miscellaneous'), 134 | ] 135 | 136 | 137 | def setup(app): 138 | app.add_stylesheet('asciinema-player.css') 139 | app.add_javascript('asciinema-player.js') 140 | -------------------------------------------------------------------------------- /jak/aes_cipher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Copyright 2018 Dispel, LLC 5 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 6 | """ 7 | 8 | import hmac 9 | import binascii 10 | from .compat import b 11 | from Crypto import Random 12 | from Crypto.Cipher import AES 13 | from Crypto.Hash import SHA512 14 | from .padding import pad, unpad 15 | from .exceptions import JakException, WrongKeyException 16 | 17 | 18 | class AES256Cipher(object): 19 | """AES256 using CBC mode and a 16bit block size.""" 20 | 21 | def __init__(self, key, mode=AES.MODE_CBC): 22 | """You can override the mode if you want, But you had better know 23 | what you are doing.""" 24 | 25 | self.cipher = AES 26 | self.mode = mode 27 | self.BLOCK_SIZE = AES.block_size 28 | self.SIG_SIZE = SHA512.digest_size 29 | self.VERSION = 'JAK-000' 30 | 31 | # We force the key to be 64 hexdigits (nibbles) because we are sadists. 32 | key_issue_exception = JakException( 33 | ("Key must be 64 hexadecimal [0-f] characters long. \n" 34 | "jak recommends you use the 'keygen' command to generate a strong key.")) 35 | 36 | # Long enough? 37 | if len(key) != 64: 38 | raise key_issue_exception 39 | 40 | try: 41 | self.key = binascii.unhexlify(key) 42 | except (TypeError, binascii.Error): 43 | 44 | # Not all of them are hexadecimals in all likelihood 45 | raise key_issue_exception 46 | 47 | # Generate a separate HMAC key. This is (to my understanding) not 48 | # strictly necessary. 49 | # But was recommended by Thomas Pornin (http://crypto.stackexchange.com/a/8086) 50 | self.hmac_key = SHA512.new(data=key.encode()).digest() 51 | 52 | def _generate_iv(self): 53 | """Generates an Initialization Vector (IV). 54 | 55 | This implementation is the currently recommended way of generating an IV 56 | in PyCrypto's docs (https://www.dlitz.net/software/pycrypto/api/current/) 57 | """ 58 | return Random.new().read(self.BLOCK_SIZE) 59 | 60 | def _authenticate(self, data, signature): 61 | """True if key is correct and data has not been tampered with else False""" 62 | new_mac = hmac.new(key=self.hmac_key, msg=data, digestmod=SHA512).digest() 63 | 64 | # It is important to compare them like this instead of using '==' to prevent 65 | # timing attacks 66 | return hmac.compare_digest(new_mac, signature) 67 | 68 | def extract_iv(self, ciphertext): 69 | """Extract the IV""" 70 | return ciphertext[len(self.VERSION):len(self.VERSION) + self.BLOCK_SIZE] 71 | 72 | def _extract_signature(self, ciphertext): 73 | """extract the HMAC signature""" 74 | return ciphertext[-self.SIG_SIZE:] 75 | 76 | def _extract_payload(self, ciphertext): 77 | """Returns the meat and potatoes, the encrypted data payload. 78 | said another way it doesn't return the IV nor the MAC signature. 79 | """ 80 | return ciphertext[len(self.VERSION) + self.BLOCK_SIZE:-self.SIG_SIZE] 81 | 82 | def _extract_version(self, ciphertext): 83 | """Tag the ciphertexts with a version like JAK-001 84 | that way if we edit the cipher or mac we can still decrypt it but then 85 | re-encrypt it with the new stronger/bug free encryption. 86 | 87 | >>> self._extract_version('JAK-XXX324872y34g23yug...') 88 | "JAK-XXX" 89 | """ 90 | 91 | # Could also just write 7 here... just saying. 92 | return ciphertext[:len('JAK-000')] 93 | 94 | def _need_old_decrypt_function(self, version): 95 | return version != b(self.VERSION) 96 | 97 | def _use_old_decrypt_function(self, version, ciphertext): 98 | """jak version is not the current one, so we need to use an old 99 | decryption function to go back to the plaintext. 100 | This makes it so we can upgrade the our ciphers and not doom users to 101 | installing old versions of jak or being unable to decrypt files that 102 | were generated by previous jak versions.""" 103 | 104 | # Haven't upgraded our encryption since we added ciphertext versioning. 105 | # When we do we will replace this with a switch statement selecting old 106 | # Decryption methods. 107 | raise Exception('FATAL: No one should end up here.... VERSION: {}, C: {}'.format(version, ciphertext)) 108 | 109 | def decrypt(self, ciphertext): 110 | """Decrypts a ciphertext secret""" 111 | 112 | # This allows us to upgrade the encryption and MAC 113 | version = self._extract_version(ciphertext=ciphertext) 114 | 115 | if self._need_old_decrypt_function(version): 116 | return self._use_old_decrypt_function(version=version, ciphertext=ciphertext) 117 | 118 | signature = self._extract_signature(ciphertext=ciphertext) 119 | iv = self.extract_iv(ciphertext=ciphertext) 120 | payload = self._extract_payload(ciphertext=ciphertext) 121 | 122 | if not self._authenticate(data=payload, signature=signature): 123 | raise WrongKeyException('Wrong key OR the encrypted payload has been tampered with. Either way I am aborting...') # noqa 124 | 125 | # Setup cipher and perform actual decryption 126 | cipher_instance = self.cipher.new(key=self.key, mode=self.mode, IV=iv) 127 | payload_padded = cipher_instance.decrypt(ciphertext=payload) 128 | return unpad(data=payload_padded) 129 | 130 | def encrypt(self, plaintext, iv=False): 131 | """Encrypts a plaintext secret""" 132 | if not iv: 133 | iv = self._generate_iv() 134 | 135 | cipher_instance = self.cipher.new(key=self.key, mode=self.mode, IV=iv) 136 | plaintext_padded = pad(data=plaintext) 137 | encrypted_data = cipher_instance.encrypt(plaintext=plaintext_padded) 138 | signature = hmac.new(key=self.hmac_key, msg=encrypted_data, digestmod=SHA512).digest() 139 | return b(self.VERSION) + iv + encrypted_data + signature 140 | -------------------------------------------------------------------------------- /jak/diff.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2018 Dispel, LLC 4 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 5 | """ 6 | 7 | import os 8 | import re 9 | import click 10 | import base64 11 | import random 12 | import binascii 13 | import subprocess 14 | from io import open 15 | from . import helpers 16 | from .compat import b 17 | from .aes_cipher import AES256Cipher 18 | from .exceptions import JakException, WrongKeyException 19 | from . import decorators 20 | 21 | 22 | def _create_local_remote_diff_files(filepath, local, remote): 23 | """ 24 | Generates two files for use with diffing. 25 | 26 | _LOCAL_. 27 | _REMOTE_. 28 | 29 | Returns their paths as a tuple 30 | """ 31 | tag = random.randrange(10000, 99999) 32 | (filepath, ext) = os.path.splitext(filepath) 33 | local_file_path = '{}_LOCAL_{}{}'.format(filepath, tag, ext) 34 | remote_file_path = '{}_REMOTE_{}{}'.format(filepath, tag, ext) 35 | 36 | helpers.create_or_overwrite_file(filepath=local_file_path, content=local) 37 | helpers.create_or_overwrite_file(filepath=remote_file_path, content=remote) 38 | return local_file_path, remote_file_path 39 | 40 | 41 | def _vimdiff(filepath, local_file_path, remote_file_path): 42 | """ 43 | Tried for a ludicrous amount of time to get it to open vimdiff automagically. 44 | Instead we settled on just letting user know what command they should run. 45 | """ 46 | command = "vimdiff -f -d -c 'wincmd J' {merged} {local} {remote}".format( 47 | merged=filepath, local=local_file_path, remote=remote_file_path) 48 | 49 | return ''' 50 | 51 | ~*Currently under development*~ 52 | 53 | To open the diff use this command: 54 | $> {}'''.format(command) 55 | 56 | 57 | def _opendiff(filepath, local_file_path, remote_file_path): 58 | """""" 59 | # Write to devnull so user doesnt see a bunch of messages. 60 | # FIXME put in logfile instead? 61 | FNULL = open(os.devnull, 'w') 62 | subprocess.Popen(['opendiff', local_file_path, remote_file_path, '-merge', filepath], 63 | stdout=FNULL, 64 | stderr=subprocess.STDOUT) 65 | return "Opened opendiff." 66 | 67 | 68 | def _decrypt(key, local, remote): 69 | """ 70 | TODO 71 | why not just use crypto libraries decrypt here instead? 72 | """ 73 | try: 74 | ugly_local = base64.urlsafe_b64decode(b(local)) 75 | ugly_remote = base64.urlsafe_b64decode(b(remote)) 76 | except binascii.Error: 77 | msg = '''Failed during decryption. Are you sure the file you are pointing to is jak encrypted? 78 | 79 | For example: 80 | <<<<<<< SOMETHING 81 | 85 | ======= 86 | 90 | >>>>>>> SOMETHING''' 91 | raise JakException(msg) 92 | 93 | aes256_cipher = AES256Cipher(key=key) 94 | 95 | secrets = [] 96 | try: 97 | decrypted = aes256_cipher.decrypt(ciphertext=ugly_local) 98 | except WrongKeyException as wke: 99 | raise JakException('LOCAL - {}'.format(wke.__str__())) 100 | else: 101 | secrets.append(decrypted.decode('utf-8').rstrip('\n')) 102 | 103 | try: 104 | decrypted = aes256_cipher.decrypt(ciphertext=ugly_remote) 105 | except WrongKeyException as wke: 106 | raise JakException('REMOTE - {}'.format(wke.__str__())) 107 | else: 108 | secrets.append(decrypted.decode('utf-8').rstrip('\n')) 109 | 110 | return secrets 111 | 112 | 113 | def _extract_merge_conflict_parts(content): 114 | regex = re.compile(r'(<<<<<<<\s\S+.)(.+)(=======.)(.+)(>>>>>>>\s\S+.)', re.DOTALL) 115 | return regex.findall(content)[0] 116 | 117 | 118 | @decorators.read_jakfile 119 | @decorators.select_key 120 | def diff(filepath, key, **kwargs): 121 | """Diff and merge a file that has a merge conflict.""" 122 | with open(filepath, 'rt') as f: 123 | encrypted_diff_file = f.read() 124 | 125 | (header, local, separator, remote, end) = _extract_merge_conflict_parts(encrypted_diff_file) 126 | (decrypted_local, decrypted_remote) = _decrypt(key, local, remote) 127 | 128 | output = '''{header}{local} 129 | {separator}{remote} 130 | {end}'''.format( 131 | header=header, 132 | local=decrypted_local, 133 | separator=separator, 134 | remote=decrypted_remote, 135 | end=end) 136 | 137 | # Python 3 does not have a decode for this 138 | # but it doesn't need to perform the decode so all is well here. 139 | # Obviously once we give up on python 2 we won't have to 140 | # do horrible stuff like this anymore. 141 | try: 142 | output = output.decode('utf-8') 143 | except AttributeError: 144 | pass 145 | 146 | msg = '''Which editor do you want to use? 147 | plain (default): will simply decrypt the contents of the original file. 148 | opendiff: The macOS default merge tool. 149 | vimdiff: Hacker 4 life yo! 150 | 151 | [plain, opendiff, vimdiff]''' 152 | response = click.prompt(msg, default='plain') 153 | 154 | if response == 'opendiff': 155 | (local_file_path, remote_file_path) = _create_local_remote_diff_files( 156 | filepath=filepath, 157 | local=decrypted_local, 158 | remote=decrypted_remote) 159 | result = _opendiff(filepath=filepath, 160 | local_file_path=local_file_path, 161 | remote_file_path=remote_file_path) 162 | elif response == 'vimdiff': 163 | (local_file_path, remote_file_path) = _create_local_remote_diff_files( 164 | filepath=filepath, 165 | local=decrypted_local, 166 | remote=decrypted_remote) 167 | result = _vimdiff(filepath=filepath, 168 | local_file_path=local_file_path, 169 | remote_file_path=remote_file_path) 170 | elif response == 'plain': 171 | result = "Ok, file decrypted, go ahead an edit it manually. Godspeed you master of the universe." 172 | else: 173 | return "Unrecognized choice. Aborting without changing anything." 174 | 175 | # Replace the original file with the decrypted output 176 | with open(filepath, 'w', encoding='utf-8') as f: 177 | f.write(output) 178 | 179 | return result 180 | -------------------------------------------------------------------------------- /jak/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2018 Dispel, LLC 3 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 4 | """ 5 | 6 | import os 7 | import json 8 | import errno 9 | import binascii 10 | from io import open 11 | 12 | 13 | def grouper(iterable, n): 14 | """split iterable data into n-length blocks 15 | grouper('aaa', 2) == ('aa', 'a') 16 | """ 17 | return tuple(iterable[i:i + n] for i in range(0, len(iterable), n)) 18 | 19 | 20 | def create_or_overwrite_file(filepath, content): 21 | """""" 22 | # If not a path and just a file default to a local folder. 23 | dirname = os.path.dirname(filepath) or '.' 24 | 25 | if not os.path.exists(dirname): 26 | try: 27 | os.makedirs(dirname) 28 | 29 | # Guard against race condition 30 | except OSError as exc: 31 | if exc.errno != errno.EEXIST: 32 | raise 33 | 34 | try: 35 | content = content.decode('utf-8') 36 | except AttributeError: 37 | pass 38 | 39 | with open(filepath, 'w') as f: 40 | f.write(content) 41 | 42 | 43 | def create_backup_filepath(jwd, filepath): 44 | """Example: 45 | Input: jwd='/a/b/c', filepath: '/a/b/c/d/e.txt' 46 | Output: /a/b/c/.jak/d_e.txt_backup 47 | 48 | Input: jwd='/', filepath: '/a' 49 | Output: /.jak/a_backup 50 | 51 | Input: jwd='/a/b', filepath: '/a/b/c' 52 | Output: /a/b/.jak/c_backup 53 | 54 | FIXME: There is probably a way cleaner way to write this function. 55 | """ 56 | 57 | # To make this easier to understand: 58 | # filepath === /a/b/c/d 59 | # jwd = /a 60 | filename = filepath.replace(jwd, '') # /b/c/d 61 | 62 | # Special case: root. 63 | if ('/' not in filename): 64 | return '/.jak/{}_backup'.format(filename) 65 | 66 | filename = filename[1:] # b/c/d 67 | filename = filename.replace('/', '_') # b_c_d 68 | return '{}/.jak/{}_backup'.format(jwd, filename) # /a/.jak/b_c_d_backup 69 | 70 | 71 | def backup_file_content(jwd, filepath, content): 72 | """backs up a string in the .jak folder. 73 | 74 | TODO Needs test 75 | """ 76 | backup_filepath = create_backup_filepath(jwd=jwd, filepath=filepath) 77 | return create_or_overwrite_file(filepath=backup_filepath, content=content) 78 | 79 | 80 | def is_there_a_backup(jwd, filepath): 81 | """Check if a backup for a file exists""" 82 | filename = create_backup_filepath(jwd=jwd, filepath=filepath) 83 | return os.path.exists(filename) 84 | 85 | 86 | def get_backup_content_for_file(jwd, filepath): 87 | """Get the value of a previously encrypted file. 88 | The original use case is to restore encrypted state instead of randomizing 89 | a new one (due to IV random generation). This makes jak way more friendly 90 | to VCS systems such as git. 91 | 92 | TODO Needs test 93 | """ 94 | filename = create_backup_filepath(jwd=jwd, filepath=filepath) 95 | with open(filename, 'rt') as f: 96 | encrypted_secret = f.read() 97 | return encrypted_secret 98 | 99 | 100 | def two_column(left, right, col1_length=65, col2_length=1): 101 | """Two column layout for printouts. 102 | Example: 103 | I did this thing done! 104 | """ 105 | tmp = '%-{}s%-{}s'.format(col1_length, col2_length) 106 | 107 | # The space in front of the right column add minimal padding in case 108 | # lefts content is very long (>col1_length) 109 | return tmp % (left, ' ' + right) 110 | 111 | 112 | def generate_256bit_key(): 113 | """Generate a pseudo-random secure ready-for-crypto-use key. 114 | 115 | Generate it straight using urandom. Proving randomness is impossible, and a good source 116 | is a hotly debated subject. As always, opinions are welcome but please inform 117 | yourself first and be prepared to cite a source. 118 | 119 | Further Reading: 120 | https://docs.python.org/3.5/library/os.html#os.urandom 121 | https://docs.python.org/2.7/library/os.html#os.urandom 122 | https://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/ 123 | http://www.2uo.de/myths-about-urandom/ 124 | https://github.com/dlitz/pycrypto/blob/master/lib/Crypto/Random/__init__.py 125 | """ 126 | return binascii.hexlify(os.urandom(32)) 127 | 128 | 129 | def get_jak_working_directory(cwd=os.getcwd()): 130 | """Finds a git repository parent and returns the path to it. 131 | if none is found default to current directory: './'""" 132 | 133 | # They are probably in a .git repo so let's check that right off the bat. 134 | if os.path.exists('{}/.git'.format(cwd)): 135 | return cwd 136 | 137 | cwd_path = cwd.split('/') 138 | 139 | # Remove final one since we already checked current directory above. 140 | del cwd_path[-1] 141 | 142 | # Traverse up looking for a folder with a .git folder in it. 143 | # For example if C has a .git in it 144 | # /A/B/C/D/E/.git --> False 145 | # /A/B/C/D/.git --> False 146 | # /A/B/C/.git --> True, returns '/A/B/C' 147 | for directory in reversed(cwd_path): 148 | dirpath = '/'.join(cwd_path) 149 | if os.path.exists('{}/.git'.format(dirpath)): 150 | return dirpath 151 | cwd_path.remove(directory) 152 | 153 | # No parent git repo, let's just use the current directory 154 | return cwd 155 | 156 | 157 | def does_jwd_have_gitignore(cwd=os.getcwd()): 158 | """'' means they are in repo root.""" 159 | jwd = get_jak_working_directory(cwd=cwd) 160 | return os.path.exists('{}/.gitignore'.format(jwd)) 161 | 162 | 163 | def read_jakfile_to_dict(jwd=get_jak_working_directory()): 164 | """Read the jakfile and dump its json comments into a dict for easy usage""" 165 | with open('{}/jakfile'.format(jwd), 'rt') as f: 166 | contents_raw = f.read() 167 | 168 | sans_comments = _remove_comments_from_JSON(contents_raw) 169 | return json.loads(sans_comments) 170 | 171 | 172 | def _remove_comments_from_JSON(raw_json): 173 | """Technically JSON does not have comments. But it is very user friendly to 174 | allow for commenting so we strip the comments out in this function. 175 | 176 | Example input: 177 | // Comment 0 178 | { 179 | // Comment 1 180 | "Ada": "Lovelace" // Comment 2 181 | // Comment 3 182 | } // Comment 4 183 | 184 | Expected output: 185 | { 186 | "Ada": "Lovelace" 187 | } 188 | """ 189 | import re 190 | tmp = re.sub(r'//.*\n', '\n', raw_json) 191 | tmp = "".join(tmp.replace('\n', '').split()) 192 | return tmp 193 | -------------------------------------------------------------------------------- /docs/guide/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | Basic usage 4 | =========== 5 | 6 | 7 | 8 | Installation 9 | ------------ 10 | 11 | Assuming you :ref:`fullfill the basic support requirements ` all you need to do is ``pip install jak``. 12 | 13 | 14 | 15 | Getting started 16 | --------------- 17 | 18 | jak is intended for developers who want to protect secret files (such as .env files) in their shared git repositories. However there is nothing stopping jak from being instantiated into any folder nor encrypting any text file. 19 | 20 | Let's say we have the project ``flowers`` which has two secret files we want to protect, ``/flowers/.env and /flowers/settings/keys``. 21 | 22 | .. sourcecode:: shell 23 | 24 | $> cd /path/to/flowers 25 | 26 | $> jak start 27 | 28 | # edit the jakfile to look like this 29 | # { 30 | # "files_to_encrypt": [".env", "settings/keys"], 31 | # "keyfile": ".jak/keyfile" 32 | # } 33 | 34 | $> jak encrypt all 35 | 36 | # you can also encrypt/decrypt specific files 37 | $> jak decrypt .env 38 | 39 | Easy peasy lemon squeeze! :ref:`Read more about initializing jak here. ` 40 | 41 | 42 | 43 | Using jak without a jakfile 44 | --------------------------- 45 | 46 | Heres a video that explains: 47 | 48 | * Using jak without setup (which is fine, but not recommended for teams). 49 | * Generating a secure key. 50 | * Using the key to encrypt/decrypt a file via the CLI. 51 | * Creating your own keyfile. 52 | * One thing I do want to highlight is that the key will be stored in your CLI history, so this is not inherently more secure than keeping the key in a keyfile. 53 | 54 | 55 | .. raw:: html 56 | 57 | 58 |
59 | 67 | 68 | 69 | Which jak files should be committed? 70 | ------------------------------------ 71 | 72 | **commit:** jakfile 73 | 74 | **ignore:** .jak folder (which by default includes the keyfile) 75 | 76 | **NEVER EVER COMMIT YOUR KEYFILE! IT IS WHAT ENCRYPTS/DECRYPTS YOUR SECRETS!** 77 | 78 | 79 | 80 | .. _keyfile: 81 | 82 | keyfile 83 | ------- 84 | 85 | The keyfile is optional, as you can always pass through a key if you wish. This means you can store the key somewhere else if you are worried about having it in plaintext, in a file, on your computer. Which is a really bad idea if someone else has access to your computer, or you suspect your computer has been in some other way compromised. However, since you do need to use the key in some fashion to decrypt/encrypt files with jak an argument can definitely be made that having it in a file as opposed to having it in your command history (``$> history``) is about the same level of security. Passing keys to jak in a more secure way is something we are actively thinking about, and if you have opinions you should get in touch. 86 | 87 | A Keyfile can be referenced from the jakfile (see below) or directly ``jak encrypt --keyfile /path/to/keyfile``. 88 | 89 | The keyfile should have NO INFORMATION other than a :ref:`secure key `. 90 | 91 | 92 | 93 | .. _key: 94 | 95 | key 96 | --- 97 | 98 | Generate a new key by issuing the :ref:`jak keygen ` command. 99 | 100 | Since jak generates a key 32 byte key (64 characters, which jak generates as `Nibbles `_ (4bit) to keep things easy to read. If you really know what you are doing there is nothing stopping you from feeding jak 64 characters where each is a full byte though, so you could theoretically go for AES512 under this scheme. 101 | 102 | 103 | 104 | 105 | .. _jakfile: 106 | 107 | jakfile 108 | ------- 109 | 110 | A jakfile holds the common settings when issuing jak commands from the current working directory that has the jakfile in it. 111 | 112 | .. sourcecode:: json 113 | 114 | { 115 | "files_to_encrypt": ["file1", "dir/file2"], 116 | "keyfile": "/path/to/keyfile" 117 | } 118 | 119 | A jakfile has two values in it: ``"files_to_encrypt"`` and ``"keyfile"``. 120 | 121 | The ``keyfile`` value is optional as you can supply a key or a different keyfile manually as an optional argument. It should point to where your keyfile is located either absolutely or relatively to the location of the jakfile. 122 | We recommend using the ``keyfile`` value in the jakfile due to it (1) being easier and (2) not being less secure than supplying it as a command. 123 | 124 | **You should switch your key and cycle all of your secrets if you computer is compromised.** 125 | 126 | The ``files_to_encrypt`` value is a list specifying the files you wish to encrypt. This serves two purposes: 127 | 128 | 1. If you are in a git repository and have added the :ref:`pre-commit hook ` the hook will check against this list to identify whether you are adding a secret file in its decrypted state, and if so encrypt it for you. 129 | 2. It allows you to use the ``jak stomp/shave`` commands for encrypting and decrypting all of the files in the list really easily. 130 | 131 | 132 | 133 | .. _diffing: 134 | 135 | Diffing 136 | ------- 137 | 138 | :ref:`Reference on the diff command. ` 139 | 140 | The file being diffed should have a conflict looking something like this: 141 | 142 | .. sourcecode:: text 143 | 144 | <<<<<<< HEAD 145 | ZDRiM2Q0Yjg0ZTFkNDg3NzRhOTljOWVmYjAxOTE4NmI4Y2UzMTkwNTM5N2Nj 146 | YjdiYmQyZDU3MjI1MDkwY2ExYmU0NTMzOGYxYTViY2I0YWNlYzdmOWM2OTgz 147 | NmI5ODkxOWNhNjc5YjdiNGQ5ZDJiMTYyNDFhMzcwMWYxNDVmMWO8ttnsUSsa 148 | iDNgzDF18NB5RMHOOxjt13wRdV_RHxtZgw== 149 | ======= 150 | MGUwMWJhYjgxNDcyMjY2MjhmMzMzNWFlYTMwZDYzYzc5ZDc0NzVhMDc0M2Ji 151 | ZWUyMDc2NTAyZWM5MTRkMzQ5MmU4NTBlYzY1YjlmYTUwYTdlN2M2MDg3ZTI4 152 | NGMxNDZjYzJiZDczNGE1ZDEzYmRkZDMyY2IwMDI5Mjc3MWJmOWNXRvFeiNn8 153 | b6JFJwpATrZOE2srs1sc3p2TM529sw-11Q== 154 | >>>>>>> f8eb651525b7403aa5ed93c251374ddef8796dee 155 | 156 | Here is a video for your viewing pleasure. 157 | 158 | .. raw:: html 159 | 160 | 161 |
162 | 170 | -------------------------------------------------------------------------------- /tests/test_crypto.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import six 4 | import pytest 5 | from jak import helpers 6 | from jak.compat import b 7 | from Crypto.Cipher import AES 8 | from click.testing import CliRunner 9 | import jak.crypto_services as crypto 10 | from jak.exceptions import JakException 11 | 12 | 13 | @pytest.fixture 14 | def runner(): 15 | return CliRunner() 16 | 17 | 18 | @pytest.fixture 19 | def cipher(): 20 | key = '4e9e5f6688e2a011856f7d58a27f7d9695013a893d89ad7652f2976a5c61f97f' 21 | return crypto.AES256Cipher(key=key) 22 | 23 | 24 | def test_cipher(cipher): 25 | assert cipher.cipher == AES 26 | assert cipher.BLOCK_SIZE == AES.block_size 27 | assert cipher.mode == AES.MODE_CBC 28 | 29 | 30 | def test_bad_create_cipher(): 31 | # Fails due to no key argument. 32 | with pytest.raises(TypeError): 33 | crypto.AES256Cipher() 34 | 35 | 36 | def test_generate_iv(cipher): 37 | result = cipher._generate_iv() 38 | assert len(result) == 16 39 | assert isinstance(result, six.binary_type) 40 | 41 | 42 | @pytest.mark.parametrize('key', [ 43 | '', 44 | '1', 45 | '1111111111111111', # 16 46 | '111111111111111111111111', # 24 47 | '11111111111111111111111111111111', # 32 48 | '111111111111111111111111111111111111111111111111111111111111111', # 63 49 | '11111111111111111111111111111111111111111111111111111111111111111', # 65 50 | 'notmadeupofonlyhexadecimalcharacters1111111111111111111111111111' # 64 51 | ]) 52 | def test_bad_keys_for_cipher_exceptions(key): 53 | with pytest.raises(JakException) as excinfo: 54 | crypto.AES256Cipher(key=key) 55 | assert 'Key must be 64' in str(excinfo.value) 56 | 57 | 58 | def test_encrypt_decrypt(cipher): 59 | secret = b'secret' 60 | 61 | ciphertext = cipher.encrypt(plaintext=secret) 62 | plaintext = cipher.decrypt(ciphertext=ciphertext) 63 | assert isinstance(ciphertext, six.binary_type) 64 | assert isinstance(plaintext, six.binary_type) 65 | assert plaintext == secret 66 | assert ciphertext != secret 67 | assert ciphertext != plaintext 68 | 69 | 70 | def test_extractors(cipher): 71 | cipher.BLOCK_SIZE = len('IV') 72 | cipher.SIG_SIZE = len('signature') 73 | assert len(cipher.VERSION) == len('JAK-XXX') 74 | 75 | ciphertext = 'JAK-XXXIVpayloadsignature' 76 | assert cipher.extract_iv(ciphertext) == 'IV' 77 | assert cipher._extract_payload(ciphertext) == 'payload' 78 | assert cipher._extract_signature(ciphertext) == 'signature' 79 | assert cipher._extract_version(ciphertext) == 'JAK-XXX' 80 | 81 | 82 | def test_authenticate(cipher): 83 | plaintext = b'integrity' 84 | ciphertext = cipher.encrypt(plaintext=plaintext) 85 | payload = cipher._extract_payload(ciphertext=ciphertext) 86 | signature = cipher._extract_signature(ciphertext=ciphertext) 87 | assert cipher._authenticate(data=payload, signature=signature) is True 88 | 89 | bad_key = '02944c68b750474b85609147ce6d3aae875e6ae8ac63618086a58b1c1716402d' 90 | assert bad_key != cipher.key 91 | 92 | # Maybe we should allow setting of key/hmac_key in a method? 93 | new_cipher = crypto.AES256Cipher(key=bad_key) 94 | assert new_cipher._authenticate(data=payload, signature=signature) is False 95 | 96 | 97 | def test_authenticate_tampered(cipher): 98 | secret = b'integrity' 99 | ciphertext = cipher.encrypt(plaintext=secret) 100 | signature = cipher._extract_signature(ciphertext=ciphertext) 101 | payload = cipher._extract_payload(ciphertext=ciphertext) 102 | 103 | # Let's tamper with the payload 104 | dump = [x for x in payload] 105 | 106 | try: 107 | dump[5] = dump[5] + 1 if dump[5] != 255 else dump[5] - 1 108 | except TypeError: 109 | 110 | # Python 2 or PyPy 111 | x = ord(dump[5]) 112 | dump[5] = chr(x + 1) if x != 255 else chr(x - 1) 113 | tampered_payload = "".join(dump) 114 | else: 115 | tampered_payload = b("".join(map(chr, dump))) 116 | 117 | assert payload != tampered_payload 118 | assert cipher._authenticate(data=tampered_payload, signature=signature) is False 119 | 120 | 121 | def test_encrypt_file(tmpdir): 122 | secretfile = tmpdir.join("encrypt_file") 123 | secretfile.write("secret") 124 | assert secretfile.read() == "secret" 125 | key = helpers.generate_256bit_key().decode('utf-8') 126 | crypto.encrypt_file(jwd=secretfile.dirpath().strpath, filepath=secretfile.strpath, key=key) 127 | assert secretfile.read() != "secret" 128 | assert crypto.ENCRYPTED_BY_HEADER in secretfile.read() 129 | 130 | 131 | def test_bad_encrypt_file_filepath(tmpdir): 132 | key = helpers.generate_256bit_key().decode('utf-8') 133 | with pytest.raises(JakException) as excinfo: 134 | crypto.encrypt_file(jwd='', filepath='', key=key) 135 | assert "can't find the file: " in str(excinfo.value) 136 | 137 | 138 | def test_decrypt_file(runner, tmpdir): 139 | with runner.isolated_filesystem(): 140 | secretfile = tmpdir.join("hello") 141 | secretfile.write("""- - - Encrypted by jak - - - 142 | 143 | SkFLLTAwMI_ZCxve00vRIZq7if3C2cgVQ3Dlpjg2KPttRWtfq-bXOMsA1RUD 144 | 5h4PW-mnkFVkPJXWS0IHK95gfJNG9U13pcUoEj4bOGqtu62PCavRXZFcSwZ6 145 | -rNE_PQvkoIFq7KlBFrdu8pWPCyFVvZjpGEFgw4=""") 146 | key = '2a57929b3610ba53b96f472b0dca27402a57929b3610ba53b96f472b0dca2740' 147 | crypto.decrypt_file(jwd=secretfile.dirpath().strpath, filepath=secretfile.strpath, key=key) 148 | assert secretfile.read().strip('\n') == "we attack at dawn" 149 | 150 | 151 | def test_encrypt_and_decrypt_a_file(runner, tmpdir): 152 | with runner.isolated_filesystem(): 153 | secretfile = tmpdir.mkdir("sub").join("hello") 154 | secret_content = "supercalifragialisticexpialidocious" 155 | secretfile.write(secret_content) 156 | assert secretfile.read() == secret_content 157 | key = helpers.generate_256bit_key().decode('utf-8') 158 | crypto.encrypt_file(jwd=secretfile.dirpath().strpath, filepath=secretfile.strpath, key=key) 159 | 160 | # File has changed 161 | assert secretfile.read() != secret_content 162 | 163 | # File has the header (which we now assume means it is encrypted, 164 | # which might be presumptuous.) 165 | assert crypto.ENCRYPTED_BY_HEADER in secretfile.read() 166 | 167 | crypto.decrypt_file(jwd=secretfile.dirpath().strpath, filepath=secretfile.strpath, key=key) 168 | 169 | # Back to original 170 | assert secretfile.read() == secret_content 171 | 172 | 173 | def test_need_old_decrypt_version(cipher): 174 | same_version = b(cipher.VERSION) 175 | assert cipher._need_old_decrypt_function(version=same_version) is False 176 | 177 | different_version = b('JAK-XXX') 178 | assert cipher._need_old_decrypt_function(version=different_version) is True 179 | 180 | 181 | def test_use_old_decrypt_version(cipher): 182 | 183 | # Build out this test once we add old decrypt function calls in here. 184 | # basically making sure it picks the right one. 185 | with pytest.raises(Exception): 186 | cipher._use_old_decrypt_function('version', 'ciphertext') 187 | -------------------------------------------------------------------------------- /jak/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2018 Dispel, LLC 4 | Apache 2.0 License, see https://github.com/dispel/jak/blob/master/LICENSE for details. 5 | """ 6 | 7 | import os 8 | import click 9 | from . import helpers 10 | from . import outputs 11 | from . import __version_full__ 12 | from . import diff as diff_logic 13 | from . import decorators 14 | from . import start as start_logic 15 | from . import crypto_services as cs 16 | from .exceptions import JakException 17 | 18 | 19 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 20 | 21 | 22 | class JakGroup(click.Group): 23 | """An override of the list_commands logic of click.Group so as to order the commands 24 | in the help text more logically.""" 25 | 26 | def list_commands(self, ctx): 27 | """Override so we get commands in help file them in the order we want in the help""" 28 | 29 | # These are the ones we care about having first for usability reasons 30 | show_at_top = ['start', 'keygen', 'encrypt', 'decrypt', 'stomp', 'shave', 'diff'] 31 | 32 | # Append extra commands that are not in the priority list to the end. 33 | all_commands = sorted(self.commands) 34 | extras = set(all_commands) - set(show_at_top) 35 | return show_at_top + sorted(list(extras)) 36 | 37 | 38 | @click.group(invoke_without_command=True, 39 | context_settings=CONTEXT_SETTINGS, 40 | no_args_is_help=True, 41 | cls=JakGroup) 42 | @click.option('-v', '--version', is_flag=True) 43 | def main(version): 44 | """(c) Dispel LLC (Apache-2.0) 45 | 46 | Jak is a CLI tool for securely encrypting files. 47 | 48 | To get started I recommend typing "jak start" (preferably while your 49 | working directory is a git repository). 50 | 51 | Jaks intended use is for secret files in git repos that developers do 52 | not want to enter their permanent git history. But nothing prevents 53 | jak from being used outside of git. 54 | 55 | \b 56 | For more information about a certain command use: 57 | $> jak COMMAND --help 58 | 59 | For full documentation see https://github.com/dispel/jak 60 | """ 61 | if version: 62 | click.echo(__version_full__) 63 | 64 | 65 | @main.command() 66 | def start(): 67 | """Initializes jak in your working directory.""" 68 | click.echo('''- - - Welcome to jak - - - 69 | 70 | "jak start" does a couple of things: 71 | 1. jakfile: File with per working directory settings for jak. 72 | 2. keyfile: Holds the key used to encrypt files. 73 | ''') 74 | jwd = helpers.get_jak_working_directory() 75 | click.echo(start_logic.create_jakfile(jwd=jwd)) 76 | 77 | if not os.path.exists('{}/.git'.format(jwd)): 78 | msg = helpers.two_column('Is this a git repository?', 'Nope!') 79 | msg += '\n jak says: I work great with git, but you do you.' 80 | click.echo(msg) 81 | else: 82 | click.echo(helpers.two_column('Is this a git repository?', 'Yep!')) 83 | if helpers.does_jwd_have_gitignore(cwd=jwd): 84 | click.echo(helpers.two_column(' Is there a .gitignore?', 'Yep!')) 85 | start_logic.add_keyfile_to_gitignore(filepath=jwd + '/.gitignore') 86 | click.echo(helpers.two_column(' Adding ".jak" to .gitignore', 'Done')) 87 | else: 88 | click.echo(helpers.two_column(' Is there a .gitignore?', 'Nope!')) 89 | helpers.create_or_overwrite_file(filepath=jwd + '/.gitignore', 90 | content='# Jak KeyFile\n .jak \n') 91 | click.echo(helpers.two_column(' Creating ./.gitignore', 'Done')) 92 | click.echo(helpers.two_column(' Adding ".jak" to .gitignore', 'Done')) 93 | 94 | if start_logic.want_to_add_pre_commit_encrypt_hook(): 95 | click.echo('\n' + start_logic.add_pre_commit_encrypt_hook(jwd)) 96 | 97 | click.echo(outputs.FINAL_START_MESSAGE.format(version=__version_full__)) 98 | 99 | 100 | @main.command() 101 | @click.option('-m', '--minimal', is_flag=True) 102 | def keygen(minimal): 103 | """Generate a strong key for use with jak. 104 | 105 | You can keep the key wherever, but I would recommend putting it 106 | in a .gitignored keyfile that your jakfile points to. 107 | 108 | Do not add this key to your git repository. Nor should you ever give it 109 | to anyone who should not have access. Remember, if you give someone a key 110 | they can look at your git history and encrypt files encrypted with that key 111 | that happened in the past. If your current or past keys get out, I would 112 | recommend cycling your secrets and your keys. 113 | 114 | In fact I would recommend cycling your keys every so often (3-6 months) 115 | anyway, just as a standard best practice. But in reality very few developers 116 | actually do this. =( 117 | """ 118 | key = helpers.generate_256bit_key().decode('utf-8') 119 | if minimal: 120 | output = key 121 | else: 122 | output = outputs.KEYGEN_RESPONSE.format(key=key) 123 | click.echo(output) 124 | 125 | 126 | @decorators.attach_jwd 127 | @decorators.read_jakfile 128 | @decorators.select_key 129 | @decorators.select_files 130 | def encrypt_inner(files, key, **kwargs): 131 | """Logic for encrypting file(s)""" 132 | for filepath in files: 133 | try: 134 | result = cs.encrypt_file(filepath=filepath, key=key, **kwargs) 135 | except JakException as je: 136 | click.echo(je) 137 | else: 138 | click.echo(result) 139 | 140 | 141 | @main.command(help='jak encrypt ') 142 | @click.argument('filepaths', nargs=-1) 143 | @click.option('-k', '--key', default=None, metavar='') 144 | @click.option('-kf', '--keyfile', default=None, metavar='') 145 | def encrypt(filepaths, key, keyfile): 146 | """Encrypt file(s)""" 147 | for filepath in filepaths: 148 | try: 149 | encrypt_inner(all_or_filepath=filepath, key=key, keyfile=keyfile) 150 | except JakException as je: 151 | click.echo(je) 152 | 153 | 154 | @decorators.attach_jwd 155 | @decorators.read_jakfile 156 | @decorators.select_key 157 | @decorators.select_files 158 | def decrypt_inner(files, key, **kwargs): 159 | """Logic for decrypting file(s)""" 160 | for filepath in files: 161 | try: 162 | result = cs.decrypt_file(filepath=filepath, key=key, **kwargs) 163 | except JakException as je: 164 | click.echo(je) 165 | else: 166 | click.echo(result) 167 | 168 | 169 | @main.command(help='jak decrypt ') 170 | @click.argument('filepaths', nargs=-1) 171 | @click.option('-k', '--key', default=None, metavar='') 172 | @click.option('-kf', '--keyfile', default=None, metavar='') 173 | def decrypt(filepaths, key, keyfile): 174 | """Decrypt file(s)""" 175 | for filepath in filepaths: 176 | try: 177 | decrypt_inner(all_or_filepath=filepath, key=key, keyfile=keyfile) 178 | except JakException as je: 179 | click.echo(je) 180 | 181 | 182 | @main.command() 183 | @click.option('-k', '--key', default=None, metavar='') 184 | @click.option('-kf', '--keyfile', default=None, metavar='') 185 | def stomp(key, keyfile): 186 | """Alias for 'jak encrypt all'""" 187 | try: 188 | encrypt_inner(all_or_filepath='all', key=key, keyfile=keyfile) 189 | except JakException as je: 190 | click.echo(je) 191 | 192 | 193 | @main.command() 194 | @click.option('-k', '--key', default=None, metavar='') 195 | @click.option('-kf', '--keyfile', default=None, metavar='') 196 | def shave(key, keyfile): 197 | """Alias for 'jak decrypt all'""" 198 | try: 199 | decrypt_inner(all_or_filepath='all', key=key, keyfile=keyfile) 200 | except JakException as je: 201 | click.echo(je) 202 | 203 | 204 | @main.command(options_metavar='') 205 | @click.argument('conflicted_file', metavar='') 206 | @click.option('-k', '--key', default=None, metavar='') 207 | @click.option('-kf', '--keyfile', default=None, metavar='') 208 | def diff(conflicted_file, key, keyfile): 209 | """Decrypt conflicted file for an easier merge. 210 | 211 | \b 212 | Supported merge tools: 213 | plain: Just decrypted and you can sort it out in a text editor. (default) 214 | opendiff: macOS built in FileMerge GUI tool. 215 | vimdiff: I decrypt and give you the vimdiff command to run to finish the merge. 216 | """ 217 | try: 218 | result = diff_logic.diff(filepath=conflicted_file, key=key, keyfile=keyfile) 219 | except JakException as je: 220 | result = je 221 | click.echo(result) 222 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /docs/_static/videos/diffmerge_short.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "width": 92, 4 | "height": 23, 5 | "duration": 27.8602, 6 | "command": null, 7 | "title": "diffmerge", 8 | "env": { 9 | "TERM": "xterm-256color", 10 | "SHELL": "/bin/zsh" 11 | }, 12 | "stdout": [ 13 | [ 14 | 2.316494, 15 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r\u001b]2;chris@pauling: ~/diff\u0007\u001b]1;~/diff\u0007" 16 | ], 17 | [ 18 | 0.020861, 19 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/diff \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[69C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[82D" 20 | ], 21 | [ 22 | 0.000472, 23 | "\u001b[?1h\u001b=" 24 | ], 25 | [ 26 | 0.000732, 27 | "\u001b[?2004h" 28 | ], 29 | [ 30 | 2.313995, 31 | "\u001b[32ml\u001b[39m" 32 | ], 33 | [ 34 | 0.098671, 35 | "\b\u001b[32ml\u001b[32ms\u001b[39m" 36 | ], 37 | [ 38 | 0.557072, 39 | "\u001b[?1l\u001b>" 40 | ], 41 | [ 42 | 0.001124, 43 | "\u001b[?2004l\r\r\n" 44 | ], 45 | [ 46 | 0.000565, 47 | "\u001b]2;ls -G\u0007\u001b]1;ls\u0007" 48 | ], 49 | [ 50 | 0.006183, 51 | "secret\r\n" 52 | ], 53 | [ 54 | 0.000816, 55 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 56 | ], 57 | [ 58 | 0.000158, 59 | "\u001b]2;chris@pauling: ~/diff\u0007\u001b]1;~/diff\u0007" 60 | ], 61 | [ 62 | 0.033904, 63 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/diff \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[69C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[82D" 64 | ], 65 | [ 66 | 0.000181, 67 | "\u001b[?1h\u001b=" 68 | ], 69 | [ 70 | 0.000572, 71 | "\u001b[?2004h" 72 | ], 73 | [ 74 | 0.299973, 75 | "\u001b[1m\u001b[31mc\u001b[0m\u001b[39m" 76 | ], 77 | [ 78 | 0.119899, 79 | "\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 80 | ], 81 | [ 82 | 0.124118, 83 | "\b\b\u001b[0m\u001b[32mc\u001b[0m\u001b[32ma\u001b[32mt\u001b[39m" 84 | ], 85 | [ 86 | 0.108163, 87 | " " 88 | ], 89 | [ 90 | 0.094634, 91 | "\u001b[4ms\u001b[24m" 92 | ], 93 | [ 94 | 0.170686, 95 | "\b\u001b[4ms\u001b[4me\u001b[24m" 96 | ], 97 | [ 98 | 0.131543, 99 | "\b\u001b[4me\u001b[4mc\u001b[24m" 100 | ], 101 | [ 102 | 0.190153, 103 | "\u001b[?7l\u001b[31m......\u001b[39m\u001b[?7h" 104 | ], 105 | [ 106 | 0.019323, 107 | "\r\r\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/diff \u001b[38;5;105m»\u001b[00m \u001b[32mcat\u001b[39m \u001b[4msecret\u001b[24m\u001b[1m \u001b[0m\u001b[K\u001b[58C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[71D" 108 | ], 109 | [ 110 | 0.542795, 111 | "\b\u001b[0m \b" 112 | ], 113 | [ 114 | 2.4e-05, 115 | "\u001b[?1l\u001b>" 116 | ], 117 | [ 118 | 0.001393, 119 | "\u001b[?2004l\r\r\n" 120 | ], 121 | [ 122 | 0.000435, 123 | "\u001b]2;cat secret\u0007\u001b]1;cat\u0007" 124 | ], 125 | [ 126 | 0.004064, 127 | "- - - Encrypted by jak - - -\r\n\r\n\r\n<<<<<<< HEAD\r\nNGJkMjc2YWRmMTYyN2RmZjZjZTNjNTAzNDQ3NTcyZmYxYjMyOWY5N2NiOThk\r\nM2RhOTRkMDkwMjJhZTA1NTgxOTY1MGYxNWFkYzk1OWQ1N2Q3OWE4OTczZDk1\r\nZGM0ZTM2NGQ5MDkxZmQyMzQxYmEzNzA2ZDM5OWU2MDVlNTdjZjeRpmoyhjV4\r\nN9NzWMfhSEzSqEBCxzcD1c6PsExEnGrKYpSMUGof_7qo8DgdoXx9C3c=\r\n=======\r\nMmE4NDI5Yzg3MWJkOWExYzhhNWU4Zjc2ZTY5NzM5YTkwMDgwZmM4ZTllZTli\r\nYjRjZGU5MTY4MmJhMTk1MmRjZjk3ZGE1Njg2OTMyZGE3NDM3NDk5NDRhY2Mw\r\nM2U0ZjEyM2YxNmM4YjMyNWZiOTE0ZTFmNjZkMmEwYzEyNjQ5YmUO9cNGc7zr\r\nyMCxQUX6tDlF26_gaiv3cnlFIvQftPXbRzxhRQ71CVSY7iXlN0AH9gE=\r\n>>>>>>> f8eb651525b7403aa5ed93c251374ddef8796dee\r\n" 128 | ], 129 | [ 130 | 0.000545, 131 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 132 | ], 133 | [ 134 | 9.1e-05, 135 | "\u001b]2;chris@pauling: ~/diff\u0007\u001b]1;~/diff\u0007" 136 | ], 137 | [ 138 | 0.026468, 139 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/diff \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[69C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[82D" 140 | ], 141 | [ 142 | 0.000129, 143 | "\u001b[?1h\u001b=" 144 | ], 145 | [ 146 | 0.000389, 147 | "\u001b[?2004h" 148 | ], 149 | [ 150 | 0.999604, 151 | "\u001b[1m\u001b[31mj\u001b[0m\u001b[39m" 152 | ], 153 | [ 154 | 0.088845, 155 | "\b\u001b[1m\u001b[31mj\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 156 | ], 157 | [ 158 | 0.087527, 159 | "\b\b\u001b[0m\u001b[32mj\u001b[0m\u001b[32ma\u001b[32mk\u001b[39m" 160 | ], 161 | [ 162 | 0.180214, 163 | " " 164 | ], 165 | [ 166 | 0.134305, 167 | "d" 168 | ], 169 | [ 170 | 0.100083, 171 | "i" 172 | ], 173 | [ 174 | 0.134226, 175 | "f" 176 | ], 177 | [ 178 | 0.123529, 179 | "f" 180 | ], 181 | [ 182 | 0.140908, 183 | " " 184 | ], 185 | [ 186 | 0.068507, 187 | "\u001b[4ms\u001b[24m" 188 | ], 189 | [ 190 | 0.164118, 191 | "\b\u001b[4ms\u001b[4me\u001b[24m" 192 | ], 193 | [ 194 | 0.142869, 195 | "\b\u001b[4me\u001b[4mc\u001b[24m" 196 | ], 197 | [ 198 | 0.211252, 199 | "\u001b[?7l" 200 | ], 201 | [ 202 | 3.3e-05, 203 | "\u001b[31m......\u001b[39m\u001b[?7h" 204 | ], 205 | [ 206 | 0.006159, 207 | "\r\r\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/diff \u001b[38;5;105m»\u001b[00m \u001b[32mjak\u001b[39m diff \u001b[4msecret\u001b[24m\u001b[1m \u001b[0m\u001b[K\u001b[53C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[66D" 208 | ], 209 | [ 210 | 1.01908, 211 | "\b\u001b[0m -" 212 | ], 213 | [ 214 | 0.126764, 215 | "-" 216 | ], 217 | [ 218 | 0.172991, 219 | "k" 220 | ], 221 | [ 222 | 0.104786, 223 | "e" 224 | ], 225 | [ 226 | 0.114304, 227 | "y" 228 | ], 229 | [ 230 | 0.175861, 231 | " " 232 | ], 233 | [ 234 | 0.385308, 235 | "\u001b[22D\u001b[39mj\u001b[39ma\u001b[39mk\u001b[6C\u001b[24ms\u001b[24me\u001b[24mc\u001b[24mr\u001b[24me\u001b[24mt\u001b[7C\u001b[7m7bf880ac682f9d8326b7a2f328821e93f6b29c4c3ae9bf2fee689e8c6115c\u001b[7m5\u001b[7m60\u001b[27m\u001b[K" 236 | ], 237 | [ 238 | 1.000635, 239 | "\u001b[A\u001b[6C\u001b[32mj\u001b[32ma\u001b[32mk\u001b[39m\u001b[6C\u001b[4ms\u001b[4me\u001b[4mc\u001b[4mr\u001b[4me\u001b[4mt\u001b[24m\u001b[7C\u001b[27m7\u001b[27mb\u001b[27mf\u001b[27m8\u001b[27m8\u001b[27m0\u001b[27ma\u001b[27mc\u001b[27m6\u001b[27m8\u001b[27m2\u001b[27mf\u001b[27m9\u001b[27md\u001b[27m8\u001b[27m3\u001b[27m2\u001b[27m6\u001b[27mb\u001b[27m7\u001b[27ma\u001b[27m2\u001b[27mf\u001b[27m3\u001b[27m2\u001b[27m8\u001b[27m8\u001b[27m2\u001b[27m1\u001b[27me\u001b[27m9\u001b[27m3\u001b[27mf\u001b[27m6\u001b[27mb\u001b[27m2\u001b[27m9\u001b[27mc\u001b[27m4\u001b[27mc\u001b[27m3\u001b[27ma\u001b[27me\u001b[27m9\u001b[27mb\u001b[27mf\u001b[27m2\u001b[27mf\u001b[27me\u001b[27me\u001b[27m6\u001b[27m8\u001b[27m9\u001b[27me\u001b[27m8\u001b[27mc\u001b[27m6\u001b[27m1\u001b[27m1\u001b[27m5\u001b[27mc5\u001b[27m6\u001b[27m0" 240 | ], 241 | [ 242 | 3.7e-05, 243 | "\u001b[?1l\u001b>" 244 | ], 245 | [ 246 | 0.00273, 247 | "\u001b[?2004l\r\r\n" 248 | ], 249 | [ 250 | 0.000572, 251 | "\u001b]2;jak diff secret --key \u0007\u001b]1;jak\u0007" 252 | ], 253 | [ 254 | 0.249594, 255 | "Which editor do you want to use?\r\nplain (default): will simply decrypt the contents of the original file.\r\nopendiff: The macOS default merge tool.\r\nvimdiff: Hacker 4 life yo!\r\n\r\n[plain, opendiff, vimdiff] [plain]: " 256 | ], 257 | [ 258 | 1.213871, 259 | "p" 260 | ], 261 | [ 262 | 0.063682, 263 | "l" 264 | ], 265 | [ 266 | 0.092254, 267 | "a" 268 | ], 269 | [ 270 | 0.083951, 271 | "i" 272 | ], 273 | [ 274 | 0.056244, 275 | "n" 276 | ], 277 | [ 278 | 0.295038, 279 | "\r\n" 280 | ], 281 | [ 282 | 0.000368, 283 | "Ok, file decrypted, go ahead an edit it manually. Godspeed you master of the universe.\r\n" 284 | ], 285 | [ 286 | 0.019957, 287 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 288 | ], 289 | [ 290 | 0.000126, 291 | "\u001b]2;chris@pauling: ~/diff\u0007" 292 | ], 293 | [ 294 | 2.3e-05, 295 | "\u001b]1;~/diff\u0007" 296 | ], 297 | [ 298 | 0.022491, 299 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/diff \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[69C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[82D" 300 | ], 301 | [ 302 | 8e-05, 303 | "\u001b[?1h\u001b=" 304 | ], 305 | [ 306 | 0.000466, 307 | "\u001b[?2004h" 308 | ], 309 | [ 310 | 0.426073, 311 | "\u001b[1m\u001b[31mn\u001b[0m\u001b[39m" 312 | ], 313 | [ 314 | 0.09961, 315 | "\b\u001b[1m\u001b[31mn\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 316 | ], 317 | [ 318 | 0.060034, 319 | "\b\b\u001b[1m\u001b[31mn\u001b[1m\u001b[31ma\u001b[1m\u001b[31mn\u001b[0m\u001b[39m" 320 | ], 321 | [ 322 | 0.144372, 323 | "\b\b\b\u001b[0m\u001b[32mn\u001b[0m\u001b[32ma\u001b[0m\u001b[32mn\u001b[32mo\u001b[39m" 324 | ], 325 | [ 326 | 0.164373, 327 | " " 328 | ], 329 | [ 330 | 0.056996, 331 | "\u001b[4ms\u001b[24m" 332 | ], 333 | [ 334 | 0.150787, 335 | "\b\u001b[4ms\u001b[4me\u001b[24m" 336 | ], 337 | [ 338 | 0.149089, 339 | "\b\u001b[4me\u001b[4mc\u001b[24m" 340 | ], 341 | [ 342 | 0.19311, 343 | "\u001b[?7l\u001b[31m......\u001b[39m\u001b[?7h" 344 | ], 345 | [ 346 | 0.005719, 347 | "\r\r\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/diff \u001b[38;5;105m»\u001b[00m \u001b[32mnano\u001b[39m \u001b[4msecret\u001b[24m\u001b[1m \u001b[0m\u001b[K\u001b[57C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[70D" 348 | ], 349 | [ 350 | 0.489953, 351 | "\b\u001b[0m \b" 352 | ], 353 | [ 354 | 2.5e-05, 355 | "\u001b[?1l\u001b>" 356 | ], 357 | [ 358 | 0.001427, 359 | "\u001b[?2004l\r\r\n" 360 | ], 361 | [ 362 | 0.000513, 363 | "\u001b]2;nano secret\u0007\u001b]1;nano\u0007" 364 | ], 365 | [ 366 | 0.004608, 367 | "\u001b[?1049h\u001b[1;23r\u001b(B\u001b[m\u001b[4l\u001b[?7h\u001b[?12l\u001b[?25h" 368 | ], 369 | [ 370 | 3.9e-05, 371 | "\u001b[?1h\u001b=\u001b[?1h\u001b=\u001b[?1h\u001b=" 372 | ], 373 | [ 374 | 0.000673, 375 | "\u001b[39;49m\u001b[39;49m\u001b(B\u001b[m\u001b[H\u001b[2J\u001b(B\u001b[0;7m GNU nano 2.0.6 File: secret \u001b[3;1H\u001b(B\u001b[m<<<<<<< HEAD\r\u001b[4dAUDIENCE=PRETTY_HOT\r\u001b[5d=======\r\u001b[6dAUDIENCE=REALLY_HOT\r\u001b[7d>>>>>>> f8eb651525b7403aa5ed93c251374ddef8796dee\u001b[21;39H\u001b(B\u001b[0;7m[ Read 5 lines ]\r\u001b[22d^G\u001b(B\u001b[m Get Help \u001b(B\u001b[0;7m^O\u001b(B\u001b[m WriteOut \u001b(B\u001b[0;7m^R\u001b(B\u001b[m Read File \u001b(B\u001b[0;7m^Y\u001b(B\u001b[m Prev Page \u001b(B\u001b[0;7m^K\u001b(B\u001b[m Cut Text \u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cur Pos\r\u001b[23d\u001b(B\u001b[0;7m^X\u001b(B\u001b[m Exit\u001b[23;16H\u001b(B\u001b[0;7m^J\u001b(B\u001b[m Justify \u001b(B\u001b[0;7m^W\u001b(B\u001b[m Where Is \u001b(B\u001b[0;7m^V\u001b(B\u001b[m Next Page \u001b(B\u001b[0;7m^U\u001b(B\u001b[m UnCut Text \u001b(B\u001b[0;7m^T\u001b(B\u001b[m To Spell\r\u001b[3d" 376 | ], 377 | [ 378 | 1.759468, 379 | "\u001b[3;20r\u001b[20;1H\n\u001b[1;23r\u001b[1;83H\u001b(B\u001b[0;7mModified\r\u001b[3d\u001b(B\u001b[m" 380 | ], 381 | [ 382 | 0.159499, 383 | "\u001b[3;20r\u001b[20;1H\n\u001b[1;23r\u001b[3;1H" 384 | ], 385 | [ 386 | 0.299699, 387 | "\u001b[3;20r\u001b[20;1H\n\u001b[1;23r\u001b[3;1H" 388 | ], 389 | [ 390 | 0.705947, 391 | "\u001b[4d" 392 | ], 393 | [ 394 | 0.595072, 395 | "\u001b[K" 396 | ], 397 | [ 398 | 0.671052, 399 | "\u001b[21d\u001b(B\u001b[0;7mSave modified buffer (ANSWERING \"No\" WILL DESTROY CHANGES) ? \u001b[22;1H Y\u001b(B\u001b[m Yes\u001b[K\r\u001b[23d\u001b(B\u001b[0;7m N\u001b(B\u001b[m No \u001b[23;16H \u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cancel\u001b[K\u001b[21;62H" 400 | ], 401 | [ 402 | 0.220605, 403 | "\r\u001b(B\u001b[0;7mFile Name to Write: secret \r\u001b[22d^G\u001b(B\u001b[m Get Help\u001b[22;24H\u001b(B\u001b[0;7m^T\u001b(B\u001b[m To Files\u001b[22;47H\u001b(B\u001b[0;7mM-M\u001b(B\u001b[m Mac Format\u001b[22;70H\u001b(B\u001b[0;7mM-P\u001b(B\u001b[m Prepend\r\u001b[23d\u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cancel\u001b[17G \u001b(B\u001b[0;7mM-D\u001b(B\u001b[m DOS Format\u001b[23;47H\u001b(B\u001b[0;7mM-A\u001b(B\u001b[m Append\u001b[23;70H\u001b(B\u001b[0;7mM-B\u001b(B\u001b[m Backup File\u001b[21;27H" 404 | ], 405 | [ 406 | 0.460922, 407 | "\r\u001b[22d\u001b[39;49m\u001b(B\u001b[m\u001b[J\u001b[1;83H\u001b(B\u001b[0;7m \u001b[21;38H\u001b(B\u001b[m\u001b[1K \u001b(B\u001b[0;7m[ Wrote 1 line ]\u001b(B\u001b[m\u001b[K\u001b[23;92H\u001b[23;1H\u001b[?1049l\r\u001b[?1l\u001b>" 408 | ], 409 | [ 410 | 0.000738, 411 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 412 | ], 413 | [ 414 | 0.000119, 415 | "\u001b]2;chris@pauling: ~/diff\u0007" 416 | ], 417 | [ 418 | 2.4e-05, 419 | "\u001b]1;~/diff\u0007" 420 | ], 421 | [ 422 | 0.022623, 423 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/diff \u001b[38;5;105m»\u001b[00m " 424 | ], 425 | [ 426 | 3.3e-05, 427 | "\u001b[K\u001b[69C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[82D" 428 | ], 429 | [ 430 | 0.000181, 431 | "\u001b[?1h\u001b=" 432 | ], 433 | [ 434 | 0.0004, 435 | "\u001b[?2004h" 436 | ], 437 | [ 438 | 0.352034, 439 | "\u001b[1m\u001b[31me\u001b[0m\u001b[39m" 440 | ], 441 | [ 442 | 0.160721, 443 | "\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mc\u001b[0m\u001b[39m" 444 | ], 445 | [ 446 | 0.08815, 447 | "\b\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mc\u001b[1m\u001b[31mh\u001b[0m\u001b[39m" 448 | ], 449 | [ 450 | 0.202649, 451 | "\b\b\b\u001b[0m\u001b[32me\u001b[0m\u001b[32mc\u001b[0m\u001b[32mh\u001b[32mo\u001b[39m" 452 | ], 453 | [ 454 | 0.455266, 455 | " " 456 | ], 457 | [ 458 | 0.531232, 459 | "\u001b[33m\"\u001b[39m" 460 | ], 461 | [ 462 | 0.116437, 463 | "\b\u001b[33m\"\u001b[33m\"\u001b[39m" 464 | ], 465 | [ 466 | 0.211167, 467 | "\b" 468 | ], 469 | [ 470 | 0.391657, 471 | "\u001b[33mB\u001b[33m\"\u001b[39m\b" 472 | ], 473 | [ 474 | 0.123403, 475 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 476 | ], 477 | [ 478 | 0.142431, 479 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 480 | ], 481 | [ 482 | 0.203648, 483 | "\u001b[33mm\u001b[33m\"\u001b[39m\b" 484 | ], 485 | [ 486 | 0.591432, 487 | "\u001b[33m.\u001b[33m\"\u001b[39m\b" 488 | ], 489 | [ 490 | 0.177073, 491 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 492 | ], 493 | [ 494 | 0.139904, 495 | "\u001b[33mP\u001b[33m\"\u001b[39m\b" 496 | ], 497 | [ 498 | 0.095809, 499 | "\u001b[33mr\u001b[33m\"\u001b[39m\b" 500 | ], 501 | [ 502 | 0.079683, 503 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 504 | ], 505 | [ 506 | 0.111774, 507 | "\u001b[33mf\u001b[33m\"\u001b[39m\b" 508 | ], 509 | [ 510 | 0.072666, 511 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 512 | ], 513 | [ 514 | 0.112798, 515 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 516 | ], 517 | [ 518 | 0.14194, 519 | "\u001b[33m.\u001b[33m\"\u001b[39m\b" 520 | ], 521 | [ 522 | 0.497845, 523 | "\u001b[?1l\u001b>" 524 | ], 525 | [ 526 | 0.001496, 527 | "\u001b[?2004l\r\r\n" 528 | ], 529 | [ 530 | 0.000466, 531 | "\u001b]2;echo \"Boom. Profit.\"\u0007\u001b]1;echo\u0007" 532 | ], 533 | [ 534 | 0.000126, 535 | "Boom. Profit.\r\n" 536 | ], 537 | [ 538 | 2.5e-05, 539 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 540 | ], 541 | [ 542 | 0.000124, 543 | "\u001b]2;chris@pauling: ~/diff\u0007\u001b]1;~/diff\u0007" 544 | ], 545 | [ 546 | 0.025579, 547 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/diff \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[69C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[82D" 548 | ], 549 | [ 550 | 0.000219, 551 | "\u001b[?1h\u001b=" 552 | ], 553 | [ 554 | 0.000448, 555 | "\u001b[?2004h" 556 | ], 557 | [ 558 | 1.013109, 559 | "\u001b[?2004l\r\r\n" 560 | ] 561 | ] 562 | } -------------------------------------------------------------------------------- /docs/_static/videos/nosetup.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "width": 92, 4 | "height": 23, 5 | "duration": 117.867784, 6 | "command": null, 7 | "title": "no-setup", 8 | "env": { 9 | "TERM": "xterm-256color", 10 | "SHELL": "/bin/zsh" 11 | }, 12 | "stdout": [ 13 | [ 14 | 3.204427, 15 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r\u001b]2;chris@pauling: ~/no-setup\u0007\u001b]1;~/no-setup\u0007" 16 | ], 17 | [ 18 | 0.02146, 19 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m " 20 | ], 21 | [ 22 | 0.000132, 23 | "\u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 24 | ], 25 | [ 26 | 7.8e-05, 27 | "\u001b[?1h\u001b=" 28 | ], 29 | [ 30 | 0.000363, 31 | "\u001b[?2004h" 32 | ], 33 | [ 34 | 2.879382, 35 | "\u001b[1m\u001b[31me\u001b[0m\u001b[39m" 36 | ], 37 | [ 38 | 0.200015, 39 | "\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mc\u001b[0m\u001b[39m" 40 | ], 41 | [ 42 | 0.080307, 43 | "\b\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mc\u001b[1m\u001b[31mh\u001b[0m\u001b[39m" 44 | ], 45 | [ 46 | 0.165021, 47 | "\b\b\b\u001b[0m\u001b[32me\u001b[0m\u001b[32mc\u001b[0m\u001b[32mh\u001b[32mo\u001b[39m" 48 | ], 49 | [ 50 | 0.173956, 51 | " " 52 | ], 53 | [ 54 | 0.909066, 55 | "\u001b[33m\"\u001b[39m" 56 | ], 57 | [ 58 | 0.111829, 59 | "\b\u001b[33m\"\u001b[33m\"\u001b[39m" 60 | ], 61 | [ 62 | 0.217046, 63 | "\b" 64 | ], 65 | [ 66 | 0.515814, 67 | "\u001b[33mH\u001b[33m\"\u001b[39m\b" 68 | ], 69 | [ 70 | 0.107217, 71 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 72 | ], 73 | [ 74 | 0.063678, 75 | "\u001b[33my\u001b[33m\"\u001b[39m\b" 76 | ], 77 | [ 78 | 1.143141, 79 | "\u001b[33m,\u001b[33m\"\u001b[39m\b" 80 | ], 81 | [ 82 | 0.21058, 83 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 84 | ], 85 | [ 86 | 0.259046, 87 | "\u001b[33mI\u001b[33m\"\u001b[39m\b" 88 | ], 89 | [ 90 | 0.213355, 91 | "\u001b[33mm\u001b[33m\"\u001b[39m\b" 92 | ], 93 | [ 94 | 0.170072, 95 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 96 | ], 97 | [ 98 | 0.10644, 99 | "\u001b[33mg\u001b[33m\"\u001b[39m\b" 100 | ], 101 | [ 102 | 0.061273, 103 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 104 | ], 105 | [ 106 | 0.027523, 107 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 108 | ], 109 | [ 110 | 0.164981, 111 | "\u001b[33mn\u001b[33m\"\u001b[39m\b" 112 | ], 113 | [ 114 | 0.068283, 115 | "\u001b[33mg\u001b[33m\"\u001b[39m\b" 116 | ], 117 | [ 118 | 0.096169, 119 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 120 | ], 121 | [ 122 | 0.086303, 123 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 124 | ], 125 | [ 126 | 0.051849, 127 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 128 | ], 129 | [ 130 | 0.08058, 131 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 132 | ], 133 | [ 134 | 0.07573, 135 | "\u001b[33ms\u001b[33m\"\u001b[39m\b" 136 | ], 137 | [ 138 | 0.059774, 139 | "\u001b[33mh\u001b[33m\"\u001b[39m\b" 140 | ], 141 | [ 142 | 0.124713, 143 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 144 | ], 145 | [ 146 | 0.096417, 147 | "\u001b[33mw\u001b[33m\"\u001b[39m\b" 148 | ], 149 | [ 150 | 0.043844, 151 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 152 | ], 153 | [ 154 | 0.08806, 155 | "\u001b[33my\u001b[33m\"\u001b[39m\b" 156 | ], 157 | [ 158 | 0.138176, 159 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 160 | ], 161 | [ 162 | 0.043888, 163 | "\u001b[33mu\u001b[33m\"\u001b[39m\b" 164 | ], 165 | [ 166 | 0.067983, 167 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 168 | ], 169 | [ 170 | 0.09821, 171 | "\u001b[33mh\u001b[33m\"\u001b[39m\b" 172 | ], 173 | [ 174 | 0.128118, 175 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 176 | ], 177 | [ 178 | 0.063453, 179 | "\u001b[33mw\u001b[33m\"\u001b[39m\b" 180 | ], 181 | [ 182 | 0.128304, 183 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 184 | ], 185 | [ 186 | 0.462944, 187 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 188 | ], 189 | [ 190 | 0.088332, 191 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 192 | ], 193 | [ 194 | 0.111685, 195 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 196 | ], 197 | [ 198 | 0.122334, 199 | "\u001b[33mu\u001b[33m\"\u001b[39m\b" 200 | ], 201 | [ 202 | 0.067377, 203 | "\u001b[33ms\u001b[33m\"\u001b[39m\b" 204 | ], 205 | [ 206 | 0.198669, 207 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 208 | ], 209 | [ 210 | 0.063673, 211 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 212 | ], 213 | [ 214 | 1.364107, 215 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 216 | ], 217 | [ 218 | 0.079597, 219 | "\u001b[33mh\u001b[33m\"\u001b[39m\b" 220 | ], 221 | [ 222 | 0.082394, 223 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 224 | ], 225 | [ 226 | 0.079998, 227 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 228 | ], 229 | [ 230 | 0.108423, 231 | "\u001b[33mv\u001b[33m\"\u001b[39m\b" 232 | ], 233 | [ 234 | 0.124638, 235 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 236 | ], 237 | [ 238 | 0.064681, 239 | "\u001b[33mr\u001b[33m\"\u001b[39m\b" 240 | ], 241 | [ 242 | 0.047402, 243 | "\u001b[33my\u001b[33m\"\u001b[39m\b" 244 | ], 245 | [ 246 | 0.076073, 247 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 248 | ], 249 | [ 250 | 0.14928, 251 | "\u001b[33mb\u001b[33m\"\u001b[39m\b" 252 | ], 253 | [ 254 | 0.154282, 255 | "\u001b[33ma\u001b[33m\"\u001b[39m\b" 256 | ], 257 | [ 258 | 0.047053, 259 | "\u001b[33ms\u001b[33m\"\u001b[39m\b" 260 | ], 261 | [ 262 | 0.104032, 263 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 264 | ], 265 | [ 266 | 0.132505, 267 | "\u001b[33mc\u001b[33m\"\u001b[39m\b" 268 | ], 269 | [ 270 | 0.111817, 271 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 272 | ], 273 | [ 274 | 0.139975, 275 | "\u001b[33mf\u001b[33m\"\u001b[39m\b" 276 | ], 277 | [ 278 | 0.051861, 279 | "\u001b[33mu\u001b[33m\"\u001b[39m\b" 280 | ], 281 | [ 282 | 0.076303, 283 | "\u001b[33mn\u001b[33m\"\u001b[39m\b" 284 | ], 285 | [ 286 | 0.071858, 287 | "\u001b[33mc\u001b[33m\"\u001b[39m\b" 288 | ], 289 | [ 290 | 0.208624, 291 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 292 | ], 293 | [ 294 | 0.002774, 295 | "\u001b[33mi\u001b[33m\"\u001b[39m\u001b[K\b" 296 | ], 297 | [ 298 | 0.069006, 299 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 300 | ], 301 | [ 302 | 0.151455, 303 | "\u001b[33mn\u001b[33m\"\u001b[39m\b" 304 | ], 305 | [ 306 | 0.084869, 307 | "\u001b[33ma\u001b[33m\"\u001b[39m\b" 308 | ], 309 | [ 310 | 0.103449, 311 | "\u001b[33ml\u001b[33m\"\u001b[39m\b" 312 | ], 313 | [ 314 | 0.028114, 315 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 316 | ], 317 | [ 318 | 0.205849, 319 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 320 | ], 321 | [ 322 | 0.156073, 323 | "\u001b[33my\u001b[33m\"\u001b[39m\b" 324 | ], 325 | [ 326 | 0.270971, 327 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 328 | ], 329 | [ 330 | 0.125644, 331 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 332 | ], 333 | [ 334 | 0.118597, 335 | "\u001b[33mf\u001b[33m\"\u001b[39m\b" 336 | ], 337 | [ 338 | 0.092386, 339 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 340 | ], 341 | [ 342 | 0.100068, 343 | "\u001b[33mj\u001b[33m\"\u001b[39m\b" 344 | ], 345 | [ 346 | 0.107343, 347 | "\u001b[33ma\u001b[33m\"\u001b[39m\b" 348 | ], 349 | [ 350 | 0.144242, 351 | "\u001b[33mk\u001b[33m\"\u001b[39m \r\u001b[K\u001b[A\u001b[91C" 352 | ], 353 | [ 354 | 0.127294, 355 | "\u001b[33m \u001b[33m\"\u001b[39m\r" 356 | ], 357 | [ 358 | 0.206383, 359 | "\u001b[33mb\u001b[33m\"\u001b[39m\b" 360 | ], 361 | [ 362 | 0.152431, 363 | "\r\u001b[33mb\u001b[33my\u001b[33m\"\u001b[39m\b" 364 | ], 365 | [ 366 | 0.726862, 367 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 368 | ], 369 | [ 370 | 0.116074, 371 | "\u001b[33ms\u001b[33m\"\u001b[39m\b" 372 | ], 373 | [ 374 | 0.063999, 375 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 376 | ], 377 | [ 378 | 0.06866, 379 | "\u001b[33mm\u001b[33m\"\u001b[39m\b" 380 | ], 381 | [ 382 | 0.158333, 383 | "\u001b[33mp\u001b[33m\"\u001b[39m\b" 384 | ], 385 | [ 386 | 0.053993, 387 | "\u001b[33ml\u001b[33m\"\u001b[39m\b" 388 | ], 389 | [ 390 | 0.203446, 391 | "\u001b[33my\u001b[33m\"\u001b[39m\b" 392 | ], 393 | [ 394 | 0.341042, 395 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 396 | ], 397 | [ 398 | 0.180958, 399 | "\u001b[33mg\u001b[33m\"\u001b[39m\b" 400 | ], 401 | [ 402 | 0.088565, 403 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 404 | ], 405 | [ 406 | 0.087568, 407 | "\u001b[33mn\u001b[33m\"\u001b[39m\b" 408 | ], 409 | [ 410 | 0.092042, 411 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 412 | ], 413 | [ 414 | 0.098899, 415 | "\u001b[33mr\u001b[33m\"\u001b[39m\b" 416 | ], 417 | [ 418 | 0.124421, 419 | "\u001b[33ma\u001b[33m\"\u001b[39m\b" 420 | ], 421 | [ 422 | 0.116186, 423 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 424 | ], 425 | [ 426 | 0.083691, 427 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 428 | ], 429 | [ 430 | 0.051755, 431 | "\u001b[33mn\u001b[33m\"\u001b[39m\b" 432 | ], 433 | [ 434 | 0.092671, 435 | "\u001b[33mg\u001b[33m\"\u001b[39m\b" 436 | ], 437 | [ 438 | 0.095743, 439 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 440 | ], 441 | [ 442 | 0.063903, 443 | "\u001b[33ma\u001b[33m\"\u001b[39m\b" 444 | ], 445 | [ 446 | 0.100112, 447 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 448 | ], 449 | [ 450 | 0.116155, 451 | "\u001b[33mk\u001b[33m\"\u001b[39m\b" 452 | ], 453 | [ 454 | 0.119885, 455 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 456 | ], 457 | [ 458 | 0.120072, 459 | "\u001b[33my\u001b[33m\"\u001b[39m\b" 460 | ], 461 | [ 462 | 0.080049, 463 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 464 | ], 465 | [ 466 | 0.063924, 467 | "\u001b[33ma\u001b[33m\"\u001b[39m\b" 468 | ], 469 | [ 470 | 0.108762, 471 | "\u001b[33mn\u001b[33m\"\u001b[39m\b" 472 | ], 473 | [ 474 | 0.065428, 475 | "\u001b[33md\u001b[33m\"\u001b[39m\b" 476 | ], 477 | [ 478 | 0.074658, 479 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 480 | ], 481 | [ 482 | 0.068266, 483 | "\u001b[33mp\u001b[33m\"\u001b[39m\b" 484 | ], 485 | [ 486 | 0.083929, 487 | "\u001b[33ma\u001b[33m\"\u001b[39m\b" 488 | ], 489 | [ 490 | 0.055983, 491 | "\u001b[33ms\u001b[33m\"\u001b[39m\b" 492 | ], 493 | [ 494 | 0.133046, 495 | "\u001b[33ms\u001b[33m\"\u001b[39m\b" 496 | ], 497 | [ 498 | 0.093212, 499 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 500 | ], 501 | [ 502 | 0.066314, 503 | "\u001b[33mn\u001b[33m\"\u001b[39m\b" 504 | ], 505 | [ 506 | 0.085024, 507 | "\u001b[33mg\u001b[33m\"\u001b[39m\b" 508 | ], 509 | [ 510 | 0.109075, 511 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 512 | ], 513 | [ 514 | 0.130208, 515 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 516 | ], 517 | [ 518 | 0.128389, 519 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 520 | ], 521 | [ 522 | 2.051271, 523 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 524 | ], 525 | [ 526 | 0.136606, 527 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 528 | ], 529 | [ 530 | 0.064478, 531 | "\u001b[33mh\u001b[33m\"\u001b[39m\b" 532 | ], 533 | [ 534 | 0.113414, 535 | "\u001b[33mr\u001b[33m\"\u001b[39m\b" 536 | ], 537 | [ 538 | 0.072035, 539 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 540 | ], 541 | [ 542 | 0.05272, 543 | "\u001b[33mu\u001b[33m\"\u001b[39m\b" 544 | ], 545 | [ 546 | 0.110793, 547 | "\u001b[33mg\u001b[33m\"\u001b[39m\b" 548 | ], 549 | [ 550 | 0.067905, 551 | "\u001b[33mh\u001b[33m\"\u001b[39m\b" 552 | ], 553 | [ 554 | 0.067757, 555 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 556 | ], 557 | [ 558 | 0.056328, 559 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 560 | ], 561 | [ 562 | 0.108221, 563 | "\u001b[33mh\u001b[33m\"\u001b[39m\b" 564 | ], 565 | [ 566 | 0.031661, 567 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 568 | ], 569 | [ 570 | 0.088792, 571 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 572 | ], 573 | [ 574 | 0.19627, 575 | "\u001b[33mC\u001b[33m\"\u001b[39m\b" 576 | ], 577 | [ 578 | 0.124224, 579 | "\u001b[33mL\u001b[33m\"\u001b[39m\b" 580 | ], 581 | [ 582 | 0.043243, 583 | "\u001b[33mI\u001b[33m\"\u001b[39m\b" 584 | ], 585 | [ 586 | 0.606663, 587 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 588 | ], 589 | [ 590 | 0.173501, 591 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 592 | ], 593 | [ 594 | 0.065115, 595 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 596 | ], 597 | [ 598 | 0.083074, 599 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 600 | ], 601 | [ 602 | 0.093962, 603 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 604 | ], 605 | [ 606 | 0.08625, 607 | "\u001b[33mn\u001b[33m\"\u001b[39m\b" 608 | ], 609 | [ 610 | 0.084018, 611 | "\u001b[33mc\u001b[33m\"\u001b[39m\b" 612 | ], 613 | [ 614 | 0.173605, 615 | "\u001b[33mr\u001b[33m\"\u001b[39m\b" 616 | ], 617 | [ 618 | 0.043115, 619 | "\u001b[33my\u001b[33m\"\u001b[39m\b" 620 | ], 621 | [ 622 | 0.170971, 623 | "\u001b[33mp\u001b[33m\"\u001b[39m\b" 624 | ], 625 | [ 626 | 0.105589, 627 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 628 | ], 629 | [ 630 | 0.08347, 631 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 632 | ], 633 | [ 634 | 0.100193, 635 | "\u001b[33ma\u001b[33m\"\u001b[39m\b" 636 | ], 637 | [ 638 | 0.084037, 639 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 640 | ], 641 | [ 642 | 0.112728, 643 | "\u001b[33mf\u001b[33m\"\u001b[39m\b" 644 | ], 645 | [ 646 | 0.083248, 647 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 648 | ], 649 | [ 650 | 0.171012, 651 | "\u001b[33ml\u001b[33m\"\u001b[39m\b" 652 | ], 653 | [ 654 | 0.171931, 655 | "\u001b[33me\u001b[33m\"\u001b[39m\b" 656 | ], 657 | [ 658 | 0.183483, 659 | "\u001b[33m.\u001b[33m\"\u001b[39m\b" 660 | ], 661 | [ 662 | 0.845647, 663 | "\u001b[?1l\u001b>" 664 | ], 665 | [ 666 | 0.003674, 667 | "\u001b[?2004l\r\r\n" 668 | ], 669 | [ 670 | 0.000719, 671 | "\u001b]2;echo \u0007\u001b]1;echo\u0007" 672 | ], 673 | [ 674 | 7.3e-05, 675 | "Hey, Im going to show you how to use the very basic functionality of jak by simply generating a key and passing it through the CLI to encrypt a file.\r\n" 676 | ], 677 | [ 678 | 3.2e-05, 679 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 680 | ], 681 | [ 682 | 5.6e-05, 683 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 684 | ], 685 | [ 686 | 2.2e-05, 687 | "\u001b]1;~/no-setup\u0007" 688 | ], 689 | [ 690 | 0.023698, 691 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 692 | ], 693 | [ 694 | 8.4e-05, 695 | "\u001b[?1h\u001b=" 696 | ], 697 | [ 698 | 0.000425, 699 | "\u001b[?2004h" 700 | ], 701 | [ 702 | 3.541268, 703 | "\u001b[1m\u001b[31mn\u001b[0m\u001b[39m" 704 | ], 705 | [ 706 | 0.107563, 707 | "\b\u001b[1m\u001b[31mn\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 708 | ], 709 | [ 710 | 0.064863, 711 | "\b\b\u001b[1m\u001b[31mn\u001b[1m\u001b[31ma\u001b[1m\u001b[31mn\u001b[0m\u001b[39m" 712 | ], 713 | [ 714 | 0.13806, 715 | "\b\b\b\u001b[0m\u001b[32mn\u001b[0m\u001b[32ma\u001b[0m\u001b[32mn\u001b[32mo\u001b[39m" 716 | ], 717 | [ 718 | 0.137551, 719 | " " 720 | ], 721 | [ 722 | 0.100247, 723 | "s" 724 | ], 725 | [ 726 | 0.167393, 727 | "e" 728 | ], 729 | [ 730 | 0.14919, 731 | "c" 732 | ], 733 | [ 734 | 0.210935, 735 | "r" 736 | ], 737 | [ 738 | 0.068113, 739 | "e" 740 | ], 741 | [ 742 | 0.186651, 743 | "t" 744 | ], 745 | [ 746 | 0.91731, 747 | "\u001b[?1l\u001b>" 748 | ], 749 | [ 750 | 0.001512, 751 | "\u001b[?2004l\r\r\n" 752 | ], 753 | [ 754 | 0.000417, 755 | "\u001b]2;nano secret\u0007\u001b]1;nano\u0007" 756 | ], 757 | [ 758 | 0.005721, 759 | "\u001b[?1049h\u001b[1;23r\u001b(B\u001b[m\u001b[4l\u001b[?7h\u001b[?12l\u001b[?25h\u001b[?1h\u001b=" 760 | ], 761 | [ 762 | 3.2e-05, 763 | "\u001b[?1h\u001b=\u001b[?1h\u001b=" 764 | ], 765 | [ 766 | 0.000476, 767 | "\u001b[39;49m\u001b[39;49m\u001b(B\u001b[m\u001b[H\u001b[2J\u001b(B\u001b[0;7m GNU nano 2.0.6 File: secret \u001b[21;41H[ New File ]\r\u001b[22d^G\u001b(B\u001b[m Get Help \u001b(B\u001b[0;7m^O\u001b(B\u001b[m WriteOut \u001b(B\u001b[0;7m^R\u001b(B\u001b[m Read File \u001b(B\u001b[0;7m^Y\u001b(B\u001b[m Prev Page \u001b(B\u001b[0;7m^K\u001b(B\u001b[m Cut Text \u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cur Pos\r\u001b[23d\u001b(B\u001b[0;7m^X\u001b(B\u001b[m Exit\u001b[23;16H\u001b(B\u001b[0;7m^J\u001b(B\u001b[m Justify \u001b(B\u001b[0;7m^W\u001b(B\u001b[m Where Is \u001b(B\u001b[0;7m^V\u001b(B\u001b[m Next Page \u001b(B\u001b[0;7m^U\u001b(B\u001b[m UnCut Text \u001b(B\u001b[0;7m^T\u001b(B\u001b[m To Spell\r\u001b[3d" 768 | ], 769 | [ 770 | 1.25798, 771 | "\u001b[1;83H\u001b(B\u001b[0;7mModified\r\u001b[3d\u001b(B\u001b[mM" 772 | ], 773 | [ 774 | 0.248429, 775 | "Y" 776 | ], 777 | [ 778 | 0.351862, 779 | "S" 780 | ], 781 | [ 782 | 0.171626, 783 | "E" 784 | ], 785 | [ 786 | 0.132048, 787 | "C" 788 | ], 789 | [ 790 | 0.199921, 791 | "R" 792 | ], 793 | [ 794 | 0.052425, 795 | "E" 796 | ], 797 | [ 798 | 0.163623, 799 | "T" 800 | ], 801 | [ 802 | 0.305354, 803 | "=" 804 | ], 805 | [ 806 | 0.421953, 807 | "T" 808 | ], 809 | [ 810 | 0.131762, 811 | "R" 812 | ], 813 | [ 814 | 0.111864, 815 | "U" 816 | ], 817 | [ 818 | 0.112003, 819 | "E" 820 | ], 821 | [ 822 | 0.860947, 823 | "\r\u001b[21d\u001b(B\u001b[0;7mSave modified buffer (ANSWERING \"No\" WILL DESTROY CHANGES) ? \u001b[22;1H Y\u001b(B\u001b[m Yes\u001b[K\r\u001b[23d\u001b(B\u001b[0;7m N\u001b(B\u001b[m No \u001b[23;16H \u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cancel\u001b[K\u001b[21;62H" 824 | ], 825 | [ 826 | 0.20143, 827 | "\r\u001b(B\u001b[0;7mFile Name to Write: secret \r\u001b[22d^G\u001b(B\u001b[m Get Help\u001b[22;24H\u001b(B\u001b[0;7m^T\u001b(B\u001b[m To Files\u001b[22;47H\u001b(B\u001b[0;7mM-M\u001b(B\u001b[m Mac Format\u001b[22;70H\u001b(B\u001b[0;7mM-P\u001b(B\u001b[m Prepend\r\u001b[23d\u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cancel\u001b[17G \u001b(B\u001b[0;7mM-D\u001b(B\u001b[m DOS Format\u001b[23;47H\u001b(B\u001b[0;7mM-A\u001b(B\u001b[m Append\u001b[23;70H\u001b(B\u001b[0;7mM-B\u001b(B\u001b[m Backup File\u001b[21;27H" 828 | ], 829 | [ 830 | 0.500201, 831 | "\r\u001b[22d\u001b[39;49m\u001b(B\u001b[m\u001b[J\u001b[1;83H\u001b(B\u001b[0;7m \u001b[21;38H\u001b(B\u001b[m\u001b[1K \u001b(B\u001b[0;7m[ Wrote 1 line ]\u001b(B\u001b[m\u001b[K\u001b[23;92H\u001b[23;1H\u001b[?1049l\r\u001b[?1l\u001b>" 832 | ], 833 | [ 834 | 0.000726, 835 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 836 | ], 837 | [ 838 | 0.0001, 839 | "\u001b]2;chris@pauling: ~/no-setup\u0007\u001b]1;~/no-setup\u0007" 840 | ], 841 | [ 842 | 0.022293, 843 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 844 | ], 845 | [ 846 | 0.000136, 847 | "\u001b[?1h\u001b=" 848 | ], 849 | [ 850 | 0.000791, 851 | "\u001b[?2004h" 852 | ], 853 | [ 854 | 1.671534, 855 | "\u001b[1m\u001b[31mj\u001b[0m\u001b[39m" 856 | ], 857 | [ 858 | 0.112855, 859 | "\b\u001b[1m\u001b[31mj\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 860 | ], 861 | [ 862 | 0.071778, 863 | "\b\b\u001b[0m\u001b[32mj\u001b[0m\u001b[32ma\u001b[32mk\u001b[39m" 864 | ], 865 | [ 866 | 0.116101, 867 | " " 868 | ], 869 | [ 870 | 0.169086, 871 | "k" 872 | ], 873 | [ 874 | 0.118897, 875 | "e" 876 | ], 877 | [ 878 | 0.119338, 879 | "y" 880 | ], 881 | [ 882 | 0.14859, 883 | "g" 884 | ], 885 | [ 886 | 0.075585, 887 | "e" 888 | ], 889 | [ 890 | 0.08445, 891 | "n" 892 | ], 893 | [ 894 | 0.529119, 895 | "\u001b[?1l\u001b>" 896 | ], 897 | [ 898 | 0.001551, 899 | "\u001b[?2004l\r\r\n" 900 | ], 901 | [ 902 | 0.000448, 903 | "\u001b]2;jak keygen\u0007\u001b]1;jak\u0007" 904 | ], 905 | [ 906 | 0.232799, 907 | "Here is your shiny new key.\r\n\r\n9d3a09c89cc864a63858f337e3d4a6f86463f3e4dac044764bfff4c64f94b203\r\n\r\nRemember to keep this password secret and save it. Without it you will NOT be able\r\nto decrypt any file(s) you encrypt using it.\r\n" 908 | ], 909 | [ 910 | 0.017103, 911 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 912 | ], 913 | [ 914 | 7.4e-05, 915 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 916 | ], 917 | [ 918 | 2e-05, 919 | "\u001b]1;~/no-setup\u0007" 920 | ], 921 | [ 922 | 0.017671, 923 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 924 | ], 925 | [ 926 | 0.000107, 927 | "\u001b[?1h\u001b=" 928 | ], 929 | [ 930 | 0.000352, 931 | "\u001b[?2004h" 932 | ], 933 | [ 934 | 4.905486, 935 | "\u001b[1m\u001b[31mj\u001b[0m\u001b[39m" 936 | ], 937 | [ 938 | 0.159655, 939 | "\b\u001b[1m\u001b[31mj\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 940 | ], 941 | [ 942 | 0.063692, 943 | "\b\b\u001b[0m\u001b[32mj\u001b[0m\u001b[32ma\u001b[32mk\u001b[39m" 944 | ], 945 | [ 946 | 0.125858, 947 | " " 948 | ], 949 | [ 950 | 0.409802, 951 | "e" 952 | ], 953 | [ 954 | 0.108608, 955 | "n" 956 | ], 957 | [ 958 | 0.121179, 959 | "c" 960 | ], 961 | [ 962 | 0.19264, 963 | "r" 964 | ], 965 | [ 966 | 0.055665, 967 | "y" 968 | ], 969 | [ 970 | 0.169302, 971 | "p" 972 | ], 973 | [ 974 | 0.071916, 975 | "t" 976 | ], 977 | [ 978 | 0.125263, 979 | " " 980 | ], 981 | [ 982 | 0.093061, 983 | "\u001b[4ms\u001b[24m" 984 | ], 985 | [ 986 | 0.14654, 987 | "\b\u001b[4ms\u001b[4me\u001b[24m" 988 | ], 989 | [ 990 | 0.184715, 991 | "\b\u001b[4me\u001b[4mc\u001b[24m" 992 | ], 993 | [ 994 | 0.171444, 995 | "\b\u001b[4mc\u001b[4mr\u001b[24m" 996 | ], 997 | [ 998 | 0.068494, 999 | "\b\u001b[4mr\u001b[4me\u001b[24m" 1000 | ], 1001 | [ 1002 | 0.133055, 1003 | "\b\u001b[4me\u001b[4mt\u001b[24m" 1004 | ], 1005 | [ 1006 | 0.266407, 1007 | " " 1008 | ], 1009 | [ 1010 | 0.239889, 1011 | "-" 1012 | ], 1013 | [ 1014 | 0.119826, 1015 | "-" 1016 | ], 1017 | [ 1018 | 0.191418, 1019 | "k" 1020 | ], 1021 | [ 1022 | 0.13619, 1023 | "e" 1024 | ], 1025 | [ 1026 | 0.111793, 1027 | "y" 1028 | ], 1029 | [ 1030 | 0.215655, 1031 | " " 1032 | ], 1033 | [ 1034 | 0.555595, 1035 | "\u001b[25D\u001b[39mj\u001b[39ma\u001b[39mk\u001b[9C\u001b[24ms\u001b[24me\u001b[24mc\u001b[24mr\u001b[24me\u001b[24mt\u001b[7C\u001b[7m9d3a09c89cc864a63858f337e3d4a6f86463f3e4dac044764bfff4\u001b[7mc\u001b[7m64f94b203\u001b[27m\u001b[K" 1036 | ], 1037 | [ 1038 | 1.180152, 1039 | "\u001b[A\u001b[3C\u001b[32mj\u001b[32ma\u001b[32mk\u001b[39m\u001b[9C\u001b[4ms\u001b[4me\u001b[4mc\u001b[4mr\u001b[4me\u001b[4mt\u001b[24m\u001b[7C\u001b[27m9\u001b[27md\u001b[27m3\u001b[27ma\u001b[27m0\u001b[27m9\u001b[27mc\u001b[27m8\u001b[27m9\u001b[27mc\u001b[27mc\u001b[27m8\u001b[27m6\u001b[27m4\u001b[27ma\u001b[27m6\u001b[27m3\u001b[27m8\u001b[27m5\u001b[27m8\u001b[27mf\u001b[27m3\u001b[27m3\u001b[27m7\u001b[27me\u001b[27m3\u001b[27md\u001b[27m4\u001b[27ma\u001b[27m6\u001b[27mf\u001b[27m8\u001b[27m6\u001b[27m4\u001b[27m6\u001b[27m3\u001b[27mf\u001b[27m3\u001b[27me\u001b[27m4\u001b[27md\u001b[27ma\u001b[27mc\u001b[27m0\u001b[27m4\u001b[27m4\u001b[27m7\u001b[27m6\u001b[27m4\u001b[27mb\u001b[27mf\u001b[27mf\u001b[27mf\u001b[27m4c\u001b[27m6\u001b[27m4\u001b[27mf\u001b[27m9\u001b[27m4\u001b[27mb\u001b[27m2\u001b[27m0\u001b[27m3\u001b[?1l\u001b>" 1040 | ], 1041 | [ 1042 | 0.002463, 1043 | "\u001b[?2004l\r\r\n" 1044 | ], 1045 | [ 1046 | 0.000427, 1047 | "\u001b]2;jak encrypt secret --key \u0007\u001b]1;jak\u0007" 1048 | ], 1049 | [ 1050 | 0.232344, 1051 | "/Users/chris/no-setup/secret - is now encrypted.\r\n" 1052 | ], 1053 | [ 1054 | 0.019141, 1055 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 1056 | ], 1057 | [ 1058 | 8.4e-05, 1059 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 1060 | ], 1061 | [ 1062 | 2e-05, 1063 | "\u001b]1;~/no-setup\u0007" 1064 | ], 1065 | [ 1066 | 0.018084, 1067 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 1068 | ], 1069 | [ 1070 | 0.000153, 1071 | "\u001b[?1h\u001b=" 1072 | ], 1073 | [ 1074 | 0.00046, 1075 | "\u001b[?2004h" 1076 | ], 1077 | [ 1078 | 1.334302, 1079 | "\u001b[1m\u001b[31mj\u001b[0m\u001b[39m" 1080 | ], 1081 | [ 1082 | 0.09525, 1083 | "\b\u001b[1m\u001b[31mj\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 1084 | ], 1085 | [ 1086 | 0.079318, 1087 | "\b\b\u001b[0m\u001b[32mj\u001b[0m\u001b[32ma\u001b[32mk\u001b[39m" 1088 | ], 1089 | [ 1090 | 0.088241, 1091 | " " 1092 | ], 1093 | [ 1094 | 0.112959, 1095 | "d" 1096 | ], 1097 | [ 1098 | 0.091531, 1099 | "e" 1100 | ], 1101 | [ 1102 | 0.15452, 1103 | "c" 1104 | ], 1105 | [ 1106 | 0.191428, 1107 | "r" 1108 | ], 1109 | [ 1110 | 0.056301, 1111 | "y" 1112 | ], 1113 | [ 1114 | 0.183346, 1115 | "p" 1116 | ], 1117 | [ 1118 | 0.063083, 1119 | "t" 1120 | ], 1121 | [ 1122 | 0.445275, 1123 | "\b \b" 1124 | ], 1125 | [ 1126 | 0.200243, 1127 | "\b \b" 1128 | ], 1129 | [ 1130 | 0.039073, 1131 | "\b \b" 1132 | ], 1133 | [ 1134 | 0.034134, 1135 | "\b \b" 1136 | ], 1137 | [ 1138 | 0.036451, 1139 | "\b \b" 1140 | ], 1141 | [ 1142 | 0.034666, 1143 | "\b \b" 1144 | ], 1145 | [ 1146 | 0.035282, 1147 | "\b \b" 1148 | ], 1149 | [ 1150 | 0.035747, 1151 | "\b" 1152 | ], 1153 | [ 1154 | 0.034698, 1155 | "\b\b\b\u001b[1m\u001b[31mj\u001b[1m\u001b[31ma\u001b[0m\u001b[39m\u001b[39m \b" 1156 | ], 1157 | [ 1158 | 0.034502, 1159 | "\b\b\u001b[1m\u001b[31mj\u001b[0m\u001b[39m\u001b[0m\u001b[39m \b" 1160 | ], 1161 | [ 1162 | 0.036897, 1163 | "\b\u001b[0m\u001b[39m \b" 1164 | ], 1165 | [ 1166 | 0.18801, 1167 | "\u001b[1m\u001b[31mc\u001b[0m\u001b[39m" 1168 | ], 1169 | [ 1170 | 0.133535, 1171 | "\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 1172 | ], 1173 | [ 1174 | 0.124897, 1175 | "\b\b\u001b[0m\u001b[32mc\u001b[0m\u001b[32ma\u001b[32mt\u001b[39m" 1176 | ], 1177 | [ 1178 | 0.13087, 1179 | " " 1180 | ], 1181 | [ 1182 | 0.087981, 1183 | "\u001b[4ms\u001b[24m" 1184 | ], 1185 | [ 1186 | 0.15426, 1187 | "\b\u001b[4ms\u001b[4me\u001b[24m" 1188 | ], 1189 | [ 1190 | 0.114785, 1191 | "\b\u001b[4me\u001b[4mc\u001b[24m" 1192 | ], 1193 | [ 1194 | 0.167492, 1195 | "\u001b[?7l\u001b[31m......\u001b[39m\u001b[?7h" 1196 | ], 1197 | [ 1198 | 0.028929, 1199 | "\r\r\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[32mcat\u001b[39m \u001b[4msecret\u001b[24m\u001b[1m \u001b[0m\u001b[K\u001b[54C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[67D" 1200 | ], 1201 | [ 1202 | 0.648745, 1203 | "\b\u001b[0m \b\u001b[?1l\u001b>" 1204 | ], 1205 | [ 1206 | 0.001375, 1207 | "\u001b[?2004l\r\r\n" 1208 | ], 1209 | [ 1210 | 0.00049, 1211 | "\u001b]2;cat secret\u0007\u001b]1;cat\u0007" 1212 | ], 1213 | [ 1214 | 0.00373, 1215 | "- - - Encrypted by jak - - -\r\n\r\nNzI2ZjgzNDE5ZmNkMjc1YWVlYTkzNmJhMmJiY2UwNjUwNWI3ZWQwYjdiOTU2\r\nMjM0NTg4MDhmMDhjYTM4YTFjN2ZjOGY5MmIzYWNlNDNkNTA4ZWMwYTk3MjFh\r\nMWRlZjk1MTBkNWU5ODNhNGY1MTk1NDg2YTVkZDNjZjY4NzdkZGQpLMLi_JP3\r\nZPcQbO2IU_LQAQWCJuyoTlSZbRZT9vvGAw==\r\n" 1216 | ], 1217 | [ 1218 | 0.000468, 1219 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 1220 | ], 1221 | [ 1222 | 7.6e-05, 1223 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 1224 | ], 1225 | [ 1226 | 2.7e-05, 1227 | "\u001b]1;~/no-setup\u0007" 1228 | ], 1229 | [ 1230 | 0.024139, 1231 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 1232 | ], 1233 | [ 1234 | 7.6e-05, 1235 | "\u001b[?1h\u001b=" 1236 | ], 1237 | [ 1238 | 0.000324, 1239 | "\u001b[?2004h" 1240 | ], 1241 | [ 1242 | 1.120368, 1243 | "\u001b[1m\u001b[31mj\u001b[0m\u001b[39m" 1244 | ], 1245 | [ 1246 | 0.103843, 1247 | "\b\u001b[1m\u001b[31mj\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 1248 | ], 1249 | [ 1250 | 0.869332, 1251 | "\b\b\u001b[0m\u001b[32mj\u001b[0m\u001b[32ma\u001b[32mk\u001b[39m" 1252 | ], 1253 | [ 1254 | 0.347903, 1255 | " " 1256 | ], 1257 | [ 1258 | 0.242548, 1259 | "d" 1260 | ], 1261 | [ 1262 | 0.076303, 1263 | "e" 1264 | ], 1265 | [ 1266 | 0.1509, 1267 | "c" 1268 | ], 1269 | [ 1270 | 0.183902, 1271 | "r" 1272 | ], 1273 | [ 1274 | 0.064363, 1275 | "y" 1276 | ], 1277 | [ 1278 | 0.175661, 1279 | "p" 1280 | ], 1281 | [ 1282 | 0.06396, 1283 | "t" 1284 | ], 1285 | [ 1286 | 0.132647, 1287 | " " 1288 | ], 1289 | [ 1290 | 0.057057, 1291 | "\u001b[4ms\u001b[24m" 1292 | ], 1293 | [ 1294 | 0.160113, 1295 | "\b\u001b[4ms\u001b[4me\u001b[24m" 1296 | ], 1297 | [ 1298 | 0.151269, 1299 | "\b\u001b[4me\u001b[4mc\u001b[24m" 1300 | ], 1301 | [ 1302 | 0.185405, 1303 | "\b\u001b[4mc\u001b[4mr\u001b[24m" 1304 | ], 1305 | [ 1306 | 0.072446, 1307 | "\b\u001b[4mr\u001b[4me\u001b[24m" 1308 | ], 1309 | [ 1310 | 0.150494, 1311 | "\b\u001b[4me\u001b[4mt\u001b[24m" 1312 | ], 1313 | [ 1314 | 0.144991, 1315 | " " 1316 | ], 1317 | [ 1318 | 0.991005, 1319 | "-" 1320 | ], 1321 | [ 1322 | 0.261215, 1323 | "k" 1324 | ], 1325 | [ 1326 | 0.288679, 1327 | " " 1328 | ], 1329 | [ 1330 | 0.437779, 1331 | "\u001b[22D\u001b[39mj\u001b[39ma\u001b[39mk\u001b[9C\u001b[24ms\u001b[24me\u001b[24mc\u001b[24mr\u001b[24me\u001b[24mt\u001b[4C\u001b[7m9d3a09c89cc864a63858f337e3d4a6f86463f3e4dac044764bfff4c64\u001b[7mf\u001b[7m94b203\u001b[27m\u001b[K" 1332 | ], 1333 | [ 1334 | 1.272084, 1335 | "\u001b[A\u001b[6C\u001b[32mj\u001b[32ma\u001b[32mk\u001b[39m\u001b[9C\u001b[4ms\u001b[4me\u001b[4mc\u001b[4mr\u001b[4me\u001b[4mt\u001b[24m\u001b[4C\u001b[27m9\u001b[27md\u001b[27m3\u001b[27ma\u001b[27m0\u001b[27m9\u001b[27mc\u001b[27m8\u001b[27m9\u001b[27mc\u001b[27mc\u001b[27m8\u001b[27m6\u001b[27m4\u001b[27ma\u001b[27m6\u001b[27m3\u001b[27m8\u001b[27m5\u001b[27m8\u001b[27mf\u001b[27m3\u001b[27m3\u001b[27m7\u001b[27me\u001b[27m3\u001b[27md\u001b[27m4\u001b[27ma\u001b[27m6\u001b[27mf\u001b[27m8\u001b[27m6\u001b[27m4\u001b[27m6\u001b[27m3\u001b[27mf\u001b[27m3\u001b[27me\u001b[27m4\u001b[27md\u001b[27ma\u001b[27mc\u001b[27m0\u001b[27m4\u001b[27m4\u001b[27m7\u001b[27m6\u001b[27m4\u001b[27mb\u001b[27mf\u001b[27mf\u001b[27mf\u001b[27m4\u001b[27mc\u001b[27m6\u001b[27m4f\u001b[27m9\u001b[27m4\u001b[27mb\u001b[27m2\u001b[27m0\u001b[27m3" 1336 | ], 1337 | [ 1338 | 2.4e-05, 1339 | "\u001b[?1l\u001b>" 1340 | ], 1341 | [ 1342 | 0.002416, 1343 | "\u001b[?2004l\r\r\n" 1344 | ], 1345 | [ 1346 | 0.000494, 1347 | "\u001b]2;jak decrypt secret -k \u0007\u001b]1;jak\u0007" 1348 | ], 1349 | [ 1350 | 0.242915, 1351 | "/Users/chris/no-setup/secret - is now decrypted.\r\n" 1352 | ], 1353 | [ 1354 | 0.017841, 1355 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 1356 | ], 1357 | [ 1358 | 0.000154, 1359 | "\u001b]2;chris@pauling: ~/no-setup\u0007\u001b]1;~/no-setup\u0007" 1360 | ], 1361 | [ 1362 | 0.019361, 1363 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D\u001b[?1h\u001b=" 1364 | ], 1365 | [ 1366 | 0.00032, 1367 | "\u001b[?2004h" 1368 | ], 1369 | [ 1370 | 3.244446, 1371 | "\u001b[1m\u001b[31mc\u001b[0m\u001b[39m" 1372 | ], 1373 | [ 1374 | 0.105278, 1375 | "\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 1376 | ], 1377 | [ 1378 | 0.167162, 1379 | "\b\b\u001b[0m\u001b[32mc\u001b[0m\u001b[32ma\u001b[32mt\u001b[39m" 1380 | ], 1381 | [ 1382 | 0.087637, 1383 | " " 1384 | ], 1385 | [ 1386 | 0.112896, 1387 | "\u001b[4ms\u001b[24m" 1388 | ], 1389 | [ 1390 | 0.160846, 1391 | "\b\u001b[4ms\u001b[4me\u001b[24m" 1392 | ], 1393 | [ 1394 | 0.155692, 1395 | "\b\u001b[4me\u001b[4mc\u001b[24m" 1396 | ], 1397 | [ 1398 | 0.160612, 1399 | "\u001b[?7l" 1400 | ], 1401 | [ 1402 | 3.1e-05, 1403 | "\u001b[31m......\u001b[39m\u001b[?7h" 1404 | ], 1405 | [ 1406 | 0.007551, 1407 | "\r\r\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[32mcat\u001b[39m \u001b[4msecret\u001b[24m\u001b[1m \u001b[0m\u001b[K\u001b[54C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[67D" 1408 | ], 1409 | [ 1410 | 0.51991, 1411 | "\b\u001b[0m \b" 1412 | ], 1413 | [ 1414 | 7.1e-05, 1415 | "\u001b[?1l\u001b>" 1416 | ], 1417 | [ 1418 | 0.00139, 1419 | "\u001b[?2004l\r\r\n" 1420 | ], 1421 | [ 1422 | 0.000456, 1423 | "\u001b]2;cat secret\u0007\u001b]1;cat\u0007" 1424 | ], 1425 | [ 1426 | 0.003712, 1427 | "MYSECRET=TRUE\r\n" 1428 | ], 1429 | [ 1430 | 0.00046, 1431 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 1432 | ], 1433 | [ 1434 | 0.00012, 1435 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 1436 | ], 1437 | [ 1438 | 2.2e-05, 1439 | "\u001b]1;~/no-setup\u0007" 1440 | ], 1441 | [ 1442 | 0.021663, 1443 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 1444 | ], 1445 | [ 1446 | 0.000155, 1447 | "\u001b[?1h\u001b=" 1448 | ], 1449 | [ 1450 | 0.000616, 1451 | "\u001b[?2004h" 1452 | ], 1453 | [ 1454 | 0.983305, 1455 | "\u001b[1m\u001b[31mn\u001b[0m\u001b[39m" 1456 | ], 1457 | [ 1458 | 0.088913, 1459 | "\b\u001b[1m\u001b[31mn\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 1460 | ], 1461 | [ 1462 | 0.063938, 1463 | "\b\b\u001b[1m\u001b[31mn\u001b[1m\u001b[31ma\u001b[1m\u001b[31mn\u001b[0m\u001b[39m" 1464 | ], 1465 | [ 1466 | 0.111068, 1467 | "\b\b\b\u001b[0m\u001b[32mn\u001b[0m\u001b[32ma\u001b[0m\u001b[32mn\u001b[32mo\u001b[39m" 1468 | ], 1469 | [ 1470 | 0.124449, 1471 | " " 1472 | ], 1473 | [ 1474 | 0.100617, 1475 | "k" 1476 | ], 1477 | [ 1478 | 0.117627, 1479 | "e" 1480 | ], 1481 | [ 1482 | 0.092235, 1483 | "y" 1484 | ], 1485 | [ 1486 | 0.103005, 1487 | "f" 1488 | ], 1489 | [ 1490 | 0.107934, 1491 | "i" 1492 | ], 1493 | [ 1494 | 0.160031, 1495 | "l" 1496 | ], 1497 | [ 1498 | 0.092651, 1499 | "e" 1500 | ], 1501 | [ 1502 | 1.09531, 1503 | "\u001b[?1l\u001b>" 1504 | ], 1505 | [ 1506 | 0.001374, 1507 | "\u001b[?2004l\r\r\n" 1508 | ], 1509 | [ 1510 | 0.000516, 1511 | "\u001b]2;nano keyfile\u0007\u001b]1;nano\u0007" 1512 | ], 1513 | [ 1514 | 0.004411, 1515 | "\u001b[?1049h\u001b[1;23r\u001b(B\u001b[m\u001b[4l\u001b[?7h\u001b[?12l\u001b[?25h\u001b[?1h\u001b=" 1516 | ], 1517 | [ 1518 | 2.4e-05, 1519 | "\u001b[?1h\u001b=\u001b[?1h\u001b=" 1520 | ], 1521 | [ 1522 | 0.000509, 1523 | "\u001b[39;49m\u001b[39;49m\u001b(B\u001b[m\u001b[H\u001b[2J\u001b(B\u001b[0;7m GNU nano 2.0.6 File: keyfile \u001b[21;41H[ New File ]\r\u001b[22d^G\u001b(B\u001b[m Get Help \u001b(B\u001b[0;7m^O\u001b(B\u001b[m WriteOut \u001b(B\u001b[0;7m^R\u001b(B\u001b[m Read File \u001b(B\u001b[0;7m^Y\u001b(B\u001b[m Prev Page \u001b(B\u001b[0;7m^K\u001b(B\u001b[m Cut Text \u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cur Pos\r\u001b[23d\u001b(B\u001b[0;7m^X\u001b(B\u001b[m Exit\u001b[23;16H\u001b(B\u001b[0;7m^J\u001b(B\u001b[m Justify \u001b(B\u001b[0;7m^W\u001b(B\u001b[m Where Is \u001b(B\u001b[0;7m^V\u001b(B\u001b[m Next Page \u001b(B\u001b[0;7m^U\u001b(B\u001b[m UnCut Text \u001b(B\u001b[0;7m^T\u001b(B\u001b[m To Spell\r\u001b[3d" 1524 | ], 1525 | [ 1526 | 1.004874, 1527 | "\u001b[1;83H\u001b(B\u001b[0;7mModified\r\u001b[21d\u001b(B\u001b[m\u001b[K\u001b[3d9d3a09c89cc864a63858f337e3d4a6f86463f3e4dac044764bfff4c64f94b203" 1528 | ], 1529 | [ 1530 | 1.135925, 1531 | "\r\u001b[21d\u001b(B\u001b[0;7mSave modified buffer (ANSWERING \"No\" WILL DESTROY CHANGES) ? \u001b[22;1H Y\u001b(B\u001b[m Yes\u001b[K\r\u001b[23d\u001b(B\u001b[0;7m N\u001b(B\u001b[m No \u001b[23;16H \u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cancel\u001b[K\u001b[21;62H" 1532 | ], 1533 | [ 1534 | 0.813772, 1535 | "\r\u001b(B\u001b[0;7mFile Name to Write: keyfile \r\u001b[22d^G\u001b(B\u001b[m Get Help\u001b[22;24H\u001b(B\u001b[0;7m^T\u001b(B\u001b[m To Files\u001b[22;47H\u001b(B\u001b[0;7mM-M\u001b(B\u001b[m Mac Format\u001b[22;70H\u001b(B\u001b[0;7mM-P\u001b(B\u001b[m Prepend\r\u001b[23d\u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cancel\u001b[17G \u001b(B\u001b[0;7mM-D\u001b(B\u001b[m DOS Format\u001b[23;47H\u001b(B\u001b[0;7mM-A\u001b(B\u001b[m Append\u001b[23;70H\u001b(B\u001b[0;7mM-B\u001b(B\u001b[m Backup File\u001b[21;28H" 1536 | ], 1537 | [ 1538 | 0.902561, 1539 | "\r\u001b[22d\u001b[39;49m\u001b(B\u001b[m\u001b[J\u001b[1;83H\u001b(B\u001b[0;7m \u001b[21;38H\u001b(B\u001b[m\u001b[1K \u001b(B\u001b[0;7m[ Wrote 1 line ]\u001b(B\u001b[m\u001b[K\u001b[23;92H\u001b[23;1H\u001b[?1049l\r\u001b[?1l\u001b>" 1540 | ], 1541 | [ 1542 | 0.000712, 1543 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 1544 | ], 1545 | [ 1546 | 0.000148, 1547 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 1548 | ], 1549 | [ 1550 | 2.3e-05, 1551 | "\u001b]1;~/no-setup\u0007" 1552 | ], 1553 | [ 1554 | 0.018945, 1555 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 1556 | ], 1557 | [ 1558 | 0.000124, 1559 | "\u001b[?1h\u001b=" 1560 | ], 1561 | [ 1562 | 0.000339, 1563 | "\u001b[?2004h" 1564 | ], 1565 | [ 1566 | 3.046174, 1567 | "\u001b[1m\u001b[31mj\u001b[0m\u001b[39m" 1568 | ], 1569 | [ 1570 | 0.079504, 1571 | "\b\u001b[1m\u001b[31mj\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 1572 | ], 1573 | [ 1574 | 0.11155, 1575 | "\b\b\u001b[0m\u001b[32mj\u001b[0m\u001b[32ma\u001b[32mk\u001b[39m" 1576 | ], 1577 | [ 1578 | 0.183074, 1579 | " " 1580 | ], 1581 | [ 1582 | 0.134708, 1583 | "e" 1584 | ], 1585 | [ 1586 | 0.080199, 1587 | "n" 1588 | ], 1589 | [ 1590 | 0.107299, 1591 | "c" 1592 | ], 1593 | [ 1594 | 0.214198, 1595 | "r" 1596 | ], 1597 | [ 1598 | 0.051809, 1599 | "y" 1600 | ], 1601 | [ 1602 | 0.16903, 1603 | "p" 1604 | ], 1605 | [ 1606 | 0.060038, 1607 | "t" 1608 | ], 1609 | [ 1610 | 0.112823, 1611 | " " 1612 | ], 1613 | [ 1614 | 0.105219, 1615 | "\u001b[4ms\u001b[24m" 1616 | ], 1617 | [ 1618 | 0.132733, 1619 | "\b\u001b[4ms\u001b[4me\u001b[24m" 1620 | ], 1621 | [ 1622 | 0.14952, 1623 | "\b\u001b[4me\u001b[4mc\u001b[24m" 1624 | ], 1625 | [ 1626 | 0.202756, 1627 | "\b\u001b[4mc\u001b[4mr\u001b[24m" 1628 | ], 1629 | [ 1630 | 0.053344, 1631 | "\b\u001b[4mr\u001b[4me\u001b[24m" 1632 | ], 1633 | [ 1634 | 0.143098, 1635 | "\b\u001b[4me\u001b[4mt\u001b[24m" 1636 | ], 1637 | [ 1638 | 0.132339, 1639 | " " 1640 | ], 1641 | [ 1642 | 0.335377, 1643 | "-" 1644 | ], 1645 | [ 1646 | 0.116495, 1647 | "-" 1648 | ], 1649 | [ 1650 | 0.155009, 1651 | "k" 1652 | ], 1653 | [ 1654 | 0.118492, 1655 | "e" 1656 | ], 1657 | [ 1658 | 0.095274, 1659 | "y" 1660 | ], 1661 | [ 1662 | 0.094428, 1663 | "f" 1664 | ], 1665 | [ 1666 | 0.100772, 1667 | "i" 1668 | ], 1669 | [ 1670 | 0.164793, 1671 | "l" 1672 | ], 1673 | [ 1674 | 0.099078, 1675 | "e" 1676 | ], 1677 | [ 1678 | 0.118032, 1679 | " " 1680 | ], 1681 | [ 1682 | 0.498396, 1683 | "\u001b[4mk\u001b[24m" 1684 | ], 1685 | [ 1686 | 0.089748, 1687 | "\b\u001b[4mk\u001b[4me\u001b[24m" 1688 | ], 1689 | [ 1690 | 0.153395, 1691 | "\b\u001b[4me\u001b[4my\u001b[24m" 1692 | ], 1693 | [ 1694 | 0.188954, 1695 | "\u001b[?7l\u001b[31m......\u001b[39m" 1696 | ], 1697 | [ 1698 | 3.1e-05, 1699 | "\u001b[?7h" 1700 | ], 1701 | [ 1702 | 0.007115, 1703 | "\r\r\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[32mjak\u001b[39m encrypt \u001b[4msecret\u001b[24m --keyfile \u001b[4mkeyfile\u001b[24m\u001b[1m \u001b[0m\u001b[K\u001b[28C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[41D" 1704 | ], 1705 | [ 1706 | 0.887697, 1707 | "\b\u001b[0m \b" 1708 | ], 1709 | [ 1710 | 3.2e-05, 1711 | "\u001b[?1l\u001b>" 1712 | ], 1713 | [ 1714 | 0.002499, 1715 | "\u001b[?2004l\r\r\n" 1716 | ], 1717 | [ 1718 | 0.000545, 1719 | "\u001b]2;jak encrypt secret --keyfile keyfile\u0007\u001b]1;jak\u0007" 1720 | ], 1721 | [ 1722 | 0.229064, 1723 | "/Users/chris/no-setup/secret - is now encrypted.\r\n" 1724 | ], 1725 | [ 1726 | 0.017584, 1727 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 1728 | ], 1729 | [ 1730 | 0.000185, 1731 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 1732 | ], 1733 | [ 1734 | 2.1e-05, 1735 | "\u001b]1;~/no-setup\u0007" 1736 | ], 1737 | [ 1738 | 0.01751, 1739 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 1740 | ], 1741 | [ 1742 | 0.000235, 1743 | "\u001b[?1h\u001b=" 1744 | ], 1745 | [ 1746 | 0.000384, 1747 | "\u001b[?2004h" 1748 | ], 1749 | [ 1750 | 0.720082, 1751 | "\u001b[1m\u001b[31mc\u001b[0m\u001b[39m" 1752 | ], 1753 | [ 1754 | 0.188747, 1755 | "\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 1756 | ], 1757 | [ 1758 | 0.107497, 1759 | "\b\b\u001b[0m\u001b[32mc\u001b[0m\u001b[32ma\u001b[32mt\u001b[39m" 1760 | ], 1761 | [ 1762 | 0.080116, 1763 | " " 1764 | ], 1765 | [ 1766 | 0.580471, 1767 | "\u001b[4ms\u001b[24m" 1768 | ], 1769 | [ 1770 | 0.178264, 1771 | "\b\u001b[4ms\u001b[4me\u001b[24m" 1772 | ], 1773 | [ 1774 | 0.129867, 1775 | "\b\u001b[4me\u001b[4mc\u001b[24m" 1776 | ], 1777 | [ 1778 | 0.163423, 1779 | "\u001b[?7l\u001b[31m......\u001b[39m\u001b[?7h" 1780 | ], 1781 | [ 1782 | 0.007038, 1783 | "\r\r\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[32mcat\u001b[39m \u001b[4msecret\u001b[24m\u001b[1m \u001b[0m\u001b[K\u001b[54C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[67D" 1784 | ], 1785 | [ 1786 | 0.605562, 1787 | "\b\u001b[0m \b\u001b[?1l\u001b>" 1788 | ], 1789 | [ 1790 | 0.001378, 1791 | "\u001b[?2004l\r\r\n" 1792 | ], 1793 | [ 1794 | 0.000503, 1795 | "\u001b]2;cat secret\u0007\u001b]1;cat\u0007" 1796 | ], 1797 | [ 1798 | 0.003782, 1799 | "- - - Encrypted by jak - - -\r\n\r\nNzI2ZjgzNDE5ZmNkMjc1YWVlYTkzNmJhMmJiY2UwNjUwNWI3ZWQwYjdiOTU2\r\nMjM0NTg4MDhmMDhjYTM4YTFjN2ZjOGY5MmIzYWNlNDNkNTA4ZWMwYTk3MjFh\r\nMWRlZjk1MTBkNWU5ODNhNGY1MTk1NDg2YTVkZDNjZjY4NzdkZGQpLMLi_JP3\r\nZPcQbO2IU_LQAQWCJuyoTlSZbRZT9vvGAw==\r\n" 1800 | ], 1801 | [ 1802 | 0.000536, 1803 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 1804 | ], 1805 | [ 1806 | 0.000138, 1807 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 1808 | ], 1809 | [ 1810 | 1.9e-05, 1811 | "\u001b]1;~/no-setup\u0007" 1812 | ], 1813 | [ 1814 | 0.01958, 1815 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 1816 | ], 1817 | [ 1818 | 5.4e-05, 1819 | "\u001b[?1h\u001b=" 1820 | ], 1821 | [ 1822 | 0.000419, 1823 | "\u001b[?2004h" 1824 | ], 1825 | [ 1826 | 3.270229, 1827 | "\u001b[1m\u001b[31mj\u001b[0m\u001b[39m" 1828 | ], 1829 | [ 1830 | 0.161874, 1831 | "\b\u001b[1m\u001b[31mj\u001b[1m\u001b[31ma\u001b[0m\u001b[39m" 1832 | ], 1833 | [ 1834 | 0.075791, 1835 | "\b\b\u001b[0m\u001b[32mj\u001b[0m\u001b[32ma\u001b[32mk\u001b[39m" 1836 | ], 1837 | [ 1838 | 0.149015, 1839 | " " 1840 | ], 1841 | [ 1842 | 1.128934, 1843 | "d" 1844 | ], 1845 | [ 1846 | 0.083386, 1847 | "e" 1848 | ], 1849 | [ 1850 | 0.168272, 1851 | "c" 1852 | ], 1853 | [ 1854 | 0.198363, 1855 | "r" 1856 | ], 1857 | [ 1858 | 0.05528, 1859 | "y" 1860 | ], 1861 | [ 1862 | 0.189671, 1863 | "p" 1864 | ], 1865 | [ 1866 | 0.065029, 1867 | "t" 1868 | ], 1869 | [ 1870 | 0.108924, 1871 | " " 1872 | ], 1873 | [ 1874 | 0.136814, 1875 | "\u001b[4ms\u001b[24m" 1876 | ], 1877 | [ 1878 | 0.175658, 1879 | "\b\u001b[4ms\u001b[4me\u001b[24m" 1880 | ], 1881 | [ 1882 | 0.15151, 1883 | "\b\u001b[4me\u001b[4mc\u001b[24m" 1884 | ], 1885 | [ 1886 | 0.199183, 1887 | "\b\u001b[4mc\u001b[4mr\u001b[24m" 1888 | ], 1889 | [ 1890 | 0.065138, 1891 | "\b\u001b[4mr\u001b[4me\u001b[24m" 1892 | ], 1893 | [ 1894 | 0.125458, 1895 | "\b\u001b[4me\u001b[4mt\u001b[24m" 1896 | ], 1897 | [ 1898 | 0.396127, 1899 | " " 1900 | ], 1901 | [ 1902 | 0.27511, 1903 | "-" 1904 | ], 1905 | [ 1906 | 0.139087, 1907 | "-" 1908 | ], 1909 | [ 1910 | 0.222485, 1911 | "k" 1912 | ], 1913 | [ 1914 | 0.140202, 1915 | "f" 1916 | ], 1917 | [ 1918 | 0.123249, 1919 | " " 1920 | ], 1921 | [ 1922 | 0.542302, 1923 | "\b" 1924 | ], 1925 | [ 1926 | 0.127686, 1927 | "\b \b" 1928 | ], 1929 | [ 1930 | 0.125952, 1931 | "\b \b" 1932 | ], 1933 | [ 1934 | 0.31199, 1935 | "\b \b" 1936 | ], 1937 | [ 1938 | 0.22009, 1939 | "k" 1940 | ], 1941 | [ 1942 | 0.099966, 1943 | "f" 1944 | ], 1945 | [ 1946 | 0.1047, 1947 | " " 1948 | ], 1949 | [ 1950 | 0.527992, 1951 | "\u001b[4ms\u001b[24m" 1952 | ], 1953 | [ 1954 | 0.244151, 1955 | "\b\u001b[24m \b" 1956 | ], 1957 | [ 1958 | 0.174664, 1959 | "\u001b[4mk\u001b[24m" 1960 | ], 1961 | [ 1962 | 0.09254, 1963 | "\b\u001b[4mk\u001b[4me\u001b[24m" 1964 | ], 1965 | [ 1966 | 0.141484, 1967 | "\b\u001b[4me\u001b[4my\u001b[24m" 1968 | ], 1969 | [ 1970 | 0.158288, 1971 | "\u001b[?7l\u001b[31m......\u001b[39m\u001b[?7h" 1972 | ], 1973 | [ 1974 | 0.006553, 1975 | "\r\r\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[32mjak\u001b[39m decrypt \u001b[4msecret\u001b[24m -kf \u001b[4mkeyfile\u001b[24m\u001b[1m \u001b[0m\u001b[K\u001b[34C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[47D" 1976 | ], 1977 | [ 1978 | 0.792212, 1979 | "\b\u001b[0m \b" 1980 | ], 1981 | [ 1982 | 2.5e-05, 1983 | "\u001b[?1l\u001b>" 1984 | ], 1985 | [ 1986 | 0.002396, 1987 | "\u001b[?2004l\r\r\n" 1988 | ], 1989 | [ 1990 | 0.001502, 1991 | "\u001b]2;jak decrypt secret -kf keyfile\u0007\u001b]1;jak\u0007" 1992 | ], 1993 | [ 1994 | 0.222998, 1995 | "/Users/chris/no-setup/secret - is now decrypted.\r\n" 1996 | ], 1997 | [ 1998 | 0.02058, 1999 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 2000 | ], 2001 | [ 2002 | 8.6e-05, 2003 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 2004 | ], 2005 | [ 2006 | 2e-05, 2007 | "\u001b]1;~/no-setup\u0007" 2008 | ], 2009 | [ 2010 | 0.02301, 2011 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 2012 | ], 2013 | [ 2014 | 0.000142, 2015 | "\u001b[?1h\u001b=" 2016 | ], 2017 | [ 2018 | 0.000347, 2019 | "\u001b[?2004h" 2020 | ], 2021 | [ 2022 | 2.363609, 2023 | "\u001b[1m\u001b[31me\u001b[0m\u001b[39m" 2024 | ], 2025 | [ 2026 | 0.187545, 2027 | "\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mc\u001b[0m\u001b[39m" 2028 | ], 2029 | [ 2030 | 0.091443, 2031 | "\b\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mc\u001b[1m\u001b[31mh\u001b[0m\u001b[39m" 2032 | ], 2033 | [ 2034 | 0.167135, 2035 | "\b\b\b\u001b[0m\u001b[32me\u001b[0m\u001b[32mc\u001b[0m\u001b[32mh\u001b[32mo\u001b[39m" 2036 | ], 2037 | [ 2038 | 0.239036, 2039 | " " 2040 | ], 2041 | [ 2042 | 1.076759, 2043 | "\u001b[33m\"\u001b[39m" 2044 | ], 2045 | [ 2046 | 0.127814, 2047 | "\b\u001b[33m\"\u001b[33m\"\u001b[39m" 2048 | ], 2049 | [ 2050 | 0.25973, 2051 | "\b" 2052 | ], 2053 | [ 2054 | 1.135843, 2055 | "\u001b[33mB\u001b[33m\"\u001b[39m\b" 2056 | ], 2057 | [ 2058 | 0.107255, 2059 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 2060 | ], 2061 | [ 2062 | 0.144003, 2063 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 2064 | ], 2065 | [ 2066 | 0.172353, 2067 | "\u001b[33mm\u001b[33m\"\u001b[39m\b" 2068 | ], 2069 | [ 2070 | 0.360097, 2071 | "\u001b[34m!\u001b[39m\u001b[33m\"\u001b[39m\b" 2072 | ], 2073 | [ 2074 | 0.889784, 2075 | "\b\u001b[33m\"\u001b[39m\u001b[39m \b\b" 2076 | ], 2077 | [ 2078 | 0.712961, 2079 | "\u001b[33m,\u001b[33m\"\u001b[39m\b" 2080 | ], 2081 | [ 2082 | 0.193543, 2083 | "\u001b[33m \u001b[33m\"\u001b[39m\b" 2084 | ], 2085 | [ 2086 | 0.201159, 2087 | "\u001b[33mP\u001b[33m\"\u001b[39m\b" 2088 | ], 2089 | [ 2090 | 0.103248, 2091 | "\u001b[33mr\u001b[33m\"\u001b[39m\b" 2092 | ], 2093 | [ 2094 | 0.09185, 2095 | "\u001b[33mo\u001b[33m\"\u001b[39m\b" 2096 | ], 2097 | [ 2098 | 0.104193, 2099 | "\u001b[33mf\u001b[33m\"\u001b[39m\b" 2100 | ], 2101 | [ 2102 | 0.088034, 2103 | "\u001b[33mi\u001b[33m\"\u001b[39m\b" 2104 | ], 2105 | [ 2106 | 0.111604, 2107 | "\u001b[33mt\u001b[33m\"\u001b[39m\b" 2108 | ], 2109 | [ 2110 | 0.164272, 2111 | "\u001b[33m.\u001b[33m\"\u001b[39m\b" 2112 | ], 2113 | [ 2114 | 0.74319, 2115 | "\u001b[?1l\u001b>" 2116 | ], 2117 | [ 2118 | 0.001511, 2119 | "\u001b[?2004l\r\r\n" 2120 | ], 2121 | [ 2122 | 0.000533, 2123 | "\u001b]2;echo \"Boom, Profit.\"\u0007\u001b]1;echo\u0007" 2124 | ], 2125 | [ 2126 | 2.8e-05, 2127 | "Boom, Profit.\r\n" 2128 | ], 2129 | [ 2130 | 1.4e-05, 2131 | "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r" 2132 | ], 2133 | [ 2134 | 0.000114, 2135 | "\u001b]2;chris@pauling: ~/no-setup\u0007" 2136 | ], 2137 | [ 2138 | 2.7e-05, 2139 | "\u001b]1;~/no-setup\u0007" 2140 | ], 2141 | [ 2142 | 0.022685, 2143 | "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[38;5;237m------------------------------------------------------------\u001b[00m\r\n\u001b[38;5;032m~/no-setup \u001b[38;5;105m»\u001b[00m \u001b[K\u001b[65C\u001b[38;5;237mchris@pauling\u001b[00m\u001b[78D" 2144 | ], 2145 | [ 2146 | 0.000191, 2147 | "\u001b[?1h\u001b=" 2148 | ], 2149 | [ 2150 | 0.000334, 2151 | "\u001b[?2004h" 2152 | ], 2153 | [ 2154 | 3.655326, 2155 | "\u001b[?2004l\r\r\n" 2156 | ] 2157 | ] 2158 | } --------------------------------------------------------------------------------