├── 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 | }
--------------------------------------------------------------------------------