├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── run-bats-core-tests.yml ├── CHANGELOG.md ├── INSTALL.md ├── LICENSE ├── README.md ├── contrib ├── bash │ └── transcrypt ├── packaging │ └── pacman │ │ ├── .gitignore │ │ └── PKGBUILD └── zsh │ └── _transcrypt ├── man ├── transcrypt.1 └── transcrypt.1.ronn ├── sensitive_file ├── tests ├── _test_helper.bash ├── test_cleanup.bats ├── test_contexts.bats ├── test_crypt.bats ├── test_init.bats ├── test_merge.bats ├── test_not_inited.bats └── test_pre_commit.bats └── transcrypt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.md] 10 | indent_size = 2 11 | indent_style = space 12 | trim_trailing_whitespace = false 13 | 14 | [transcrypt] 15 | indent_style = tab 16 | tab_width = 4 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | sensitive_file filter=crypt diff=crypt merge=crypt 2 | -------------------------------------------------------------------------------- /.github/workflows/run-bats-core-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | # Only run tests on push to main branch 5 | push: 6 | branches: [main] 7 | # Run tests for all pull request changes targeting main 8 | pull_request: 9 | branches: "**" 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-24.04 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | # https://github.com/luizm/action-sh-checker 19 | - name: Run shellcheck and shfmt 20 | uses: luizm/action-sh-checker@master 21 | with: 22 | sh_checker_exclude: tests 23 | sh_checker_comment: true 24 | 25 | test: 26 | # Test on older Ubuntu with OpenSSL < 1.1 and newer with OpenSSL >= 1.1 27 | runs-on: ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | os: 31 | [ 32 | # ubuntu 20.04 runner was deprecated early 2025 33 | # ubuntu-20.04, 34 | ubuntu-22.04, 35 | ubuntu-24.04, 36 | ubuntu-latest, 37 | # macOS 12 runner was deprecated late 2024 38 | # macos-12, 39 | macos-13, 40 | macos-14, 41 | macos-latest, 42 | ] 43 | 44 | steps: 45 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 46 | - uses: actions/checkout@v2 47 | 48 | - name: Print bash version 49 | run: bash --version 50 | 51 | - name: Print OpenSSL version 52 | run: openssl version 53 | 54 | - name: Print Git version 55 | run: git version 56 | 57 | # Configure default Git branch name to suppress hint warnings 58 | - name: Configure default Git branch to "main" 59 | run: git config --global init.defaultBranch main 60 | 61 | - name: Install and set up bats-core 62 | run: | 63 | git clone https://github.com/bats-core/bats-core.git /tmp/bats-core-repo 64 | mkdir -p /tmp/bats-core 65 | bash /tmp/bats-core-repo/install.sh /tmp/bats-core 66 | 67 | - name: Run tests 68 | run: /tmp/bats-core/bin/bats tests/ 69 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for transcrypt 2 | 3 | All notable changes to the project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog][1], and this project adheres to 6 | [Semantic Versioning][2]. 7 | 8 | [1]: https://keepachangelog.com/en/1.0.0/ 9 | [2]: https://semver.org/spec/v2.0.0.html 10 | 11 | ## Steps to Upgrade 12 | 13 | To upgrade _transcrypt_ it is not enough to have a newer version on your 14 | system, you must also run the `--upgrade` command in each repository: 15 | 16 | 1. Check the version of _transcrypt_ on your system: 17 | 18 | ```bash 19 | $ transcrypt --version 20 | ``` 21 | 22 | 2. Check the version of _transcrypt_ in your Git repository, which may be 23 | different: 24 | 25 | ```bash 26 | $ .git/crypt/transcrypt --version 27 | ``` 28 | 29 | 3. Upgrade the version of _transcrypt_ in your Git repository: 30 | 31 | ``` 32 | $ transcrypt --upgrade 33 | ``` 34 | 35 | ## [Unreleased] 36 | 37 | ### Added 38 | 39 | - New commands make it easier to add file patterns to .gitattributes: 40 | `transcrypt --add` and `git add-crypt` (#125) 41 | 42 | ### Changed 43 | 44 | - Improve check for incorrect password to avoid false report when transcrypt 45 | init is run with --force in a repo containing dirty files & add tests (#196) 46 | - Greatly improve performance in a repository with many files for pre-commit 47 | safety check, encrypted file listing, and showing raw file (#193) 48 | 49 | ### Fixed 50 | 51 | - Fix pre-commit hook to use "fast" multi-threaded mode for Bash versions 5+ as 52 | well as 4.4+, and even if some encrypted files are empty (#197) 53 | - Don't show message about unexpected dirty files after a `--rekey` because 54 | dirty files are expected (#201) 55 | - Show useful error instead of `unbound variable` when --long option is missing 56 | a value, e.g. `transcrypt --context` 57 | 58 | ## [2.3.1] - 2025-02-24 59 | 60 | ### Fixed 61 | 62 | - Warn when password is probably incorrect by returning an error message and 63 | return code if repo has dirty files after init (#182) 64 | - Fail with error when an empty password is provided to the -p or --password 65 | options (#188) 66 | - Fix handling of double-quotes in encrypted file names (#173) 67 | - Make --upgrade safer by failing fast if transcrypt config cannot be read 68 | (#189) 69 | - Fix --export-gpg command to properly include cipher in exported .asc file 70 | 71 | ## [2.3.0] - 2024-09-10 72 | 73 | ### Added 74 | 75 | - Add contexts feature that lets you encrypt different sets of files with 76 | different passwords for a different audience, such as super-users versus 77 | normal repository users. See `--context=` / `-C` / `--list-context` arguments 78 | and documentation for this advanced feature. 79 | - When transcrypt refuses to do work in a dirty repository, print a list of 80 | changed files to help the user understand and fix the issue. 81 | 82 | ### Fixed 83 | 84 | - Prevent `cd` commands printing out excess details when `CDPATH` is set (#156) 85 | - Fix `--flush` command to work with contexts (#175) 86 | - Fix unbound variable error using `$GIT_REFLOG_ACTION` (issue #150) 87 | 88 | ### Changed 89 | 90 | - Remove hard dependency on `xxd` which is often a heavy requirement because it 91 | is only available with Vim on some platforms. Fall back to `printf` with full 92 | %b support or `perl` when either of these are available, and only require 93 | `xxd` when it is the only viable option (#181) 94 | - Prevent global options set in `GREP_OPTIONS` enviroment variable from 95 | breaking transcrypt's use of grep (#166) 96 | - If `CDPATH` is set then cd will print the path (#156) 97 | - Centralise load and save of password into functions (#141) 98 | 99 | ## [2.2.3] - 2023-03-09 100 | 101 | ### Fixed 102 | 103 | - Revert faulty automatic fix for mistakenly double-salted encrypted files, 104 | which caused more problems than it solved by preventing decryption of some 105 | files on some systems #158 106 | 107 | ### Changed 108 | 109 | - The `hexdump` command is no longer required by Transcrypt. 110 | 111 | ## [2.2.2] - 2023-03-01 112 | 113 | ### Changed 114 | 115 | - The `hexdump` command is now required by Transcrypt. It will be installed 116 | already on many systems, or comes with the `bsdmainutils` package on 117 | Ubuntu/Debian that was already required to get the `column` command. 118 | 119 | ### Fixed 120 | 121 | - Avoid null byte warnings when decrypting certain files, caused by a work- 122 | around in 2.2.1 to repair files that could have been incorrectly encrypted 123 | with 2.2.0 due to issue #147 124 | 125 | ## [2.2.1] - 2023-02-11 126 | 127 | ### Fixed 128 | 129 | - Compatibility fix for LibreSSL versions 3 (and above) especially for MacOS 130 | 13 Ventura, to more carefully apply a work-around required for OpenSSL 3+ 131 | that isn't required for LibreSSL 3+ (#147 #133) 132 | - Fix errors applying a stash containing a secret file that needs to be merged 133 | with staged changes to the same file (#150) 134 | 135 | ## [2.2.0] - 2022-07-09 136 | 137 | ### Added 138 | 139 | - Add `--set-openssl-path` option to configure transcrypt to use a specific 140 | openssl version instead of the default version found in `$PATH`. This will be 141 | most useful to macOS users who might want to use a newer version of OpenSSL. 142 | This option can be used on init, on upgrade, or by itself. 143 | - Add support for an optional `transcrypt.crypt-dir` setting for advanced users 144 | to override the path of the _.git/crypt/_ directory to permit things like 145 | installing transcrypt in a repository on a device without execute 146 | permissions (#104) 147 | 148 | ### Changed 149 | 150 | - No longer need stand-alone scripts for git operations `clean`, `smudge`, 151 | `textconv`, and `merge` in the repository's _crypt/_ directory; the single 152 | consolidated `transcrypt` script is stored there instead. 153 | 154 | ### Fixed 155 | 156 | - Remain compatible with OpenSSL versions 3 and above which changes the way 157 | explicit salt values are expressed in ciphertext, requires `xxd` command (#133) 158 | - Ensure Git index is up-to-date before checking for dirty repo, to avoid 159 | failures seen in CI systems where the repo seems dirty when it isn't. (#37) 160 | - Respect Git `core.hooksPath` setting when installing the pre-commit hook. (#104) 161 | - Zsh completion. (#107) 162 | - Fix salt generation for partial (patch) commits (#118) 163 | - Improve command hint to fix secret files not encrypted in index (#120) 164 | - Fix handling of files with null in first 8 bytes (#116) 165 | 166 | ## [2.1.0] - 2020-09-07 167 | 168 | This release includes features to make it easier and safer to use transcrypt, in 169 | particular: fix merge of encrypted files with conflicts, preventing accidental 170 | commit of plain text files by incompatible Git tools, and upgrade easily with 171 | `--upgrade`. 172 | 173 | ### Steps to Upgrade 174 | 175 | 1. Make sure you are running the latest version of _transcrypt_: 176 | 177 | ``` 178 | $ transcrypt --version 179 | ``` 180 | 181 | 2. Upgrade a repository: 182 | 183 | ``` 184 | $ transcrypt --upgrade 185 | ``` 186 | 187 | 3. Enable the merge handling fix by adding `merge=crypt` to the end of each 188 | _transcrypt_ pattern in `.gitattribute`, to look like this: 189 | 190 | ``` 191 | sensitive_file filter=crypt diff=crypt merge=crypt 192 | ``` 193 | 194 | ### Added 195 | 196 | - Add `--upgrade` command to apply the latest transcrypt scripts in an already 197 | configured repository without the need to re-apply existing settings. 198 | - Install a Git pre-commit hook to reject accidental commit of unencrypted plain 199 | text version of sensitive files, which could otherwise happen if a tool does 200 | not respect the `.gitattribute` filters Transcrypt needs to do its job. 201 | 202 | ### Changed 203 | 204 | - Add a functional test suite built on 205 | [bats-core](https://github.com/bats-core/bats-core#installation). 206 | - Apply Continuous Integration: run functional tests with GitHub Actions. 207 | - Fix [EditorConfig](https://editorconfig.org/) file config for Markdown files. 208 | - Add [CHANGELOG.md](CHANGELOG.md) file to make it easier to find notes about 209 | project changes (see also Release) 210 | 211 | ### Fixed 212 | 213 | - Fix handling of branch merges with conflicts in encrypted files, which would 214 | previously leave the user to manually merge files with a mix of encrypted and 215 | unencrypted content. (#69, #8, #23, #67) 216 | - Remove any cached unencrypted files from Git's object database when 217 | credentials are removed from a repository with a flush or uninstall, so 218 | sensitive file data does not remain accessible in a surprising way. (#74) 219 | - Fix handling of sensitive files with non-ASCII file names, such as extended 220 | Unicode characters. (#78) 221 | - transcrypt `--version` and `--help` commands now work when run outside a Git 222 | repository. (#68) 223 | - The `--list` command now works in a repository that has not yet been init-ed. 224 | 225 | ## [2.0.0] - 2019-07-20 226 | 227 | **\*\*\* WARNING: Re-encryption will be required when updating to version 2.0.0! 228 | \*\*\*** 229 | 230 | This is not a security issue, but the result of a 231 | [bug fix](https://github.com/elasticdog/transcrypt/pull/57) to ensure that the 232 | salt generation is consistent across all operating systems. Once someone on your 233 | team updates to version 2.0.0, it will manifest as the encrypted files in your 234 | repository showing as _changed_. You should ensure that all users upgrade at the 235 | same time...since `transcrypt` itself is small, it may make sense to commit the 236 | script directly into your repo to maintain consistency moving forward. 237 | 238 | ### Steps to Re-encrypt 239 | 240 | After you've upgraded to v2.0.0... 241 | 242 | 1. Display the current config so you can reference the command to re-initialize 243 | things: 244 | 245 | ``` 246 | $ transcrypt --display 247 | The current repository was configured using transcrypt version 1.1.0 248 | and has the following configuration: 249 | 250 | GIT_WORK_TREE: /home/elasticdog/src/transcrypt 251 | GIT_DIR: /home/elasticdog/src/transcrypt/.git 252 | GIT_ATTRIBUTES: /home/elasticdog/src/transcrypt/.gitattributes 253 | 254 | CIPHER: aes-256-cbc 255 | PASSWORD: correct horse battery staple 256 | 257 | Copy and paste the following command to initialize a cloned repository: 258 | 259 | transcrypt -c aes-256-cbc -p 'correct horse battery staple' 260 | ``` 261 | 262 | 2. Flush the credentials and re-configure the repo with the same settings as 263 | above: 264 | 265 | ``` 266 | $ transcrypt --flush-credentials 267 | $ transcrypt -c aes-256-cbc -p 'correct horse battery staple' 268 | ``` 269 | 270 | 3. Now that all of the appropriate files have been re-encrypted, add them and 271 | commit the changes: 272 | ``` 273 | $ git add -- $(transcrypt --list) 274 | $ git commit --message="Re-encrypt files protected by transcrypt using new salt value" 275 | ``` 276 | 277 | ### Changed 278 | 279 | - Add an [EditorConfig](https://editorconfig.org/) file to help with consistency 280 | in formatting (#51) 281 | - Use 282 | [unofficial Bash strict mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/) 283 | for safety (#53) 284 | - Reformat files using the automated formatting tools 285 | [Prettier](https://prettier.io/) and [shfmt](https://github.com/mvdan/sh) 286 | - Ensure that `transcrypt` addresses all 287 | [ShellCheck](https://github.com/koalaman/shellcheck) static analysis warnings 288 | 289 | ### Fixed 290 | 291 | - Force the use of macOS's system `sed` binary to prevent errors (#50) 292 | - Fix cross-platform compatibility by making salt generation logic consistent 293 | (#57) 294 | 295 | ## [1.1.0] - 2018-05-26 296 | 297 | ### Fixed 298 | 299 | - Fix broken cipher validation safety check when running with OpenSSL v1.1.0+. 300 | (#48) 301 | 302 | ## [1.0.3] - 2017-08-21 303 | 304 | ### Fixed 305 | 306 | - Explicitly set digest hash function to match default settings before OpenSSL 307 | v1.1.0. (#41) 308 | 309 | ## [1.0.2] - 2017-04-06 310 | 311 | ### Fixed 312 | 313 | - Ensure realpath function does not incorrectly return the current directory for 314 | certain inputs. (#38) 315 | 316 | ## [1.0.1] - 2017-01-06 317 | 318 | ### Fixed 319 | 320 | - Correct the behavior of `mktemp` when running on OS X versions 10.10 Yosemite 321 | and earlier. 322 | - Prevent unexpected error output when running transcrypt outside of a Git 323 | repository. 324 | 325 | ## [1.0.0] - 2017-01-02 326 | 327 | Since the v0.9.9 release, these are the notable improvements made to transcrypt: 328 | 329 | - properly handle file names with spaces 330 | - adjust usage of `mktemp` utility to be more cross-platform 331 | - additional safety checks for all required cli utility dependencies 332 | 333 | ## [0.9.9] - 2016-09-05 334 | 335 | Since the v0.9.7 release, these are the notable improvements made to transcrypt: 336 | 337 | - support for use of a 338 | [wildcard](https://github.com/elasticdog/transcrypt/commit/a0b7d4ec0296e83974cb02be640747149b23ef54) 339 | with `--show-raw` to dump the raw commit objects for _all_ encrypted files 340 | - GPG import/export of repository configuration 341 | - more 342 | [strict filter script behavior](https://github.com/elasticdog/transcrypt/pull/29) 343 | to adhere to upstream recommendations 344 | - automatic caching of the decrypted content for faster Git operations like 345 | `git log -p` 346 | - ability to configure bare repositories 347 | - ability to configure "fake bare" repositories for use through 348 | [vcsh](https://github.com/RichiH/vcsh) 349 | - ability configure multiple worktrees via 350 | [git-workflow](https://github.com/blog/2042-git-2-5-including-multiple-worktrees-and-triangular-workflows) 351 | - support for unencrypted archive exporting via 352 | [git-archive](https://git-scm.com/docs/git-archive) 353 | 354 | ## [0.9.8] - 2016-09-05 355 | 356 | ## [0.9.7] - 2015-03-23 357 | 358 | ## [0.9.6] - 2014-08-30 359 | 360 | ## [0.9.5] - 2014-08-23 361 | 362 | ## [0.9.4] - 2014-03-03 363 | 364 | [unreleased]: https://github.com/elasticdog/transcrypt/compare/v2.3.1...HEAD 365 | [2.3.1]: https://github.com/elasticdog/transcrypt/compare/v2.3.0...v2.3.1 366 | [2.3.0]: https://github.com/elasticdog/transcrypt/compare/v2.2.3...v2.3.0 367 | [2.2.3]: https://github.com/elasticdog/transcrypt/compare/v2.2.2...v2.2.3 368 | [2.2.2]: https://github.com/elasticdog/transcrypt/compare/v2.2.1...v2.2.2 369 | [2.2.1]: https://github.com/elasticdog/transcrypt/compare/v2.2.0...v2.2.1 370 | [2.2.0]: https://github.com/elasticdog/transcrypt/compare/v2.1.0...v2.2.0 371 | [2.1.0]: https://github.com/elasticdog/transcrypt/compare/v2.0.0...v2.1.0 372 | [2.0.0]: https://github.com/elasticdog/transcrypt/compare/v1.1.0...v2.0.0 373 | [1.1.0]: https://github.com/elasticdog/transcrypt/compare/v1.0.3...v1.1.0 374 | [1.0.3]: https://github.com/elasticdog/transcrypt/compare/v1.0.2...v1.0.3 375 | [1.0.2]: https://github.com/elasticdog/transcrypt/compare/v1.0.1...v1.0.2 376 | [1.0.1]: https://github.com/elasticdog/transcrypt/compare/v1.0.0...v1.0.1 377 | [1.0.0]: https://github.com/elasticdog/transcrypt/compare/v0.9.9...v1.0.0 378 | [0.9.9]: https://github.com/elasticdog/transcrypt/compare/v0.9.8...v0.9.9 379 | [0.9.8]: https://github.com/elasticdog/transcrypt/compare/v0.9.7...v0.9.8 380 | [0.9.7]: https://github.com/elasticdog/transcrypt/compare/v0.9.6...v0.9.7 381 | [0.9.6]: https://github.com/elasticdog/transcrypt/compare/v0.9.5...v0.9.6 382 | [0.9.5]: https://github.com/elasticdog/transcrypt/compare/v0.9.4...v0.9.5 383 | [0.9.4]: https://github.com/elasticdog/transcrypt/releases/tag/v0.9.4 384 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Install transcrypt 2 | 3 | The requirements to run transcrypt are minimal: 4 | 5 | - Bash 6 | - Git 7 | - OpenSSL 8 | - `column` command (on Ubuntu/Debian install `bsdmainutils`) 9 | - if using OpenSSL 3+ one of: `xxd` (on Ubuntu/Debian is included with `vim`) 10 | or `printf` command (with %b directive) or `perl` 11 | 12 | ...and optionally: 13 | 14 | - GnuPG - for secure configuration import/export 15 | 16 | You also need access to the _transcrypt_ script itself... 17 | 18 | ## Manual Installation 19 | 20 | You can add transcrypt directly to your repository, or just put it somewhere in 21 | your $PATH: 22 | 23 | $ git clone https://github.com/elasticdog/transcrypt.git 24 | $ cd transcrypt/ 25 | $ sudo ln -s ${PWD}/transcrypt /usr/local/bin/transcrypt 26 | 27 | ## Installation via Packages 28 | 29 | A number of packages are available for installing transcrypt directly on your 30 | system via its native package manager. Some of these packages also include man 31 | page documentation as well as shell auto-completion scripts. 32 | 33 | ### Arch Linux 34 | 35 | If you're on Arch Linux, you can build/install transcrypt using the 36 | [provided PKGBUILD](https://github.com/elasticdog/transcrypt/blob/main/contrib/packaging/pacman/PKGBUILD): 37 | 38 | $ git clone https://github.com/elasticdog/transcrypt.git 39 | $ cd transcrypt/contrib/packaging/pacman/ 40 | $ makepkg -sic 41 | 42 | ### Heroku 43 | 44 | If you're running software on Heroku, you can integrate transcrypt into your 45 | slug compilation phase by using the 46 | [transcrypt buildpack](https://github.com/perplexes/heroku-buildpack-transcrypt), 47 | developed by [Colin Curtin](https://github.com/perplexes). 48 | 49 | ### NixOS 50 | 51 | If you're on NixOS, you can install transcrypt directly via 52 | [Nix](https://nixos.org/nix/): 53 | 54 | $ nix-env -iA nixos.gitAndTools.transcrypt 55 | 56 | > _**Note:** 57 | > The [transcrypt derivation](https://github.com/NixOS/nixpkgs/blob/main/pkgs/applications/version-management/git-and-tools/transcrypt/default.nix) 58 | > was added in Oct 2015, so it is not available on the 15.09 channel._ 59 | 60 | ### OS X 61 | 62 | If you're on OS X, you can install transcrypt directly via 63 | [Homebrew](http://brew.sh/): 64 | 65 | $ brew install transcrypt 66 | 67 | ### FreeBSD 68 | 69 | If you're on FreeBSD, you can install transcrypt directly via the Ports 70 | collection: 71 | 72 | # `cd /usr/ports/security/transcrypt && make install clean distclean` 73 | 74 | or via the packages system: 75 | 76 | # `pkg install -y security/transcrypt` 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2025, James Murty 2 | Copyright (c) 2014-2020, Aaron Bull Schaefer 3 | Copyright (c) 2011, Woody Gilk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # transcrypt 2 | 3 | A script to configure transparent encryption of sensitive files stored in a Git 4 | repository. Files that you choose will be automatically encrypted when you 5 | commit them, and automatically decrypted when you check them out. The process 6 | will degrade gracefully, so even people without your encryption password can 7 | safely commit changes to the repository's non-encrypted files. 8 | 9 | transcrypt protects your data when it's pushed to remotes that you may not 10 | directly control (e.g., GitHub, Dropbox clones, etc.), while still allowing you 11 | to work normally on your local working copy. You can conveniently store things 12 | like passwords and private keys within your repository and not have to share 13 | them with your entire team or complicate your workflow. 14 | 15 | ![Tests](https://github.com/elasticdog/transcrypt/workflows/Tests/badge.svg) 16 | 17 | ## Overview 18 | 19 | transcrypt is in the same vein as existing projects like 20 | [git-crypt](https://github.com/AGWA/git-crypt) and 21 | [git-encrypt](https://github.com/shadowhand/git-encrypt), which follow Git's 22 | documentation regarding the use of clean/smudge filters for encryption. In 23 | comparison to those other projects, transcrypt makes substantial improvements in 24 | the areas of usability and safety. 25 | 26 | - transcrypt is just a Bash script and does not require compilation 27 | - transcrypt uses OpenSSL's symmetric cipher routines rather than implementing 28 | its own crypto 29 | - transcrypt does not have to remain installed after the initial repository 30 | configuration 31 | - transcrypt generates a unique salt for each encrypted file 32 | - transcrypt uses safety checks to avoid clobbering or duplicating configuration 33 | data 34 | - transcrypt facilitates setting up additional clones as well as rekeying 35 | - transcrypt adds an alias `git ls-crypt` to list all encrypted files 36 | 37 | ### Salt Generation 38 | 39 | The _decryption -> encryption_ process on an unchanged file must be 40 | deterministic for everything to work transparently. To do that, the same salt 41 | must be used each time we encrypt the same file. Rather than use a static salt 42 | common to all files, transcrypt first has OpenSSL generate an HMAC-SHA256 43 | cryptographic hash-based message authentication code for each decrypted file 44 | (keyed with a combination of the filename and transcrypt password), and then 45 | uses the last 16 bytes of that HMAC for the file's unique salt. When the content 46 | of the file changes, so does the salt. Since an 47 | [HMAC has been proven to be a PRF](https://web.archive.org/web/20090706011828/cseweb.ucsd.edu/~mihir/papers/hmac-new.html), 48 | this method of salt selection does not leak information about the original 49 | contents, but is still deterministic. 50 | 51 | ## Usage 52 | 53 | The requirements to run transcrypt are minimal: 54 | 55 | - Bash 56 | - Git 57 | - OpenSSL 58 | - `column` and `hexdump` commands (on Ubuntu/Debian install `bsdmainutils`) 59 | - if using OpenSSL 3+ one of: `xxd` (on Ubuntu/Debian is included with `vim`) 60 | or `printf` command (with %b directive) or `perl` 61 | 62 | ...and optionally: 63 | 64 | - GnuPG - for secure configuration import/export 65 | 66 | You also need access to the _transcrypt_ script itself. You can add it directly 67 | to your repository, or just put it somewhere in your \$PATH: 68 | 69 | $ git clone https://github.com/elasticdog/transcrypt.git 70 | $ cd transcrypt/ 71 | $ sudo ln -s ${PWD}/transcrypt /usr/local/bin/transcrypt 72 | 73 | #### Installation via Packages 74 | 75 | A number of packages are available for installing transcrypt directly on your 76 | system via its native package manager. Some of these packages also include man 77 | page documentation as well as shell auto-completion scripts. 78 | 79 | - Arch Linux 80 | - Heroku (via [Buildpacks](https://devcenter.heroku.com/articles/buildpacks)) 81 | - NixOS 82 | - OS X (via [Homebrew](http://brew.sh/)) 83 | 84 | ...see the [INSTALL document](INSTALL.md) for more details. 85 | 86 | ### Initialize an Unconfigured Repository 87 | 88 | transcrypt will interactively prompt you for the required information, all you 89 | have to do run the script within a Git repository: 90 | 91 | $ cd / 92 | $ transcrypt 93 | 94 | If you already know the values you want to use, you can specify them directly 95 | using the command line options. Run `transcrypt --help` for more details. 96 | 97 | ### Designate a File to be Encrypted 98 | 99 | Once a repository has been configured with transcrypt, you can designate for 100 | files to be encrypted by applying the "crypt" filter, diff, and merge to a 101 | [pattern](https://www.kernel.org/pub/software/scm/git/docs/gitignore.html#_pattern_format) 102 | in the top-level _[.gitattributes](http://git-scm.com/docs/gitattributes)_ 103 | config. If that pattern matches a file in your repository, the file will be 104 | transparently encrypted once you stage and commit it: 105 | 106 | $ cd / 107 | $ transcrypt --add sensitive_file 108 | $ git add .gitattributes sensitive_file 109 | $ git commit -m 'Add encrypted version of a sensitive file' 110 | 111 | The _.gitattributes_ file should be committed and tracked along with everything 112 | else in your repository so clones will be aware of what is encrypted. Make sure 113 | you don't accidentally add a pattern that would encrypt this file :-) 114 | 115 | > For your reference, if you find the above description confusing, you'll find 116 | > that this repository has been configured following these exact steps. 117 | 118 | ### Listing the Currently Encrypted Files 119 | 120 | For convenience, transcrypt also adds a Git alias to allow you to list all of 121 | the currently encrypted files in a repository: 122 | 123 | $ git ls-crypt 124 | sensitive_file 125 | 126 | Alternatively, you can use the `--list` command line option: 127 | 128 | $ transcrypt --list 129 | sensitive_file 130 | 131 | You can also use this to verify your _.gitattributes_ patterns when designating 132 | new files to be encrypted, as the alias will list pattern matches as long as 133 | everything has been staged (via `git add`). 134 | 135 | After committing things, but before you push to a remote repository, you can 136 | validate that files are encrypted as expected by viewing them in their raw form: 137 | 138 | $ git show HEAD: --no-textconv 139 | 140 | The `` in the above command must be relative to the _top-level_ of 141 | the repository. Alternatively, you can use the `--show-raw` command line option 142 | and provide a path relative to your current directory: 143 | 144 | $ transcrypt --show-raw sensitive_file 145 | 146 | ### Initialize a Clone of a Configured Repository 147 | 148 | If you have just cloned a repository containing files that are encrypted, you'll 149 | want to configure transcrypt with the same cipher and password as the origin 150 | repository. The owner of the origin repository can dump the credentials for you 151 | by running the `--display` command line option: 152 | 153 | $ transcrypt --display 154 | The current repository was configured using transcrypt v0.2.0 155 | and has the following configuration: 156 | 157 | CONTEXT: default 158 | CIPHER: aes-256-cbc 159 | PASSWORD: correct horse battery staple 160 | 161 | Copy and paste the following command to initialize a cloned repository: 162 | 163 | transcrypt -c aes-256-cbc -p 'correct horse battery staple' 164 | 165 | Once transcrypt has stored the matching credentials, it will force a checkout of 166 | any exising encrypted files in order to decrypt them. 167 | 168 | ### Rekeying 169 | 170 | Periodically, you may want to change the encryption cipher or password used to 171 | encrypt the files in your repository. You can do that easily with transcrypt's 172 | rekey option: 173 | 174 | $ transcrypt --rekey 175 | 176 | > As a warning, rekeying will remove your ability to see historical diffs of the 177 | > encrypted files in plain text. Changes made with the new key will still be 178 | > visible, and you can always see the historical diffs in encrypted form by 179 | > disabling the text conversion filters: 180 | > 181 | > $ git log --patch --no-textconv 182 | 183 | After rekeying, all clones of your repository should flush their transcrypt 184 | credentials, fetch and merge the new encrypted files via Git, and then 185 | re-configure transcrypt with the new credentials. 186 | 187 | $ transcrypt --flush-credentials 188 | $ git fetch origin 189 | $ git merge origin/main 190 | $ transcrypt -c aes-256-cbc -p 'the-new-password' 191 | 192 | ### Command Line Options 193 | 194 | Completion scripts for both Bash and Zsh are included in the _contrib/_ 195 | directory. 196 | 197 | transcrypt [option...] 198 | 199 | -c, --cipher=CIPHER 200 | the symmetric cipher to utilize for encryption; 201 | defaults to aes-256-cbc 202 | 203 | -p, --password=PASSWORD 204 | the password to derive the key from; 205 | defaults to 30 random base64 characters 206 | 207 | --set-openssl-path=PATH_TO_OPENSSL 208 | use OpenSSL at this path; defaults to 'openssl' in $PATH 209 | 210 | -y, --yes 211 | assume yes and accept defaults for non-specified options 212 | 213 | --add, --add=pattern 214 | add a file pattern to encrypt to the .gitattributes file 215 | 216 | -d, --display 217 | display the current repository's cipher and password 218 | 219 | -r, --rekey 220 | re-encrypt all encrypted files using new credentials 221 | 222 | -f, --flush-credentials 223 | remove the locally cached encryption credentials and re-encrypt 224 | any files that had been previously decrypted 225 | 226 | -F, --force 227 | ignore whether the git directory is clean, proceed with the 228 | possibility that uncommitted changes are overwritten 229 | 230 | -u, --uninstall 231 | remove all transcrypt configuration from the repository and 232 | leave files in the current working copy decrypted 233 | 234 | --upgrade 235 | uninstall and re-install transcrypt configuration in the repository 236 | to apply the newest scripts and .gitattributes configuration 237 | 238 | -l, --list 239 | list all of the transparently encrypted files in the repository, 240 | relative to the top-level directory 241 | 242 | -s, --show-raw=FILE 243 | show the raw file as stored in the git commit object; use this 244 | to check if files are encrypted as expected 245 | 246 | -e, --export-gpg=RECIPIENT 247 | export the repository's cipher and password to a file encrypted 248 | for a gpg recipient 249 | 250 | -i, --import-gpg=FILE 251 | import the password and cipher from a gpg encrypted file 252 | 253 | -C, --context=CONTEXT_NAME 254 | name for a context with a different passphrase and cipher from 255 | the 'default' context; use this advanced option to encrypt 256 | different files with different passphrases 257 | 258 | --list-contexts 259 | list all contexts configured in the repository, and warn about 260 | incompletely configured contexts 261 | 262 | -v, --version 263 | print the version information 264 | 265 | -h, --help 266 | view this help message 267 | 268 | ## Caveats 269 | 270 | ### Overhead 271 | 272 | The method of using filters to selectively encrypt/decrypt files does add some 273 | overhead to Git by regularly forking OpenSSL processes and removing Git's 274 | ability to efficiently cache file changes. That said, it's not too different 275 | from tracking binary files, and when used as intended, transcrypt should not 276 | noticeably impact performance. There are much better options if your goal is to 277 | encrypt the entire repository. 278 | 279 | ### Localhost 280 | 281 | Note that the configuration and encryption information is stored in plain text 282 | within the repository's _.git/config_ file. This prevents them from being 283 | transferred to remote clones, but they are not protected from inquisitive users 284 | on your local machine. 285 | 286 | For safety, you may prefer to only have the credentials stored when actually 287 | updating encrypted files, and then flush them with `--flush-credentials` once 288 | you're done (make sure you have the credentials backed up elsewhere!). This will 289 | also revert any decrypted files back to their encrypted form in your local 290 | working copy. 291 | 292 | ### Cipher Selection 293 | 294 | Last up, regarding the default cipher choice of `aes-256-cbc`...there aren't any 295 | fantastic alternatives without pulling in outside dependencies. Ideally, we 296 | would use an authenticated cipher mode like `id-aes256-GCM` by default, but 297 | there are a couple of issues: 298 | 299 | 1. I'd like to support OS X out of the box, and unfortunately they are the 300 | lowest common denominator when it comes to OpenSSL. For whatever reason, they 301 | still include OpenSSL 0.9.8y rather than a newer release. Unfortunately, 302 | GCM-based ciphers weren't added until OpenSSL 1.0.1 (back in early 2012). 303 | 304 | 2. Even with newer versions of OpenSSL, the authenticated cipher modes 305 | [don't work exactly right](http://openssl.6102.n7.nabble.com/id-aes256-GCM-command-line-encrypt-decrypt-fail-td27187.html) 306 | when utilizing the command line `openssl enc`. 307 | 308 | I'm contemplating if transcrypt should append an HMAC to the `aes-256-cbc` 309 | ciphertext to provide authentication, or if we should live with the 310 | [malleability issues](http://www.jakoblell.com/blog/2013/12/22/practical-malleability-attack-against-cbc-encrypted-luks-partitions/) 311 | as a known limitation. Essentially, malicious comitters without the transcrypt 312 | password could potentially manipulate the plaintext in limited ways (given that 313 | the attacker knows the original plaintext). Honestly, I'm not sure if the added 314 | complexity here would be worth it given transcrypt's use case. 315 | 316 | ## Advanced 317 | 318 | ### Contexts 319 | 320 | Context names let you encrypt some files with different passwords for a 321 | different audience, such as super-users. The 'default' context applies unless 322 | you set a context name. 323 | 324 | Add a context by reinitialising transcrypt with a context name then add a 325 | pattern with crypt- attributes to *.gitattributes*. For example, 326 | to encrypt a file \_top-secret* in a "super" context: 327 | 328 | # Initialise a new "super" context, and set a different password 329 | $ transcrypt --context=super 330 | 331 | # Add a pattern to .gitattributes with "crypt-super" values 332 | $ transcrypt --context=super --add=top-secret 333 | 334 | # Add and commit your top-secret and .gitattribute files 335 | $ git add .gitattributes top-secret 336 | $ git commit -m "Add top secret file for super-users only" 337 | 338 | # List all contexts 339 | $ transcrypt --list-contexts 340 | 341 | # Display the cipher and password for the "super" context 342 | $ transcrypt --context=super --display 343 | 344 | ## License 345 | 346 | transcrypt is provided under the terms of the 347 | [MIT License](https://en.wikipedia.org/wiki/MIT_License). 348 | 349 | Copyright © 2019-2025, James Murty . 350 | Copyright © 2014-2020, [Aaron Bull Schaefer](mailto:aaron@elasticdog.com). 351 | 352 | ## Contributing 353 | 354 | ### Linting and formatting 355 | 356 | Please use: 357 | 358 | - the [shellcheck](https://www.shellcheck.net) tool to check for subtle bash 359 | scripting errors in the _transcrypt_ file, and apply the recommendations when 360 | possible. E.g: `shellcheck transcrypt` 361 | - the [shfmt](https://github.com/mvdan/sh) tool to apply consistent formatting 362 | to the _transcrypt_ file, e.g: `shfmt -w transcrypt` 363 | - the [Prettier](https://prettier.io) tool to apply consistent formatting to the 364 | _README.md_ file, e.g: `prettier --write README.md` 365 | 366 | ### Tests 367 | 368 | Tests are written using [bats-core](https://github.com/bats-core/bats-core) 369 | version of "Bash Automated Testing System" and stored in the _tests/_ directory. 370 | 371 | To run the tests: 372 | 373 | - [install bats-core](https://github.com/bats-core/bats-core#installation) 374 | - run all tests with: `bats tests/` 375 | - run an individual test with e.g: `bats tests/test_crypt.bats` 376 | -------------------------------------------------------------------------------- /contrib/bash/transcrypt: -------------------------------------------------------------------------------- 1 | # completion script for transcrypt 2 | 3 | _files_and_dirs() { 4 | local IFS=$'\n' 5 | local LASTCHAR=' ' 6 | 7 | COMPREPLY=( $(compgen -o plusdirs -f -- "${COMP_WORDS[COMP_CWORD]}") ) 8 | 9 | if [[ ${#COMPREPLY[@]} -eq 1 ]]; then 10 | [[ -d "$COMPREPLY" ]] && LASTCHAR='/' 11 | COMPREPLY=$(printf '%q%s' "$COMPREPLY" "$LASTCHAR") 12 | else 13 | for ((i=0; i < ${#COMPREPLY[@]}; i++)); do 14 | [[ -d "${COMPREPLY[$i]}" ]] && COMPREPLY[$i]=${COMPREPLY[$i]}/ 15 | done 16 | fi 17 | } 18 | 19 | _transcrypt() { 20 | local cur prev opts 21 | COMPREPLY=() 22 | cur="${COMP_WORDS[COMP_CWORD]}" 23 | prev="${COMP_WORDS[COMP_CWORD-1]}" 24 | opts="-c -p -y -d -r -f -F -u -l -s -e -i -C -v -h \ 25 | --cipher --password --set-openssl-path --yes --display --rekey --flush-credentials --force --uninstall --upgrade --list --show-raw --export-gpg --import-gpg --context --list-contexts --version --help" 26 | 27 | case "${prev}" in 28 | -c | --cipher) 29 | local ciphers=$(openssl list-cipher-commands) 30 | COMPREPLY=( $(compgen -W "${ciphers}" -- ${cur}) ) 31 | return 0 32 | ;; 33 | -p | --password) 34 | return 0 35 | ;; 36 | -s | --show-raw) 37 | _files_and_dirs 38 | return 0 39 | ;; 40 | -e | --export-gpg) 41 | return 0 42 | ;; 43 | -i | --import-gpg) 44 | _files_and_dirs 45 | return 0 46 | ;; 47 | *) 48 | ;; 49 | esac 50 | 51 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 52 | COMPREPLY=$(printf '%q%s' "$COMPREPLY" ' ') 53 | } 54 | 55 | complete -o nospace -F _transcrypt transcrypt 56 | -------------------------------------------------------------------------------- /contrib/packaging/pacman/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore everything 2 | * 3 | 4 | # except these files 5 | !.gitignore 6 | !PKGBUILD 7 | -------------------------------------------------------------------------------- /contrib/packaging/pacman/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: James Murty 2 | pkgname=transcrypt 3 | pkgver=2.3.2-pre 4 | pkgrel=1 5 | pkgdesc='A script to configure transparent encryption of files within a Git repository' 6 | arch=('any') 7 | url='https://github.com/elasticdog/transcrypt' 8 | license=('MIT') 9 | depends=('git' 'openssl') 10 | optdepends=('gnupg: config import/export support') 11 | source=("https://github.com/elasticdog/${pkgname}/archive/v${pkgver}.tar.gz") 12 | sha256sums=('0075a25f7fb48ddfcfb33dd834a5f12fe0644ed4fb5ab0a5f2f7dca06e9ed48c') 13 | 14 | package() { 15 | cd "${pkgname}-${pkgver}/" 16 | 17 | install -m 755 -D transcrypt "${pkgdir}/usr/bin/transcrypt" 18 | install -m 644 -D man/transcrypt.1 "${pkgdir}/usr/share/man/man1/transcrypt.1" 19 | install -m 644 -D LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" 20 | 21 | install -m 644 -D contrib/bash/transcrypt "${pkgdir}/usr/share/bash-completion/completions/transcrypt" 22 | install -m 644 -D contrib/zsh/_transcrypt "${pkgdir}/usr/share/zsh/site-functions/_transcrypt" 23 | } 24 | -------------------------------------------------------------------------------- /contrib/zsh/_transcrypt: -------------------------------------------------------------------------------- 1 | #compdef transcrypt 2 | 3 | _transcrypt() { 4 | local curcontext="$curcontext" state line 5 | typeset -A opt_args 6 | 7 | _arguments \ 8 | '(- 1 *)'{-l,--list}'[list encrypted files]' \ 9 | '(- 1 *)'{-s,--show-raw=}'[show raw file]:file:->file' \ 10 | '(- 1 *)'{-e,--export-gpg=}'[export config to gpg recipient]:recipient:' \ 11 | '(- 1 *)'{-v,--version}'[print version]' \ 12 | '(- 1 *)'{-h,--help}'[view help message]' \ 13 | '(-c --cipher -d --display -f --flush-credentials -u --uninstall)'{-c,--cipher=}'[specify encryption cipher]:cipher:->cipher' \ 14 | '(-p --password -d --display -f --flush-credentials -u --uninstall)'{-p,--password=}'[specify encryption password]:password:' \ 15 | '(-y --yes)'{-y,--yes}'[assume yes and accept defaults]' \ 16 | '(-d --display -p --password -c --cipher -r --rekey -u --uninstall)'{-d,--display}'[display current credentials]' \ 17 | '(-r --rekey -d --display -f --flush-credentials -u --uninstall)'{-r,--rekey}'[rekey all encrypted files]' \ 18 | '(-f --flush-credentials -c --cipher -p --password -r --rekey -u --uninstall)'{-f,--flush-credentials}'[flush cached credentials]' \ 19 | '(-F --force -d --display -u --uninstall)'{-F,--force}'[ignore repository clean state]' \ 20 | '(-u --uninstall -c --cipher -d --display -f --flush-credentials -p --password -r --rekey)'{-u,--uninstall}'[uninstall transcrypt]' \ 21 | '(--set-openssl-path -c --cipher -d --display -f --flush-credentials -p --password -r --rekey)'{--set-openssl-path}'[use OpenSSL at this path]' \ 22 | '(--upgrade -c --cipher -d --display -f --flush-credentials -p --password -r --rekey)--upgrade[upgrade transcrypt]' \ 23 | '(-i --import-gpg -c --cipher -p --password -d --display -f --flush-credentials -u --uninstall)'{-i,--import-gpg=}'[import config from gpg file]:file:->file' \ 24 | && return 0 25 | 26 | case $state in 27 | cipher) 28 | ciphers=( ${(f)"$(_call_program available-ciphers openssl list-cipher-commands)"} ) 29 | _describe -t available-ciphers 'available ciphers' ciphers 30 | ;; 31 | file) 32 | _path_files 33 | ;; 34 | esac 35 | } 36 | 37 | _transcrypt "$@" 38 | 39 | return 1 40 | -------------------------------------------------------------------------------- /man/transcrypt.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "TRANSCRYPT" "1" "August 2016" "" "" 5 | . 6 | .SH "NAME" 7 | \fBtranscrypt\fR \- transparently encrypt files within a git repository 8 | . 9 | .SH "SYNOPSIS" 10 | \fBtranscrypt\fR [\fIoptions\fR\.\.\.] 11 | . 12 | .SH "DESCRIPTION" 13 | transcrypt will configure a Git repository to support the transparent encryption/decryption of files by utilizing OpenSSL\'s symmetric cipher routines and Git\'s built\-in clean/smudge filters\. It will also add a Git alias "ls\-crypt" to list all transparently encrypted files within the repository\. 14 | . 15 | .P 16 | The transcrypt source code and full documentation may be downloaded from \fIhttps://github\.com/elasticdog/transcrypt\fR\. 17 | . 18 | .SH "OPTIONS" 19 | . 20 | .TP 21 | \fB\-c\fR, \fB\-\-cipher\fR=\fIcipher\fR 22 | the symmetric cipher to utilize for encryption; defaults to aes\-256\-cbc 23 | . 24 | .TP 25 | \fB\-p\fR, \fB\-\-password\fR=\fIpassword\fR 26 | the password to derive the key from; defaults to 30 random base64 characters 27 | . 28 | .TP 29 | \fB\-y\fR, \fB\-\-yes\fR 30 | assume yes and accept defaults for non\-specified options 31 | . 32 | .TP 33 | \fB\-d\fR, \fB\-\-display\fR 34 | display the current repository\'s cipher and password 35 | . 36 | .TP 37 | \fB\-r\fR, \fB\-\-rekey\fR 38 | re\-encrypt all encrypted files using new credentials 39 | . 40 | .TP 41 | \fB\-f\fR, \fB\-\-flush\-credentials\fR 42 | remove the locally cached encryption credentials and re\-encrypt any files that had been previously decrypted 43 | . 44 | .TP 45 | \fB\-F\fR, \fB\-\-force\fR 46 | ignore whether the git directory is clean, proceed with the possibility that uncommitted changes are overwritten 47 | . 48 | .TP 49 | \fB\-u\fR, \fB\-\-uninstall\fR 50 | remove all transcrypt configuration from the repository and leave files in the current working copy decrypted 51 | . 52 | .TP 53 | \fB\-l\fR, \fB\-\-list\fR 54 | list all of the transparently encrypted files in the repository, relative to the top\-level directory 55 | . 56 | .TP 57 | \fB\-s\fR, \fB\-\-show\-raw\fR=\fIfile\fR 58 | show the raw file as stored in the git commit object; use this to check if files are encrypted as expected 59 | . 60 | .TP 61 | \fB\-e\fR, \fB\-\-export\-gpg\fR=\fIrecipient\fR 62 | export the repository\'s cipher and password to a file encrypted for a gpg recipient 63 | . 64 | .TP 65 | \fB\-i\fR, \fB\-\-import\-gpg\fR=\fIfile\fR 66 | import the password and cipher from a gpg encrypted file 67 | . 68 | .TP 69 | \fB\-v\fR, \fB\-\-version\fR 70 | print the version information 71 | . 72 | .TP 73 | \fB\-h\fR, \fB\-\-help\fR 74 | view this help message 75 | . 76 | .SH "EXAMPLES" 77 | To initialize a Git repository to support transparent encryption, just change into the repo and run the transcrypt script\. transcrypt will prompt you interactively for all required information if the corresponding option flags were not given\. 78 | . 79 | .IP "" 4 80 | . 81 | .nf 82 | 83 | $ cd / 84 | $ transcrypt 85 | . 86 | .fi 87 | . 88 | .IP "" 0 89 | . 90 | .P 91 | Once a repository has been configured with transcrypt, you can transparently encrypt files by applying the "crypt" filter and diff to a pattern in the top\-level \fI\.gitattributes\fR config\. If that pattern matches a file in your repository, the file will be transparently encrypted once you stage and commit it: 92 | . 93 | .IP "" 4 94 | . 95 | .nf 96 | 97 | $ echo \'sensitive_file filter=crypt diff=crypt\' >> \.gitattributes 98 | $ git add \.gitattributes sensitive_file 99 | $ git commit \-m \'Add encrypted version of a sensitive file\' 100 | . 101 | .fi 102 | . 103 | .IP "" 0 104 | . 105 | .P 106 | See the gitattributes(5) man page for more information\. 107 | . 108 | .P 109 | If you have just cloned a repository containing files that are encrypted, you\'ll want to configure transcrypt with the same cipher and password as the origin repository\. Once transcrypt has stored the matching credentials, it will force a checkout of any existing encrypted files in order to decrypt them\. 110 | . 111 | .P 112 | If the origin repository has just rekeyed, all clones should flush their transcrypt credentials, fetch and merge the new encrypted files via Git, and then re\-configure transcrypt with the new credentials\. 113 | . 114 | .SH "AUTHOR" 115 | Aaron Bull Schaefer 116 | . 117 | .SH "MAINTAINER" 118 | James Murty 119 | . 120 | .SH "SEE ALSO" 121 | enc(1), gitattributes(5) 122 | -------------------------------------------------------------------------------- /man/transcrypt.1.ronn: -------------------------------------------------------------------------------- 1 | transcrypt(1) -- transparently encrypt files within a git repository 2 | ==================================================================== 3 | 4 | ## SYNOPSIS 5 | 6 | `transcrypt` [...] 7 | 8 | ## DESCRIPTION 9 | 10 | transcrypt will configure a Git repository to support the transparent 11 | encryption/decryption of files by utilizing OpenSSL's symmetric cipher routines 12 | and Git's built-in clean/smudge filters. It will also add a Git alias 13 | "ls-crypt" to list all transparently encrypted files within the repository. 14 | 15 | The transcrypt source code and full documentation may be downloaded from 16 | . 17 | 18 | ## OPTIONS 19 | 20 | * `-c`, `--cipher`=: 21 | the symmetric cipher to utilize for encryption; 22 | defaults to aes-256-cbc 23 | 24 | * `-p`, `--password`=: 25 | the password to derive the key from; 26 | defaults to 30 random base64 characters 27 | 28 | * `--set-openssl-path`=: 29 | use OpenSSL at this path; defaults to 'openssl' in $PATH 30 | 31 | * `-y`, `--yes`: 32 | assume yes and accept defaults for non-specified options 33 | 34 | * `-d`, `--display`: 35 | display the current repository's cipher and password 36 | 37 | * `-r`, `--rekey`: 38 | re-encrypt all encrypted files using new credentials 39 | 40 | * `-f`, `--flush-credentials`: 41 | remove the locally cached encryption credentials 42 | and re-encrypt any files that had been previously decrypted 43 | 44 | * `-F`, `--force`: 45 | ignore whether the git directory is clean, proceed with the 46 | possibility that uncommitted changes are overwritten 47 | 48 | * `-u`, `--uninstall`: 49 | remove all transcrypt configuration from the repository 50 | and leave files in the current working copy decrypted 51 | 52 | * `--upgrade`: 53 | apply the latest transcrypt scripts in the repository without 54 | changing your configuration settings 55 | 56 | * `-l`, `--list`: 57 | list all of the transparently encrypted files in the repository, 58 | relative to the top-level directory 59 | 60 | * `-s`, `--show-raw`=: 61 | show the raw file as stored in the git commit object; 62 | use this to check if files are encrypted as expected 63 | 64 | * `-e`, `--export-gpg`=: 65 | export the repository's cipher and password to a file encrypted 66 | for a gpg recipient 67 | 68 | * `-i`, `--import-gpg`=: 69 | import the password and cipher from a gpg encrypted file 70 | 71 | * `-C`, `--context`= 72 | name for a context that can use a different passphrase and cipher 73 | from the 'default' context; use this advanced option, to permit 74 | encrypting different files with different passphrases 75 | 76 | * `--list-contexts` 77 | list all contexts configured in the repository, and warn about 78 | incompletely configured contexts. 79 | 80 | * `-v`, `--version`: 81 | print the version information 82 | 83 | * `-h`, `--help`: 84 | view this help message 85 | 86 | ## EXAMPLES 87 | 88 | To initialize a Git repository to support transparent encryption, just change 89 | into the repo and run the transcrypt script. transcrypt will prompt you 90 | interactively for all required information if the corresponding option flags 91 | were not given. 92 | 93 | $ cd / 94 | $ transcrypt 95 | 96 | Once a repository has been configured with transcrypt, you can transparently 97 | encrypt files by applying the "crypt" filter, diff and merge to a pattern in 98 | the top-level _.gitattributes_ config. If that pattern matches a file in your 99 | repository, the file will be transparently encrypted once you stage and commit 100 | it: 101 | 102 | $ echo 'sensitive_file filter=crypt diff=crypt merge=crypt' >> .gitattributes 103 | $ git add .gitattributes sensitive_file 104 | $ git commit -m 'Add encrypted version of a sensitive file' 105 | 106 | See the gitattributes(5) man page for more information. 107 | 108 | If you have just cloned a repository containing files that are encrypted, 109 | you'll want to configure transcrypt with the same cipher and password as the 110 | origin repository. Once transcrypt has stored the matching credentials, it will 111 | force a checkout of any existing encrypted files in order to decrypt them. 112 | 113 | If the origin repository has just rekeyed, all clones should flush their 114 | transcrypt credentials, fetch and merge the new encrypted files via Git, and 115 | then re-configure transcrypt with the new credentials. 116 | 117 | ## ADVANCED 118 | 119 | Context names let you encrypt some files with different passwords for a 120 | different audience, such as super-users. The 'default' context applies unless 121 | you set a context name. 122 | 123 | Add a context by reinitialising transcrypt with a context name then add a 124 | pattern with crypt- attributes to .gitattributes. 125 | For example, to encrypt a file 'top-secret' in a "super" context: 126 | 127 | # Initialise a new "super" context, and set a different password 128 | $ transcrypt --context=super 129 | 130 | # Add a pattern to .gitattributes with "crypt-super" values 131 | $ echo >> .gitattributes \\ 132 | 'top-secret filter=crypt-super diff=crypt-super merge=crypt-super' 133 | 134 | # Add and commit your top-secret and .gitattribute files 135 | $ git add .gitattributes top-secret 136 | $ git commit -m "Add top secret file for super-users only" 137 | 138 | # List all contexts 139 | $ transcrypt --list-contexts 140 | 141 | # Display the cipher and password for the "super" context 142 | $ transcrypt --context=super --display 143 | 144 | ## AUTHOR 145 | 146 | Aaron Bull Schaefer <aaron@elasticdog.com> 147 | 148 | ## MAINTAINER 149 | 150 | James Murty <james@murty.co> 151 | 152 | ## SEE ALSO 153 | 154 | enc(1), gitattributes(5) 155 | -------------------------------------------------------------------------------- /sensitive_file: -------------------------------------------------------------------------------- 1 | U2FsdGVkX1//6vyAEUROfUrBgZuXaA15WddyGnu4qyMwDAzBjDpLwEqdK+lGuahk 2 | zcurTKIJ36gmdZSd5f2928EQaHGdusIRGzjWfWQ720UUTYzERPuJxGVQSXZIA7a4 3 | o7t2LdFOloWw5g3SRWn+cPBt8lvLkuVuA4x+B4MuzBR0qq7qsk5Qvywfuk2In4Fh 4 | gWMWnUFDpdO/dUPefgZ1okXwWmb2bna7hr7j7Q1Qz+X8/ZPV7epZfonTOCvILVDy 5 | qJlhhH+qrkUwpS8qKMBwyfsNEdKFm60fhPCjWZxyS475Pc3DcG9CQX+AkQqG0frA 6 | aViFCpUkUClSJtoFCg+PaUHPbiN4g/OG7rUcIfVuFDH3Stz3CuqtzJSNkPKNX0Zm 7 | 4xgViApifWvPIijXl/VIHQ7SdzaYiWo2u1G5dCXQw39VnTikx+HWn85wgy0F9IoR 8 | c6FiowxnGsl3ErIwyvuFOqeI8/Xge/7bgWmzqVZSLrpFMPjM/JNO7htRslByo0LD 9 | h5+ngarmfzhI8fspFkmUJWN7YulBRKe4Zh5mohPLhXp/+27KdHC/kBWJtuWUTBx9 10 | RV8cp/g/uIQ6hr/qAnWLdxHgANExGXuf/1zVJYacfnP5cKEqmhYq4gyjs04n8w3a 11 | gjpINQ8bUVzl3rEEv47nlT7o6ZYCxVL4WjWqcCB75KYvDtkDG+lIbu5SBQ1GwW8q 12 | uvcdpV1l9UdXrVuPJvcXLn28xL2KItyfoa/T8rGERrSu875/hwunNmArclvv1UCW 13 | ZRzOhZYMGTHQY5TDC7H05Lwx1wiwRoKJnd+iaE9pw80WnSyarkFkokoHjoBBIO6W 14 | In+mUDJWSg+VTcJxsT91OmKQyfqGYSm3NRshcvhDgyX/Nle2ixtk1KbBM1+06Cyg 15 | zWQ2My4uYJtQAU3RYsC3fIPw9QYfwpyrChVzFVImQwGixInNCm3hilEju9MuwKkT 16 | 9yU7oKnZO5027UrwYb7nn8tUab92R3qpfwkR+ZXspTi5CjBZnU61/yw+7Klv8yBQ 17 | rXfRXVncM2tdcVWlrq7GaRwN3byeo87EQ6/QqyzwHOpNWomk6MHcIAy6pTY6ZIDs 18 | hDrBwUkBDrIyQYntHDAR4LICepnrkouWydW6A5jqR5ySpchsSPSHdR41UcouPtmA 19 | hKk1iYMS9TNu3eG69KiKAZ3djYb2GQl8Z1r/1SGAtKj263nUjazWBUkGuzdNX0ny 20 | yuqXYgXd+lh4YOuL7Dn8JyW5s0IctFj6D4gUnvG4lV/rZYOusIG5rxZn9+c88Did 21 | VWrMzIuAzbWQXweHA8EVZVb+ntqVKpYKixrLdmjNTt21oYW6LgFdxio8gyq6YGMT 22 | vjG6G/5ZM30WOsso4XFp+8i7GzVKNXQrZSEZKbEqrD/+RICVUxzXLRVXm66nfW0r 23 | xEhbuO9v6khlhM6Px1e1seyPZekvBskrB4n8CYsrTTqYww136r/WHZ8/VO+Xu0iN 24 | 1Bt+73pln+PjxiEkIcoHFaCqkqbzHjgGLXeWkfy+0tK/Yr8sTVOrzqNccDg5os98 25 | UyZG3psbOjuw8JOj2TgLVBIDJWejQLBdewflRviinAzM3jcfAS/GejhMK4NQrdm2 26 | SAXhMU+32lnJUfqEzkT3LY1PUxBWFwU0IHTQuqp23v23lFOUt4xKo9+TvbDu7V/W 27 | 8BzmtXMZl2PPTOvuEbpu8AfzzvUFkuOktKrlAGNIijx39fabFr+46rra46BeT1XG 28 | yP3LQcXB5pkjQnwl10BKOGXE014R5BmiAkcyEZiF4ZLhHFpmCJP7U/xDA4g5H4AX 29 | 7WLNu1Mn/IvM7U2Y4AwTJy1GFLCufxL5MRjmAlMwhwebwRvhi3Pamh/StzjssQ1h 30 | 2jgJ+z86DndYpeqg9A7KAMX2FBAry9YbyTT28LNnZRjSRAOWqwRFkFBHryTFgwA2 31 | IKbR/mA/BFavB7UoxBEmijPTs/IbAoXGgQUN6g3DKCfaHbeTJPI8GPemmkA6AYgb 32 | gDE/nVNe8ajQvzktXcM27ivLhjeHVjtCJYjsC3p6GFAMu6/LxKE0hWFRRnMw3RbR 33 | Bmx8n5DWfRCJVgF+pbOah0tPL7iYa4+lprBBGClLpGP4/1KWmSCkxPa2l6QenY0D 34 | C7m0hPUpL99PoAQCvCGssfLzdpDHdb0ZK808CwLnypBd52mSROpHk/4RQ3S0v68R 35 | LpLRdEL0aDBQgHWD374YihPM0dYG7pCghTxuKSZXouQkscQ6xoqxVxyWhTRMcTBz 36 | 9ggEdI0dRV8AY+HSkpOW2Ixca1Opn3UIfznQe7JaPXzpk2j3oRR9A2uXcif+zfp2 37 | IRIqhSa+oP/1wo9RxLybnoheMPZftRqpabjR9AnOzt9KLt/9mu7/lF8YWhALLu6h 38 | dLukBe1mEeVsQQ8CNcKqFK80jNCx7sR6QZCWyaxcgqw6YtOQ7ZszPRSHtCLgcGHc 39 | BY9xgAUe3FaJszt5bed9Cxh/FvY7lQwWkLvVscS/IDtA+sq8Ww3D4/JyqEMaaZcY 40 | L/aeTVBw2BnDs2K48meuFw== 41 | -------------------------------------------------------------------------------- /tests/_test_helper.bash: -------------------------------------------------------------------------------- 1 | function init_git_repo { 2 | # Warn and do nothing if test dir envvar is unset 3 | if [[ -z "$BATS_TEST_DIRNAME" ]]; then 4 | echo "WARNING: Required envvar \$BATS_TEST_DIRNAME is unset" 5 | # Warn and do nothing if test git repo path already exists 6 | elif [[ -e "$BATS_TEST_DIRNAME/.git" ]]; then 7 | echo "WARNING: Test repo already exists at $BATS_TEST_DIRNAME/.git" 8 | else 9 | # Configure "main" as the default branch name 10 | git config --local init.defaultBranch main 11 | # Initialise test git repo at the same path as the test files 12 | git init "$BATS_TEST_DIRNAME" 13 | git checkout -b main 14 | # Tests will fail if name and email aren't set 15 | git config --local user.name "John Doe" 16 | git config --local user.email johndoe@example.com 17 | # Flag test git repo as 100% the test one, for safety before later removal 18 | touch "$BATS_TEST_DIRNAME"/.git/repo-for-transcrypt-bats-tests 19 | fi 20 | } 21 | 22 | function nuke_git_repo { 23 | # Warn and do nothing if test dir envvar is unset 24 | if [[ -z "$BATS_TEST_DIRNAME" ]]; then 25 | echo "WARNING: Required envvar \$BATS_TEST_DIRNAME is unset" 26 | # Warn and do nothing if the test git repo is missing the flag file that 27 | # ensures it *really* is the test one, as set by the 'init_git_repo' function 28 | elif [[ ! -e "$BATS_TEST_DIRNAME/.git/repo-for-transcrypt-bats-tests" ]]; then 29 | echo "WARNING: Aborting delete of non-test Git repo at $BATS_TEST_DIRNAME/.git" 30 | else 31 | # Forcibly delete the test git repo 32 | rm -fR "$BATS_TEST_DIRNAME"/.git 33 | fi 34 | } 35 | 36 | function cleanup_all { 37 | nuke_git_repo 38 | rm -f "$BATS_TEST_DIRNAME"/.gitattributes 39 | rm -f "$BATS_TEST_DIRNAME"/sensitive_file 40 | } 41 | 42 | function init_transcrypt { 43 | "$BATS_TEST_DIRNAME"/../transcrypt --cipher=aes-256-cbc --password='abc 123' --yes 44 | } 45 | 46 | function uninstall_transcrypt { 47 | "$BATS_TEST_DIRNAME"/../transcrypt --uninstall --yes 48 | } 49 | 50 | function encrypt_named_file { 51 | filename="$1" 52 | content=$2 53 | context=${3:-default} 54 | if [[ "$content" ]]; then 55 | echo "$content" > "$filename" 56 | fi 57 | if [[ "$context" = "default" ]]; then 58 | echo "\"$filename\" filter=crypt diff=crypt merge=crypt" >> .gitattributes 59 | else 60 | echo "\"$filename\" filter=crypt-$context diff=crypt-$context merge=crypt-$context" >> .gitattributes 61 | fi 62 | git add .gitattributes "$filename" 63 | run git commit -m "Encrypt file \"$filename\"" 64 | } 65 | 66 | function setup { 67 | pushd "$BATS_TEST_DIRNAME" || exit 1 68 | init_git_repo 69 | if [[ ! "$SETUP_SKIP_INIT_TRANSCRYPT" ]]; then 70 | init_transcrypt 71 | fi 72 | } 73 | 74 | function teardown { 75 | cleanup_all 76 | popd || exit 1 77 | } 78 | 79 | function check_repo_is_clean { 80 | git diff-index --quiet HEAD -- 81 | } 82 | -------------------------------------------------------------------------------- /tests/test_cleanup.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/_test_helper.bash" 4 | 5 | SECRET_CONTENT="My secret content" 6 | SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/qiCg" 7 | 8 | @test "cleanup: transcrypt -f flush clears cached plaintext" { 9 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 10 | 11 | # Confirm working copy file is decrypted 12 | run cat sensitive_file 13 | [ "$status" -eq 0 ] 14 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 15 | 16 | # Show all changes, caches plaintext due to `cachetextconv` setting 17 | run git log -p -- sensitive_file 18 | [ "$status" -eq 0 ] 19 | [[ "${output}" = *"+$SECRET_CONTENT" ]] # Check last line of patch 20 | 21 | # Look up notes ref to cached plaintext 22 | [ -f $BATS_TEST_DIRNAME/.git/refs/notes/textconv/crypt ] 23 | cached_plaintext_obj=$(cat "$BATS_TEST_DIRNAME/.git/refs/notes/textconv/crypt") 24 | 25 | # Confirm plaintext is cached 26 | run git show "$cached_plaintext_obj" 27 | [ "$status" -eq 0 ] 28 | [[ "${output}" = *"+$SECRET_CONTENT" ]] # Check last line of patch 29 | 30 | # Repack to force all objects into packs (which are trickier to clear) 31 | git repack 32 | 33 | # Flush credentials 34 | run ../transcrypt -f --yes 35 | [ "$status" -eq 0 ] 36 | 37 | # Confirm working copy file is encrypted 38 | run cat sensitive_file 39 | [ "$status" -eq 0 ] 40 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 41 | 42 | # Confirm show all changes shows encrypted content, not plaintext 43 | git log -p -- sensitive_file 44 | run git log -p -- sensitive_file 45 | [ "$status" -eq 0 ] 46 | [[ "${output}" = *"+$SECRET_CONTENT_ENC" ]] # Check last line of patch 47 | 48 | # Confirm plaintext cache ref was cleared 49 | [ ! -e $BATS_TEST_DIRNAME/.git/refs/notes/textconv/crypt ] 50 | 51 | # Confirm plaintext obj was truly cleared and is no longer visible 52 | run git show "$cached_plaintext_obj" 53 | [ "$status" -ne 0 ] 54 | } 55 | 56 | @test "cleanup: transcrypt --uninstall clears cached plaintext" { 57 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 58 | 59 | # Confirm working copy file is decrypted 60 | run cat sensitive_file 61 | [ "$status" -eq 0 ] 62 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 63 | 64 | # Show all changes, caches plaintext due to `cachetextconv` setting 65 | run git log -p -- sensitive_file 66 | [ "$status" -eq 0 ] 67 | [[ "${output}" = *"+$SECRET_CONTENT" ]] # Check last line of patch 68 | 69 | # Look up notes ref to cached plaintext 70 | [ -f $BATS_TEST_DIRNAME/.git/refs/notes/textconv/crypt ] 71 | cached_plaintext_obj=$(cat "$BATS_TEST_DIRNAME/.git/refs/notes/textconv/crypt") 72 | 73 | # Confirm plaintext is cached 74 | run git show "$cached_plaintext_obj" 75 | [ "$status" -eq 0 ] 76 | [[ "${output}" = *"+$SECRET_CONTENT" ]] # Check last line of patch 77 | 78 | # Repack to force all objects into packs (which are trickier to clear) 79 | git repack 80 | 81 | # Uninstall 82 | run ../transcrypt --uninstall --yes 83 | [ "$status" -eq 0 ] 84 | 85 | # Confirm working copy file remains unencrypted (per uninstall contract) 86 | run cat sensitive_file 87 | [ "$status" -eq 0 ] 88 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 89 | 90 | # Confirm show all changes shows encrypted content, not plaintext 91 | run git log -p -- sensitive_file 92 | [ "$status" -eq 0 ] 93 | [[ "${output}" = *"+$SECRET_CONTENT_ENC" ]] # Check last line of patch 94 | 95 | # Confirm plaintext cache ref was cleared 96 | [ ! -e $BATS_TEST_DIRNAME/.git/refs/notes/textconv/crypt ] 97 | 98 | # Confirm plaintext obj was truly cleared and is no longer visible 99 | run git show "$cached_plaintext_obj" 100 | [ "$status" -ne 0 ] 101 | } 102 | -------------------------------------------------------------------------------- /tests/test_contexts.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/_test_helper.bash" 4 | 5 | SECRET_CONTENT="My secret content" 6 | SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/qiCg" 7 | SUPER_SECRET_CONTENT_ENC="U2FsdGVkX1+dAkIV/LAKXMmqjDNOGoOVK8Rmhw9tUnbR4dwBDglpkXIT3yzYBvoc" 8 | 9 | function setup { 10 | pushd "$BATS_TEST_DIRNAME" || exit 1 11 | init_git_repo 12 | init_transcrypt 13 | 14 | # Init transcrypt with 'super-secret' context 15 | "$BATS_TEST_DIRNAME"/../transcrypt --context=super-secret --cipher=aes-256-cbc --password=321cba --yes 16 | } 17 | 18 | function teardown { 19 | cleanup_all 20 | rm -f "$BATS_TEST_DIRNAME"/super_sensitive_file 21 | popd || exit 1 22 | } 23 | 24 | @test "contexts: check validation of context names" { 25 | # Invalid context names 26 | run ../transcrypt --context=-ab --cipher=aes-256-cbc --password=none --yes 27 | [ "$status" -ne 0 ] 28 | run ../transcrypt --context=1ab --cipher=aes-256-cbc --password=none --yes 29 | [ "$status" -ne 0 ] 30 | run ../transcrypt --context=a--b --cipher=aes-256-cbc --password=none --yes 31 | [ "$status" -ne 0 ] 32 | run ../transcrypt --context=a- --cipher=aes-256-cbc --password=none --yes 33 | [ "$status" -ne 0 ] 34 | run ../transcrypt --context=A --cipher=aes-256-cbc --password=none --yes 35 | [ "$status" -ne 0 ] 36 | run ../transcrypt --context=aB --cipher=aes-256-cbc --password=none --yes 37 | [ "$status" -ne 0 ] 38 | run ../transcrypt --context=a-B --cipher=aes-256-cbc --password=none --yes 39 | [ "$status" -ne 0 ] 40 | 41 | # Valid context names 42 | run ../transcrypt --context=ab --cipher=aes-256-cbc --password=none --yes 43 | [ "$status" -eq 0 ] 44 | run ../transcrypt --context=a1 --cipher=aes-256-cbc --password=none --yes 45 | [ "$status" -eq 0 ] 46 | run ../transcrypt --context=a-b --cipher=aes-256-cbc --password=none --yes 47 | [ "$status" -eq 0 ] 48 | run ../transcrypt --context=a-1 --cipher=aes-256-cbc --password=none --yes 49 | [ "$status" -eq 0 ] 50 | run ../transcrypt --context=a-b-c --cipher=aes-256-cbc --password=none --yes 51 | [ "$status" -eq 0 ] 52 | run ../transcrypt --context=a-1-c --cipher=aes-256-cbc --password=none --yes 53 | [ "$status" -eq 0 ] 54 | run ../transcrypt --context=a-b-c-d --cipher=aes-256-cbc --password=none --yes 55 | [ "$status" -eq 0 ] 56 | run ../transcrypt --context=a-1-c-d-2 --cipher=aes-256-cbc --password=none --yes 57 | [ "$status" -eq 0 ] 58 | } 59 | 60 | @test "contexts: check git config for 'super-secret' context" { 61 | VERSION=$(../transcrypt -v | awk '{print $2}') 62 | 63 | [[ $(git config --get transcrypt.version) = "$VERSION" ]] 64 | [[ $(git config --get transcrypt.super-secret.cipher) = "aes-256-cbc" ]] 65 | [[ $(git config --get transcrypt.super-secret.password) = "321cba" ]] 66 | 67 | # Use --git-common-dir if available (Git post Nov 2014) otherwise --git-dir 68 | # shellcheck disable=SC2016 69 | [ "$(git config --get filter.crypt.clean)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt clean context=default %f' ] 70 | [ "$(git config --get filter.crypt.smudge)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt smudge context=default' ] 71 | [ "$(git config --get diff.crypt.textconv)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt textconv context=default' ] 72 | [ "$(git config --get merge.crypt.driver)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt merge context=default %O %A %B %L %P' ] 73 | 74 | [[ $(git config --get filter.crypt.required) = "true" ]] 75 | [[ $(git config --get diff.crypt.cachetextconv) = "true" ]] 76 | [[ $(git config --get diff.crypt.binary) = "true" ]] 77 | [[ $(git config --get merge.renormalize) = "true" ]] 78 | 79 | [[ "$(git config --get alias.ls-crypt)" = '!"$(git config transcrypt.crypt-dir 2>/dev/null || printf %s/crypt ""$(git rev-parse --git-dir)"")"/transcrypt --list' ]] 80 | } 81 | 82 | @test "init: show extra context details in --display" { 83 | VERSION=$(../transcrypt -v | awk '{print $2}') 84 | 85 | run ../transcrypt -C super-secret --display 86 | [ "$status" -eq 0 ] 87 | [ "${lines[0]}" = "The current repository was configured using transcrypt version $VERSION" ] 88 | [ "${lines[1]}" = "and has the following configuration for context 'super-secret':" ] 89 | [ "${lines[5]}" = " CONTEXT: super-secret" ] 90 | [ "${lines[6]}" = " CIPHER: aes-256-cbc" ] 91 | [ "${lines[7]}" = " PASSWORD: 321cba" ] 92 | [ "${lines[8]}" = "The repository has 2 contexts: default super-secret" ] 93 | [ "${lines[9]}" = "Copy and paste the following command to initialize a cloned repository for context 'super-secret':" ] 94 | [ "${lines[10]}" = " transcrypt -C super-secret -c aes-256-cbc -p '321cba'" ] 95 | } 96 | 97 | @test "contexts: cannot re-init an existing context, fails with error message" { 98 | # Cannot re-init 'default' context 99 | run ../transcrypt --cipher=aes-256-cbc --password='abc 123' --yes 100 | [ "$status" -ne 0 ] 101 | [ "${lines[0]}" = "transcrypt: the current repository is already configured; see 'transcrypt --display'" ] 102 | 103 | # Cannot re-init a named context 104 | run ../transcrypt --context=super-secret --cipher=aes-256-cbc --password=321cba --yes 105 | [ "$status" -ne 0 ] 106 | [ "${lines[0]}" = "transcrypt: the current repository is already configured for context 'super-secret'; see 'transcrypt --context=super-secret --display'" ] 107 | } 108 | 109 | @test "contexts: encrypt a file in default and 'super-secret' contexts" { 110 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 111 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 112 | 113 | # Confirm .gitattributes is configured for multiple contexts 114 | run cat .gitattributes 115 | [ "${lines[1]}" = '"sensitive_file" filter=crypt diff=crypt merge=crypt' ] 116 | [ "${lines[2]}" = '"super_sensitive_file" filter=crypt-super-secret diff=crypt-super-secret merge=crypt-super-secret' ] 117 | } 118 | 119 | @test "contexts: confirm --list-contexts lists configured contexts not yet in .gitattributes" { 120 | # Confirm .gitattributes is not yet configured for multiple contexts 121 | run ../transcrypt --list-contexts 122 | [ "$status" -eq 0 ] 123 | [ "${lines[0]}" = 'default (no patterns in .gitattributes)' ] 124 | [ "${lines[1]}" = 'super-secret (no patterns in .gitattributes)' ] 125 | } 126 | 127 | @test "contexts: confirm --list-contexts lists contexts with config status" { 128 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 129 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 130 | 131 | # Confirm .gitattributes is configured for multiple contexts 132 | run ../transcrypt --list-contexts 133 | [ "$status" -eq 0 ] 134 | [ "${lines[0]}" = 'default' ] 135 | [ "${lines[1]}" = 'super-secret' ] 136 | } 137 | 138 | @test "contexts: confirm --list-contexts only lists contexts it should" { 139 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 140 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 141 | 142 | # Remove all transcrypt config, including contexts 143 | ../transcrypt --uninstall --yes 144 | 145 | # Don't list contexts when none are known 146 | echo > .gitattributes 147 | run ../transcrypt --list-contexts 148 | [ "$status" -eq 0 ] 149 | [ "${lines[0]}" = '' ] 150 | 151 | # List just super-secret context from .gitattributes 152 | echo '"super_sensitive_file" filter=crypt-super-secret diff=crypt-super-secret merge=crypt-super-secret' > .gitattributes 153 | run ../transcrypt --list-contexts 154 | [ "$status" -eq 0 ] 155 | [ "${lines[0]}" = 'super-secret (not initialised)' ] 156 | [ "${lines[1]}" = '' ] 157 | 158 | # List just default context from .gitattributes 159 | echo '"sensitive_file" filter=crypt diff=crypt merge=crypt' > .gitattributes 160 | run ../transcrypt --list-contexts 161 | [ "$status" -eq 0 ] 162 | [ "${lines[0]}" = 'default (not initialised)' ] 163 | [ "${lines[1]}" = '' ] 164 | } 165 | 166 | @test "contexts: encrypted file contents in multiple context are decrypted in working copy" { 167 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 168 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 169 | 170 | run cat sensitive_file 171 | [ "$status" -eq 0 ] 172 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 173 | 174 | run cat super_sensitive_file 175 | [ "$status" -eq 0 ] 176 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 177 | } 178 | 179 | @test "contexts: encrypted file contents in multiple contexts are encrypted differently in git (via git show)" { 180 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 181 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 182 | 183 | run git show HEAD:sensitive_file --no-textconv 184 | [ "$status" -eq 0 ] 185 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 186 | 187 | run git show HEAD:super_sensitive_file --no-textconv 188 | [ "$status" -eq 0 ] 189 | [ "${lines[0]}" = "$SUPER_SECRET_CONTENT_ENC" ] 190 | } 191 | 192 | @test "contexts: encrypted file contents can be decrypted (via git show --textconv)" { 193 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 194 | run git show HEAD:sensitive_file --textconv 195 | [ "$status" -eq 0 ] 196 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 197 | 198 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 199 | run git show HEAD:super_sensitive_file --textconv 200 | [ "$status" -eq 0 ] 201 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 202 | } 203 | 204 | @test "contexts: transcrypt --show-raw shows encrypted content for multiple contexts" { 205 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 206 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 207 | 208 | run ../transcrypt --show-raw sensitive_file 209 | [ "$status" -eq 0 ] 210 | [ "${lines[0]}" = "==> sensitive_file <==" ] 211 | [ "${lines[1]}" = "$SECRET_CONTENT_ENC" ] 212 | 213 | run ../transcrypt --show-raw super_sensitive_file 214 | [ "$status" -eq 0 ] 215 | [ "${lines[0]}" = "==> super_sensitive_file <==" ] 216 | [ "${lines[1]}" = "$SUPER_SECRET_CONTENT_ENC" ] 217 | } 218 | 219 | @test "contexts: git ls-crypt lists encrypted files for all contexts" { 220 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 221 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 222 | 223 | run git ls-crypt 224 | [ "$status" -eq 0 ] 225 | [ "${lines[0]}" = "sensitive_file" ] 226 | [ "${lines[1]}" = "super_sensitive_file" ] 227 | } 228 | 229 | @test "contexts: git ls-crypt-default lists encrypted files for all contexts" { 230 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 231 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 232 | 233 | run git ls-crypt-default 234 | [ "$status" -eq 0 ] 235 | [ "${lines[0]}" = "sensitive_file" ] 236 | [ "${lines[1]}" = "super_sensitive_file" ] 237 | } 238 | 239 | @test "contexts: git ls-crypt-super-secret lists encrypted file for only 'super-secret' context" { 240 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 241 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 242 | 243 | run git ls-crypt-super-secret 244 | [ "$status" -eq 0 ] 245 | [ "${lines[0]}" = "super_sensitive_file" ] 246 | [ "${lines[1]}" = "" ] 247 | } 248 | 249 | @test "contexts: transcrypt --list lists encrypted files for all contexts" { 250 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 251 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 252 | 253 | run ../transcrypt --list 254 | [ "$status" -eq 0 ] 255 | [ "${lines[0]}" = "sensitive_file" ] 256 | [ "${lines[1]}" = "super_sensitive_file" ] 257 | [ "${lines[2]}" = "" ] 258 | } 259 | 260 | @test "contexts: transcrypt --uninstall leaves decrypted files and repo dirty for all contexts" { 261 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 262 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 263 | 264 | run ../transcrypt --uninstall --yes 265 | [ "$status" -eq 0 ] 266 | 267 | run cat sensitive_file 268 | [ "$status" -eq 0 ] 269 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 270 | 271 | run cat super_sensitive_file 272 | [ "$status" -eq 0 ] 273 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 274 | 275 | run cat .gitattributes 276 | [ "${lines[0]}" = "" ] 277 | 278 | run check_repo_is_clean 279 | [ "$status" -ne 0 ] 280 | } 281 | 282 | @test "contexts: git reset after uninstall leaves encrypted file for all contexts" { 283 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 284 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 285 | 286 | ../transcrypt --uninstall --yes 287 | 288 | git reset --hard 289 | check_repo_is_clean 290 | 291 | run cat sensitive_file 292 | [ "$status" -eq 0 ] 293 | [ "${lines[0]}" != "$SECRET_CONTENT" ] 294 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 295 | 296 | run cat super_sensitive_file 297 | [ "$status" -eq 0 ] 298 | [ "${lines[0]}" != "$SECRET_CONTENT" ] 299 | [ "${lines[0]}" = "$SUPER_SECRET_CONTENT_ENC" ] 300 | } 301 | 302 | @test "contexts: any one of multiple contexts works in isolation" { 303 | # Init transcrypt with encrypted files then reset to be like a new clone 304 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 305 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 306 | ../transcrypt --uninstall --yes 307 | git reset --hard 308 | check_repo_is_clean 309 | 310 | # Confirm sensitive files for both contexts are encrypted in working dir 311 | run cat sensitive_file 312 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 313 | run cat super_sensitive_file 314 | [ "${lines[0]}" = "$SUPER_SECRET_CONTENT_ENC" ] 315 | 316 | # Confirm .gitattributes is configured for contexts, but Git is not 317 | run ../transcrypt --list-contexts 318 | [ "$status" -eq 0 ] 319 | [ "${lines[0]}" = 'default (not initialised)' ] 320 | [ "${lines[1]}" = 'super-secret (not initialised)' ] 321 | 322 | # Re-init only super-secret context: its files are decrypted, not default context 323 | ../transcrypt --context=super-secret --cipher=aes-256-cbc --password=321cba --yes 324 | run ../transcrypt --list-contexts 325 | [ "${lines[0]}" = 'default (not initialised)' ] 326 | [ "${lines[1]}" = 'super-secret' ] 327 | run cat super_sensitive_file 328 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 329 | run cat sensitive_file 330 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 331 | 332 | # Reset again 333 | ../transcrypt --uninstall --yes 334 | git reset --hard 335 | check_repo_is_clean 336 | 337 | # Re-init only default context: its files are decrypted, not super-secret context 338 | ../transcrypt --cipher=aes-256-cbc --password='abc 123' --yes 339 | run ../transcrypt --list-contexts 340 | [ "${lines[0]}" = 'default' ] 341 | [ "${lines[1]}" = 'super-secret (not initialised)' ] 342 | run cat sensitive_file 343 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 344 | run cat super_sensitive_file 345 | [ "${lines[0]}" = "$SUPER_SECRET_CONTENT_ENC" ] 346 | 347 | # Reset again 348 | ../transcrypt --uninstall --yes 349 | git reset --hard 350 | check_repo_is_clean 351 | 352 | # Re-init super-secret then default contexts, to confirm safety check permits this 353 | ../transcrypt --context=super-secret --cipher=aes-256-cbc --password=321cba --yes 354 | ../transcrypt --cipher=aes-256-cbc --password='abc 123' --yes 355 | } 356 | 357 | @test "contexts: --upgrade retains all context configs" { 358 | # Init transcrypt with encrypted files then reset to be like a new clone 359 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 360 | encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" 361 | 362 | # We use context names without warning notes as a surrogate for checking 363 | # that the super-secret context is configured and in .gitattributes 364 | run ../transcrypt --list-contexts 365 | [ "$status" -eq 0 ] 366 | [ "${lines[0]}" = 'default' ] 367 | [ "${lines[1]}" = 'super-secret' ] 368 | 369 | # Upgrade removes and *should* re-create config for all contexts 370 | run ../transcrypt --upgrade --yes 371 | 372 | run ../transcrypt --list-contexts 373 | [ "$status" -eq 0 ] 374 | [ "${lines[0]}" = 'default' ] 375 | [ "${lines[1]}" = 'super-secret' ] 376 | } 377 | -------------------------------------------------------------------------------- /tests/test_crypt.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/_test_helper.bash" 4 | 5 | SECRET_CONTENT="My secret content" 6 | SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/qiCg" 7 | 8 | @test "crypt: git ls-crypt command is available" { 9 | # No encrypted file yet, so command should work with no output 10 | run git ls-crypt 11 | [ "$status" -eq 0 ] 12 | [ "${lines[0]}" = "" ] 13 | } 14 | 15 | @test "crypt: encrypt a file" { 16 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 17 | } 18 | 19 | @test "crypt: encrypted file contents are decrypted in working copy" { 20 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 21 | run cat sensitive_file 22 | [ "$status" -eq 0 ] 23 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 24 | } 25 | 26 | @test "crypt: encrypted file contents are encrypted in git (via git show)" { 27 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 28 | run git show HEAD:sensitive_file --no-textconv 29 | [ "$status" -eq 0 ] 30 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 31 | } 32 | 33 | @test "crypt: encrypted file contents can be decrypted (via git show --textconv)" { 34 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 35 | run git show HEAD:sensitive_file --textconv 36 | [ "$status" -eq 0 ] 37 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 38 | } 39 | 40 | @test "crypt: transcrypt --show-raw shows encrypted content" { 41 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 42 | run ../transcrypt --show-raw sensitive_file 43 | [ "$status" -eq 0 ] 44 | [ "${lines[0]}" = "==> sensitive_file <==" ] 45 | [ "${lines[1]}" = "$SECRET_CONTENT_ENC" ] 46 | } 47 | 48 | @test "crypt: git ls-crypt lists encrypted file" { 49 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 50 | 51 | run git ls-crypt 52 | [ "$status" -eq 0 ] 53 | [ "${lines[0]}" = "sensitive_file" ] 54 | } 55 | 56 | @test "crypt: transcrypt --list lists encrypted file" { 57 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 58 | 59 | run ../transcrypt --list 60 | [ "$status" -eq 0 ] 61 | [[ "${output}" = *"sensitive_file" ]] 62 | } 63 | 64 | @test "crypt: transcrypt --uninstall leaves decrypted file and repo dirty" { 65 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 66 | 67 | run ../transcrypt --uninstall --yes 68 | [ "$status" -eq 0 ] 69 | 70 | run cat sensitive_file 71 | [ "$status" -eq 0 ] 72 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 73 | 74 | run cat .gitattributes 75 | [ "${lines[0]}" = "" ] 76 | 77 | run check_repo_is_clean 78 | [ "$status" -ne 0 ] 79 | } 80 | 81 | @test "crypt: git reset after uninstall leaves encrypted file" { 82 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 83 | 84 | "$BATS_TEST_DIRNAME"/../transcrypt --uninstall --yes 85 | 86 | git reset --hard 87 | check_repo_is_clean 88 | 89 | run cat sensitive_file 90 | [ "$status" -eq 0 ] 91 | [ "${lines[0]}" != "$SECRET_CONTENT" ] 92 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 93 | } 94 | 95 | @test "crypt: handle challenging file names when 'core.quotePath=true'" { 96 | # Set core.quotePath=true which is the Git default prior to encrypting a 97 | # file with non-ASCII characters and spaces in the name, to confirm 98 | # transcrypt can handle the file properly. 99 | # For info about the 'core.quotePath' setting see 100 | # https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath 101 | git config --local --add core.quotePath true 102 | 103 | FILENAME="Mig – røve" # Danish 104 | SECRET_CONTENT_ENC="U2FsdGVkX18jeEpsv589tzPzs+2KY6Bv6uxAHqAV6WvcSmckLHHVEvq3uItd9oq7" 105 | 106 | encrypt_named_file "$FILENAME" "$SECRET_CONTENT" 107 | [[ "${output}" = *"Encrypt file \"$FILENAME\""* ]] 108 | 109 | # Working copy is decrypted 110 | run cat "$FILENAME" 111 | [ "$status" -eq 0 ] 112 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 113 | 114 | # Git internal copy is encrypted 115 | run git show HEAD:"$FILENAME" --no-textconv 116 | [ "$status" -eq 0 ] 117 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 118 | 119 | # transcrypt --show-raw shows encrypted content 120 | run ../transcrypt --show-raw "$FILENAME" 121 | [ "$status" -eq 0 ] 122 | [ "${lines[0]}" = "==> $FILENAME <==" ] 123 | [ "${lines[1]}" = "$SECRET_CONTENT_ENC" ] 124 | 125 | # git ls-crypt lists encrypted file 126 | run git ls-crypt 127 | [ "$status" -eq 0 ] 128 | [[ "${output}" = *"$FILENAME" ]] 129 | 130 | # transcrypt --list lists encrypted file" 131 | run ../transcrypt --list 132 | [ "$status" -eq 0 ] 133 | [[ "${output}" = *"$FILENAME" ]] 134 | 135 | rm "$FILENAME" 136 | } 137 | 138 | @test "crypt: handle very small file" { 139 | FILENAME="small file.txt" 140 | SECRET_CONTENT="sh" 141 | SECRET_CONTENT_ENC="U2FsdGVkX1+fWwQTmT7tfxgGSJ+TLQJVV9WWlxtRZ38=" 142 | 143 | encrypt_named_file "$FILENAME" "$SECRET_CONTENT" 144 | [[ "${output}" = *"Encrypt file \"$FILENAME\""* ]] 145 | 146 | # Working copy is decrypted 147 | run cat "$FILENAME" 148 | [ "$status" -eq 0 ] 149 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 150 | 151 | # Git internal copy is encrypted 152 | run git show HEAD:"$FILENAME" --no-textconv 153 | [ "$status" -eq 0 ] 154 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 155 | 156 | # transcrypt --show-raw shows encrypted content 157 | run ../transcrypt --show-raw "$FILENAME" 158 | [ "$status" -eq 0 ] 159 | [ "${lines[0]}" = "==> $FILENAME <==" ] 160 | [ "${lines[1]}" = "$SECRET_CONTENT_ENC" ] 161 | 162 | # git ls-crypt lists encrypted file 163 | run git ls-crypt 164 | [ "$status" -eq 0 ] 165 | [[ "${output}" = *"$FILENAME" ]] 166 | 167 | # transcrypt --list lists encrypted file" 168 | run ../transcrypt --list 169 | [ "$status" -eq 0 ] 170 | [[ "${output}" = *"$FILENAME" ]] 171 | 172 | rm "$FILENAME" 173 | } 174 | 175 | @test "crypt: handle file with problematic bytes" { 176 | FILENAME="problem bytes file.txt" 177 | SECRET_CONTENT_ENC="U2FsdGVkX18oyzDfF0Yjh1oqnz8RvjksOYpv53eaJ7c=" 178 | 179 | # Write octal byte 375 and null byte as file contents 180 | printf "\375 \0 shh" > "$FILENAME" 181 | 182 | encrypt_named_file "$FILENAME" 183 | [[ "${output}" = *"Encrypt file \"$FILENAME\""* ]] 184 | 185 | # Git internal copy is encrypted 186 | run git show HEAD:"$FILENAME" --no-textconv 187 | [ "$status" -eq 0 ] 188 | [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] 189 | 190 | # transcrypt --show-raw shows encrypted content 191 | run ../transcrypt --show-raw "$FILENAME" 192 | [ "$status" -eq 0 ] 193 | [ "${lines[0]}" = "==> $FILENAME <==" ] 194 | [ "${lines[1]}" = "$SECRET_CONTENT_ENC" ] 195 | 196 | # git ls-crypt lists encrypted file 197 | run git ls-crypt 198 | [ "$status" -eq 0 ] 199 | [[ "${output}" = *"$FILENAME" ]] 200 | 201 | # transcrypt --list lists encrypted file" 202 | run ../transcrypt --list 203 | [ "$status" -eq 0 ] 204 | [[ "${output}" = *"$FILENAME" ]] 205 | 206 | rm "$FILENAME" 207 | } 208 | 209 | @test "crypt: add file patterns to .gitattributes" { 210 | # git add-crypt add file to gitattributes 211 | 212 | # add file 1 via `add-crypt` 213 | git add-crypt foobar 214 | run cat .gitattributes 215 | [[ "$status" -eq 0 ]] 216 | [[ "${#lines[@]}" = "2" ]] 217 | [[ "${lines[1]}" = "foobar filter=crypt diff=crypt merge=crypt" ]] 218 | 219 | # add pattern 2 via `add-crypt` 220 | git add-crypt config/*.json 221 | run cat .gitattributes 222 | [[ "$status" -eq 0 ]] 223 | [[ "${#lines[@]}" = "3" ]] 224 | [[ "${lines[2]}" = "config/*.json filter=crypt diff=crypt merge=crypt" ]] 225 | 226 | # add patterns 3 & 4 via `transcrypt --add` 227 | "$BATS_TEST_DIRNAME"/../transcrypt --add pattern2 228 | "$BATS_TEST_DIRNAME"/../transcrypt --add=*.secret 229 | run cat .gitattributes 230 | [[ "$status" -eq 0 ]] 231 | [[ "${#lines[@]}" = "5" ]] 232 | [[ "${lines[3]}" = "pattern2 filter=crypt diff=crypt merge=crypt" ]] 233 | [[ "${lines[4]}" = "*.secret filter=crypt diff=crypt merge=crypt" ]] 234 | 235 | # test ignore adding duplicate pattern 236 | git add-crypt foobar 237 | git add-crypt config/*.json 238 | git add-crypt pattern2 239 | git add-crypt *.secret 240 | run cat .gitattributes 241 | [[ "$status" -eq 0 ]] 242 | [[ "${#lines[@]}" = "5" ]] # no new line added 243 | [[ "${lines[1]}" = "foobar filter=crypt diff=crypt merge=crypt" ]] 244 | [[ "${lines[2]}" = "config/*.json filter=crypt diff=crypt merge=crypt" ]] 245 | [[ "${lines[3]}" = "pattern2 filter=crypt diff=crypt merge=crypt" ]] 246 | [[ "${lines[4]}" = "*.secret filter=crypt diff=crypt merge=crypt" ]] 247 | } 248 | 249 | @test "crypt: transcrypt --upgrade applies new merge driver" { 250 | VERSION=$("$BATS_TEST_DIRNAME"/../transcrypt -v | awk '{print $2}') 251 | 252 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 253 | 254 | # Simulate a fake old installation of transcrypt without merge driver 255 | echo "sensitive_file filter=crypt diff=crypt" > .gitattributes 256 | git add .gitattributes 257 | git commit -m "Removed merge driver config from .gitattributes" 258 | 259 | git config --local transcrypt.version "0.0" 260 | 261 | git config --local --unset merge.crypt.driver 262 | 263 | # Check .gitattributes and sensitive_file before re-install 264 | run cat .gitattributes 265 | [ "${lines[0]}" = "sensitive_file filter=crypt diff=crypt" ] 266 | # Check merge driver is not installed 267 | [ ! "$(git config --get merge.crypt.driver)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt merge %O %A %B %L %P' ] 268 | 269 | run git config --get --local transcrypt.version 270 | [ "${lines[0]}" = "0.0" ] 271 | run git config --get --local transcrypt.cipher 272 | [ "${lines[0]}" = "aes-256-cbc" ] 273 | run git config --get --local transcrypt.password 274 | [ "${lines[0]}" = "abc 123" ] 275 | 276 | run cat sensitive_file 277 | [ "$status" -eq 0 ] 278 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 279 | 280 | # Perform re-install 281 | run ../transcrypt --upgrade --yes 282 | [ "$status" -eq 0 ] 283 | 284 | run git config --get --local transcrypt.version 285 | [ "${lines[0]}" = "$VERSION" ] 286 | run git config --get --local transcrypt.cipher 287 | [ "${lines[0]}" = "aes-256-cbc" ] 288 | run git config --get --local transcrypt.password 289 | [ "${lines[0]}" = "abc 123" ] 290 | 291 | # Check sensitive_file is unchanged after re-install 292 | run cat sensitive_file 293 | [ "$status" -eq 0 ] 294 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 295 | 296 | # Check merge driver is installed 297 | [ "$(git config --get merge.crypt.driver)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt merge context=default %O %A %B %L %P' ] 298 | 299 | # Check .gitattributes is updated to include merge driver 300 | run cat .gitattributes 301 | [ "${lines[0]}" = "sensitive_file filter=crypt diff=crypt merge=crypt" ] 302 | 303 | run check_repo_is_clean 304 | [ "$status" -ne 0 ] 305 | } 306 | 307 | @test "crypt: transcrypt --force handles files missing from working copy" { 308 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 309 | 310 | "$BATS_TEST_DIRNAME"/../transcrypt --uninstall --yes 311 | 312 | # Reset repo to restore .gitattributes file 313 | git reset --hard 314 | 315 | # Delete secret file from working copy 316 | rm sensitive_file 317 | 318 | # Re-init with --force should check out deleted secret file 319 | ../transcrypt --force --cipher=aes-256-cbc --password='abc 123' --yes 320 | 321 | # Check sensitive_file is present and decrypted 322 | run cat sensitive_file 323 | [ "$status" -eq 0 ] 324 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 325 | } 326 | -------------------------------------------------------------------------------- /tests/test_init.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/_test_helper.bash" 4 | 5 | # Custom setup: don't init transcrypt 6 | # shellcheck disable=SC2034 7 | SETUP_SKIP_INIT_TRANSCRYPT=1 8 | 9 | 10 | @test "init: works at all" { 11 | # Use literal command not function to confirm command works at least once 12 | run ../transcrypt --cipher=aes-256-cbc --password='abc 123' --yes 13 | [ "$status" -eq 0 ] 14 | [[ "${output}" = *"The repository has been successfully configured by transcrypt."* ]] 15 | } 16 | 17 | @test "init: creates .gitattributes" { 18 | init_transcrypt 19 | [ -f .gitattributes ] 20 | run cat .gitattributes 21 | [ "${lines[0]}" = "#pattern filter=crypt diff=crypt merge=crypt" ] 22 | } 23 | 24 | @test "init: creates scripts in .git/crypt/" { 25 | init_transcrypt 26 | [ -d .git/crypt ] 27 | [ -f .git/crypt/transcrypt ] 28 | } 29 | 30 | @test "init: applies git config" { 31 | init_transcrypt 32 | VERSION=$(../transcrypt -v | awk '{print $2}') 33 | 34 | [ "$(git config --get transcrypt.version)" = "$VERSION" ] 35 | [ "$(git config --get transcrypt.cipher)" = "aes-256-cbc" ] 36 | [ "$(git config --get transcrypt.password)" = "abc 123" ] 37 | [ "$(git config --get transcrypt.openssl-path)" = "openssl" ] 38 | 39 | # Use --git-common-dir if available (Git post Nov 2014) otherwise --git-dir 40 | # shellcheck disable=SC2016 41 | [ "$(git config --get filter.crypt.clean)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt clean context=default %f' ] 42 | [ "$(git config --get filter.crypt.smudge)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt smudge context=default' ] 43 | [ "$(git config --get diff.crypt.textconv)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt textconv context=default' ] 44 | [ "$(git config --get merge.crypt.driver)" = '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt merge context=default %O %A %B %L %P' ] 45 | 46 | [ "$(git config --get filter.crypt.required)" = "true" ] 47 | [ "$(git config --get diff.crypt.cachetextconv)" = "true" ] 48 | [ "$(git config --get diff.crypt.binary)" = "true" ] 49 | [ "$(git config --get merge.renormalize)" = "true" ] 50 | [ "$(git config --get merge.crypt.name)" = "Merge transcrypt secret files" ] 51 | 52 | [ "$(git config --get alias.ls-crypt)" = '!"$(git config transcrypt.crypt-dir 2>/dev/null || printf %s/crypt ""$(git rev-parse --git-dir)"")"/transcrypt --list' ] 53 | 54 | [ "$(git config --get alias.add-crypt)" = '!"$(git config transcrypt.crypt-dir 2>/dev/null || printf %s/crypt ""$(git rev-parse --git-dir)"")"/transcrypt --add' ] 55 | } 56 | 57 | @test "init: show details for --display" { 58 | init_transcrypt 59 | VERSION=$(../transcrypt -v | awk '{print $2}') 60 | 61 | run ../transcrypt --display 62 | [ "$status" -eq 0 ] 63 | [[ "${output}" = *"The current repository was configured using transcrypt version $VERSION"* ]] 64 | [[ "${output}" = *" CIPHER: aes-256-cbc"* ]] 65 | [[ "${output}" = *" PASSWORD: abc 123"* ]] 66 | [[ "${output}" = *" transcrypt -c aes-256-cbc -p 'abc 123'"* ]] 67 | } 68 | 69 | @test "init: show details for -d" { 70 | init_transcrypt 71 | VERSION=$(../transcrypt -v | awk '{print $2}') 72 | 73 | run ../transcrypt -d 74 | [ "$status" -eq 0 ] 75 | [[ "${output}" = *"The current repository was configured using transcrypt version $VERSION"* ]] 76 | [[ "${output}" = *" CIPHER: aes-256-cbc"* ]] 77 | [[ "${output}" = *" PASSWORD: abc 123"* ]] 78 | [[ "${output}" = *" transcrypt -c aes-256-cbc -p 'abc 123'"* ]] 79 | } 80 | 81 | @test "init: respects core.hooksPath setting" { 82 | git config core.hooksPath ".git/myhooks" 83 | [ "$(git config --get core.hooksPath)" = '.git/myhooks' ] 84 | 85 | init_transcrypt 86 | [ -d .git/myhooks ] 87 | [ -f .git/myhooks/pre-commit ] 88 | 89 | VERSION=$(../transcrypt -v | awk '{print $2}') 90 | run ../transcrypt --display 91 | [ "$status" -eq 0 ] 92 | [[ "${output}" = *"The current repository was configured using transcrypt version $VERSION"* ]] 93 | [[ "${output}" = *" CIPHER: aes-256-cbc"* ]] 94 | [[ "${output}" = *" PASSWORD: abc 123"* ]] 95 | [[ "${output}" = *" transcrypt -c aes-256-cbc -p 'abc 123'"* ]] 96 | } 97 | 98 | @test "init: transcrypt.openssl-path config setting defaults to 'openssl'" { 99 | init_transcrypt 100 | [ "$(git config --get transcrypt.openssl-path)" = 'openssl' ] 101 | } 102 | 103 | @test "init: --set-openssl-path is applied during init" { 104 | run ../transcrypt --cipher=aes-256-cbc --password='abc 123' --yes --set-openssl-path=/test/path 105 | [ "$(git config --get transcrypt.openssl-path)" = "/test/path" ] 106 | } 107 | 108 | @test "init: --set-openssl-path is applied during upgrade" { 109 | init_transcrypt 110 | [ "$(git config --get transcrypt.openssl-path)" = 'openssl' ] 111 | 112 | # Set openssl path 113 | FULL_OPENSSL_PATH=$(which openssl) 114 | 115 | run ../transcrypt --upgrade --yes --set-openssl-path="$FULL_OPENSSL_PATH" 116 | [ "$(git config --get transcrypt.openssl-path)" = "$FULL_OPENSSL_PATH" ] 117 | [ ! "$(git config --get transcrypt.openssl-path)" = 'openssl' ] 118 | } 119 | 120 | @test "init: transcrypt.openssl-path config setting is retained with --upgrade" { 121 | init_transcrypt 122 | [ "$(git config --get transcrypt.openssl-path)" = 'openssl' ] 123 | 124 | # Set openssl path 125 | FULL_OPENSSL_PATH=$(which openssl) 126 | run ../transcrypt --set-openssl-path="$FULL_OPENSSL_PATH"'' --yes 127 | 128 | # Retain transcrypt.openssl-path config setting on upgrade 129 | run ../transcrypt --upgrade --yes 130 | [ "$(git config --get transcrypt.openssl-path)" = "$FULL_OPENSSL_PATH" ] 131 | [ ! "$(git config --get transcrypt.openssl-path)" = 'openssl' ] 132 | } 133 | 134 | @test "init: transcrypt.crypt-dir config setting is applied during init" { 135 | # Clear tmp crypt/ directory, in case junk was left there from prior test runs 136 | rm -fR /tmp/crypt/ 137 | 138 | # Set a custom location for the crypt/ directory 139 | git config transcrypt.crypt-dir /tmp/crypt 140 | 141 | init_transcrypt 142 | 143 | # Confirm crypt/ directory is populated in custom location 144 | [ ! -d .git/crypt ] 145 | [ ! -f .git/crypt/transcrypt ] 146 | [ -d /tmp/crypt ] 147 | [ -f /tmp/crypt/transcrypt ] 148 | } 149 | 150 | @test "crypt: transcrypt.crypt-dir config setting produces working scripts" { 151 | # Clear tmp crypt/ directory, in case junk was left there from prior test runs 152 | rm -fR /tmp/crypt/ 153 | 154 | # Set a custom location for the crypt/ directory 155 | git config transcrypt.crypt-dir /tmp/crypt 156 | 157 | init_transcrypt 158 | 159 | SECRET_CONTENT="My secret content" 160 | SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/qiCg" 161 | 162 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 163 | 164 | run cat sensitive_file 165 | [ "$status" -eq 0 ] 166 | [ "${lines[0]}" = "$SECRET_CONTENT" ] 167 | 168 | run ../transcrypt --show-raw sensitive_file 169 | [ "$status" -eq 0 ] 170 | [ "${lines[0]}" = "==> sensitive_file <==" ] 171 | [ "${lines[1]}" = "$SECRET_CONTENT_ENC" ] 172 | } 173 | 174 | @test "crypt: warn on incorrect password as indicated by dirty files after init" { 175 | init_transcrypt 176 | 177 | SECRET_CONTENT="My secret content" 178 | SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/qiCg" 179 | 180 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 181 | 182 | # Clear the password and reset the repo 183 | uninstall_transcrypt 184 | git reset --hard 185 | 186 | # Init transcrypt with wrong password, command fails with error message 187 | run "$BATS_TEST_DIRNAME"/../transcrypt --cipher=aes-256-cbc --password='WRONG' --yes 188 | [ "$status" -eq 1 ] 189 | [ "${lines[0]}" = "transcrypt: Unexpected new dirty files in the repository when configured by transcrypt, please check your password." ] 190 | } 191 | 192 | @test "crypt: warn on incorrect password as indicated by dirty files after init when forced" { 193 | init_transcrypt 194 | 195 | SECRET_CONTENT="My secret content" 196 | SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/qiCg" 197 | 198 | encrypt_named_file sensitive_file "$SECRET_CONTENT" 199 | 200 | # Clear the password and reset the repo 201 | uninstall_transcrypt 202 | git reset --hard 203 | 204 | # Dirty repo before init, to check pre- and post-init dirty files counts 205 | # work despite the pre-existing dirty file 206 | echo "Dirty file" > dirty_file 207 | git add dirty_file 208 | 209 | # Force init transcrypt with wrong password, command fails with error message 210 | run "$BATS_TEST_DIRNAME"/../transcrypt --force --cipher=aes-256-cbc --password='WRONG' --yes 211 | [ "$status" -eq 1 ] 212 | [ "${lines[0]}" = "transcrypt: Unexpected new dirty files in the repository when configured by transcrypt, please check your password." ] 213 | 214 | rm dirty_file 215 | } 216 | -------------------------------------------------------------------------------- /tests/test_merge.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/_test_helper.bash" 4 | 5 | @test "merge: branches with encrypted file - addition, no conflict" { 6 | echo "1. First step" > sensitive_file 7 | encrypt_named_file sensitive_file 8 | 9 | git checkout -b branch-2 10 | echo "2. Second step" >> sensitive_file 11 | git add sensitive_file 12 | git commit -m "Add line 2" 13 | 14 | git checkout - 15 | git merge branch-2 16 | 17 | run cat sensitive_file 18 | [ "$status" -eq 0 ] 19 | [ "${lines[0]}" = "1. First step" ] 20 | [ "${lines[1]}" = "2. Second step" ] 21 | } 22 | 23 | @test "merge: branches with encrypted file - line change incoming branch, no conflict" { 24 | echo "1. First step" > sensitive_file 25 | encrypt_named_file sensitive_file 26 | 27 | git checkout -b branch-2 28 | echo "1. Step the first" > sensitive_file # Cause line conflict 29 | echo "2. Second step" >> sensitive_file 30 | git add sensitive_file 31 | git commit -m "Add line 2, change line 1" 32 | 33 | git checkout - 34 | run git merge branch-2 35 | [ "$status" -eq 0 ] 36 | 37 | run cat sensitive_file 38 | [ "$status" -eq 0 ] 39 | [ "${lines[0]}" = "1. Step the first" ] 40 | [ "${lines[1]}" = "2. Second step" ] 41 | } 42 | 43 | @test "merge: branches with encrypted file - line changes both branches, no conflict" { 44 | echo "1. First step" > sensitive_file 45 | echo "2. Second step" >> sensitive_file 46 | encrypt_named_file sensitive_file 47 | 48 | git checkout -b branch-2 49 | echo "1. Step the first" > sensitive_file # Cause line conflict 50 | echo "2. Second step" >> sensitive_file 51 | git add sensitive_file 52 | git commit -m "Change line 1" 53 | 54 | git checkout - 55 | 56 | echo "1. First step" > sensitive_file 57 | echo "2. Second step" >> sensitive_file 58 | echo "3. Third step" >> sensitive_file 59 | git add sensitive_file 60 | git commit -m "Add line 3" 61 | 62 | run git merge branch-2 63 | [ "$status" -eq 0 ] 64 | 65 | run cat sensitive_file 66 | [ "$status" -eq 0 ] 67 | [ "${lines[0]}" = "1. Step the first" ] 68 | [ "${lines[1]}" = "2. Second step" ] 69 | [ "${lines[2]}" = "3. Third step" ] 70 | } 71 | 72 | @test "merge: branches with encrypted file - line changes both branches, with conflicts" { 73 | echo "1. First step" > sensitive_file 74 | encrypt_named_file sensitive_file 75 | 76 | git checkout -b branch-2 77 | echo "1. Step the first" > sensitive_file # Cause line conflict 78 | echo "2. Second step" >> sensitive_file 79 | git add sensitive_file 80 | git commit -m "Add line 2, change line 1" 81 | 82 | git checkout - 83 | echo "a. First step" > sensitive_file 84 | git add sensitive_file 85 | git commit -m "Change line 1 in original branch to set up conflict" 86 | 87 | run git merge branch-2 88 | [ "$status" -ne 0 ] 89 | [[ "${output}" = *"CONFLICT (content): Merge conflict in sensitive_file"* ]] 90 | 91 | run cat sensitive_file 92 | [ "$status" -eq 0 ] 93 | [ "${lines[0]}" = "<<<<<<< main" ] 94 | [ "${lines[1]}" = "a. First step" ] 95 | [ "${lines[2]}" = "=======" ] 96 | [ "${lines[3]}" = "1. Step the first" ] 97 | [ "${lines[4]}" = "2. Second step" ] 98 | [ "${lines[5]}" = ">>>>>>> branch-2" ] 99 | } 100 | -------------------------------------------------------------------------------- /tests/test_not_inited.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/_test_helper.bash" 4 | 5 | # Custom setup: don't init transcrypt 6 | # We need to init and tear down Git repo for these tests, mainly to avoid 7 | # falling back to the transcrypt repo's Git config and partial transcrypt setup 8 | # shellcheck disable=SC2034 9 | SETUP_SKIP_INIT_TRANSCRYPT=1 10 | 11 | 12 | # Operations that should work in a repo not yet initialised 13 | 14 | @test "not inited: show help for --help" { 15 | run ../transcrypt --help 16 | [ "${lines[1]}" = " transcrypt -- transparently encrypt files within a git repository" ] 17 | } 18 | 19 | @test "not inited: show help for -h" { 20 | run ../transcrypt -h 21 | [ "${lines[1]}" = " transcrypt -- transparently encrypt files within a git repository" ] 22 | } 23 | 24 | @test "not inited: show version for --version" { 25 | VERSION=$(../transcrypt -v | awk '{print $2}') 26 | run ../transcrypt --version 27 | [ "${lines[0]}" = "transcrypt $VERSION" ] 28 | } 29 | 30 | @test "not inited: show version for -v" { 31 | VERSION=$(../transcrypt -v | awk '{print $2}') 32 | run ../transcrypt -v 33 | [ "${lines[0]}" = "transcrypt $VERSION" ] 34 | } 35 | 36 | @test "not inited: no files listed for --list" { 37 | run ../transcrypt --list 38 | if [[ "${output}" = *"WARNING"* ]]; then 39 | [ "${lines[0]}" = "*** WARNING : deprecated key derivation used." ] 40 | [ "${lines[1]}" = "Using -iter or -pbkdf2 would be better." ] 41 | else 42 | [ "${lines[0]}" = "" ] 43 | fi 44 | } 45 | 46 | @test "not inited: no files listed for -l" { 47 | run ../transcrypt -l 48 | if [[ "${output}" = *"WARNING"* ]]; then 49 | [ "${lines[0]}" = "*** WARNING : deprecated key derivation used." ] 50 | [ "${lines[1]}" = "Using -iter or -pbkdf2 would be better." ] 51 | else 52 | [ "${lines[0]}" = "" ] 53 | fi 54 | } 55 | 56 | 57 | # Operations that should not work in a repo not yet initialised 58 | 59 | @test "not inited: error on --display" { 60 | run ../transcrypt --display 61 | [ "$status" -ne 0 ] 62 | [[ "${output}" = *"transcrypt: the current repository is not configured"* ]] 63 | } 64 | 65 | @test "not inited: error on -d" { 66 | run ../transcrypt -d 67 | [ "$status" -ne 0 ] 68 | [[ "${output}" = *"transcrypt: the current repository is not configured"* ]] 69 | } 70 | 71 | @test "not inited: error on --uninstall" { 72 | run ../transcrypt --uninstall 73 | [ "$status" -ne 0 ] 74 | [[ "${output}" = *"transcrypt: the current repository is not configured"* ]] 75 | } 76 | 77 | @test "not inited: error on -u" { 78 | run ../transcrypt -u 79 | [ "$status" -ne 0 ] 80 | [[ "${output}" = *"transcrypt: the current repository is not configured"* ]] 81 | } 82 | 83 | 84 | @test "not inited: error on --upgrade" { 85 | run ../transcrypt --upgrade 86 | [ "$status" -ne 0 ] 87 | [[ "${output}" = *"transcrypt: the current repository is not configured"* ]] 88 | } 89 | -------------------------------------------------------------------------------- /tests/test_pre_commit.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/_test_helper.bash" 4 | 5 | @test "pre-commit: pre-commit hook installed on init" { 6 | # Confirm pre-commit-crypt file is installed 7 | [ -f .git/hooks/pre-commit-crypt ] 8 | run cat .git/hooks/pre-commit-crypt 9 | [ "${lines[1]}" = '# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64' ] 10 | 11 | # Confirm hook is also installed/activated at pre-commit file name 12 | [ -f .git/hooks/pre-commit ] 13 | run cat .git/hooks/pre-commit 14 | [ "${lines[1]}" = '# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64' ] 15 | } 16 | 17 | @test "pre-commit: permit commit of encrypted file with encrypted content" { 18 | echo "Secret stuff" > sensitive_file 19 | encrypt_named_file sensitive_file 20 | 21 | echo " and more secrets" >> sensitive_file 22 | git add sensitive_file 23 | run git commit -m "Added more" 24 | [ "$status" -eq 0 ] 25 | 26 | run git log --format=oneline 27 | [ "$status" -eq 0 ] 28 | [[ "${lines[0]}" = *"Added more" ]] 29 | [[ "${lines[1]}" = *"Encrypt file \"sensitive_file\"" ]] 30 | } 31 | 32 | @test "pre-commit: reject commit of encrypted file with unencrypted content" { 33 | echo "Secret stuff" > sensitive_file 34 | encrypt_named_file sensitive_file 35 | 36 | echo " and more secrets" >> sensitive_file 37 | 38 | # Disable file's crypt config in .gitattributes, add change, then re-enable 39 | echo "" > .gitattributes 40 | git add sensitive_file 41 | echo "sensitive_file filter=crypt diff=crypt merge=crypt" > .gitattributes 42 | 43 | # Confirm the pre-commit rejects plain text content in what should be 44 | # an encrypted file 45 | run git commit -m "Added more" 46 | [ "$status" -ne 0 ] 47 | [[ "${output}" = *"Transcrypt managed file is not encrypted in the Git index: sensitive_file"* ]] 48 | [[ "${output}" = *"You probably staged this file using a tool that does not apply .gitattribute filters as required by Transcrypt."* ]] 49 | [[ "${output}" = *"Fix this by re-staging the file with a compatible tool or with Git on the command line:"* ]] 50 | [[ "${output}" = *" git rm --cached -- sensitive_file"* ]] 51 | [[ "${output}" = *" git add sensitive_file"* ]] 52 | } 53 | 54 | @test "pre-commit: pre-commit hook ignores symlinks to encrypted files" { 55 | echo "Secret stuff" > sensitive_file 56 | encrypt_named_file sensitive_file 57 | 58 | ln -s sensitive_file symlink_to_sensitive_file 59 | echo "\"symlink_to_sensitive_file\" filter=crypt diff=crypt merge=crypt" >> .gitattributes 60 | git add .gitattributes symlink_to_sensitive_file 61 | 62 | git commit -m "Commit symlink to encrypted file" 63 | [ "$status" -eq 0 ] 64 | 65 | rm symlink_to_sensitive_file 66 | } 67 | 68 | @test "pre-commit: warn and don't clobber existing pre-commit hook on init" { 69 | # Uninstall pre-existing transcrypt config from setup() 70 | run "$BATS_TEST_DIRNAME"/../transcrypt --uninstall --yes 71 | 72 | # Create a pre-existing pre-commit hook 73 | touch .git/hooks/pre-commit 74 | 75 | run "$BATS_TEST_DIRNAME"/../transcrypt --cipher=aes-256-cbc --password='abc 123' --yes 76 | [ "$status" -eq 0 ] 77 | [[ "${output}" = *"WARNING:"* ]] 78 | [[ "${output}" = *"Cannot install Git pre-commit hook script because file already exists: .git/hooks/pre-commit"* ]] 79 | [[ "${output}" = *"Please manually install the pre-commit script saved as: .git/hooks/pre-commit-crypt"* ]] 80 | 81 | # Confirm pre-commit-crypt file is installed, but not copied to pre-commit 82 | run cat .git/hooks/pre-commit-crypt 83 | [ "$status" -eq 0 ] 84 | [[ "${output}" = *'# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64'* ]] 85 | [ ! -s .git/hooks/pre-commit ] # Zero file size] 86 | } 87 | 88 | @test "pre-commit: de-activate and remove transcrypt's pre-commit hook" { 89 | "$BATS_TEST_DIRNAME"/../transcrypt --uninstall --yes 90 | [ ! -f .git/hooks/pre-commit ] 91 | [ ! -f .git/hooks/pre-commit-crypt ] 92 | } 93 | 94 | @test "pre-commit: warn and don't delete customised pre-commit hook on uninstall" { 95 | # Customise transcrypt's pre-commit hook 96 | echo "#" >> .git/hooks/pre-commit 97 | 98 | run "$BATS_TEST_DIRNAME"/../transcrypt --uninstall --yes 99 | [ "$status" -eq 0 ] 100 | [[ "${output}" = *'WARNING: Cannot safely disable Git pre-commit hook .git/hooks/pre-commit please check it yourself'* ]] 101 | [ -f .git/hooks/pre-commit ] 102 | [ ! -f .git/hooks/pre-commit-crypt ] 103 | } 104 | -------------------------------------------------------------------------------- /transcrypt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # 5 | # transcrypt - https://github.com/elasticdog/transcrypt 6 | # 7 | # A script to configure transparent encryption of sensitive files stored in 8 | # a Git repository. It utilizes OpenSSL's symmetric cipher routines and follows 9 | # the gitattributes(5) man page regarding the use of filters. 10 | # 11 | # Copyright (c) 2019-2025 James Murty 12 | # Copyright (c) 2014-2020 Aaron Bull Schaefer 13 | # This source code is provided under the terms of the MIT License 14 | # that can be be found in the LICENSE file. 15 | # 16 | 17 | ##### OVERRIDES 18 | 19 | # Disable any global grep options, to avoid problems like '--colors=always' 20 | # shellcheck disable=SC2034 21 | GREP_OPTIONS="" 22 | 23 | ##### CONSTANTS 24 | 25 | # the release version of this script 26 | readonly VERSION='2.3.2-pre' 27 | 28 | # the default cipher to utilize 29 | readonly DEFAULT_CIPHER='aes-256-cbc' 30 | 31 | # context name must match this regexp to ensure it is safe for git config and attrs 32 | readonly CONTEXT_REGEX='[a-z](-?[a-z0-9])*' 33 | 34 | ##### FUNCTIONS 35 | 36 | # load encryption password 37 | # by default is stored in git config, modify this function to move elsewhere 38 | load_password() { 39 | local context_config_group=${1:-} 40 | local password 41 | password=$(git config --get --local "transcrypt${context_config_group}.password") 42 | echo "$password" 43 | } 44 | 45 | # save encryption password 46 | # by default is stored in git config, modify this function to move elsewhere 47 | save_password() { 48 | local password=$1 49 | local context_config_group=${2:-} 50 | git config "transcrypt${context_config_group}.password" "$password" 51 | } 52 | 53 | # print a canonicalized absolute pathname 54 | realpath() { 55 | local path=$1 56 | 57 | # make path absolute 58 | local abspath=$path 59 | if [[ -n ${abspath##/*} ]]; then 60 | abspath=$(pwd -P)/$abspath 61 | fi 62 | 63 | # canonicalize path 64 | local dirname= 65 | if [[ -d $abspath ]]; then 66 | dirname=$(cd "$abspath" >/dev/null && pwd -P) 67 | abspath=$dirname 68 | elif [[ -e $abspath ]]; then 69 | dirname=$(cd "${abspath%/*}/" >/dev/null 2>/dev/null && pwd -P) 70 | abspath=$dirname/${abspath##*/} 71 | fi 72 | 73 | if [[ -d $dirname && -e $abspath ]]; then 74 | printf '%s\n' "$abspath" 75 | else 76 | printf 'invalid path: %s\n' "$path" >&2 77 | exit 1 78 | fi 79 | } 80 | 81 | # establish repository metadata and directory handling 82 | # shellcheck disable=SC2155 83 | gather_repo_metadata() { 84 | # whether or not transcrypt is already configured 85 | readonly INSTALLED_VERSION=$(git config --get --local transcrypt.version 2>/dev/null) 86 | 87 | # the current git repository's top-level directory 88 | readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null) 89 | 90 | # whether or not a HEAD revision exists 91 | readonly HEAD_EXISTS=$(git rev-parse --verify --quiet HEAD 2>/dev/null) 92 | 93 | # https://github.com/RichiH/vcsh 94 | # whether or not the git repository is running under vcsh 95 | readonly IS_VCSH=$(git config --get --local --bool vcsh.vcsh 2>/dev/null) 96 | 97 | # whether or not the git repository is bare 98 | readonly IS_BARE=$(git rev-parse --is-bare-repository 2>/dev/null || printf 'false') 99 | 100 | # the current git repository's .git directory 101 | readonly RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '') 102 | readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null) 103 | 104 | # Respect transcrypt.crypt-dir if present. Default to crypt/ in Git dir 105 | readonly CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}") 106 | 107 | # respect core.hooksPath setting, without trailing slash. Fall back to default hooks dir 108 | readonly GIT_HOOKS=$(git config core.hooksPath | sed 's:/*$::' 2>/dev/null || printf "%s/hooks" "${RELATIVE_GIT_DIR}") 109 | 110 | # the current git repository's gitattributes file 111 | local CORE_ATTRIBUTES 112 | CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile 2>/dev/null || git config --get --path core.attributesFile 2>/dev/null || printf '') 113 | if [[ $CORE_ATTRIBUTES ]]; then 114 | readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES 115 | elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then 116 | readonly GIT_ATTRIBUTES="${GIT_DIR}/info/attributes" 117 | else 118 | readonly GIT_ATTRIBUTES="${REPO}/.gitattributes" 119 | fi 120 | 121 | # fetch list of context names already configured or in .gitattributes 122 | readonly CONFIGURED_CONTEXTS=$(get_contexts_from_git_config) 123 | readonly GITATTRIBUTES_CONTEXTS=$(get_contexts_from_git_attributes) 124 | } 125 | 126 | # print a message to stderr 127 | warn() { 128 | local fmt="$1" 129 | shift 130 | # shellcheck disable=SC2059 131 | printf "transcrypt: $fmt\n" "$@" >&2 132 | } 133 | 134 | # print a message to stderr and exit with either 135 | # the given status or that of the most recent command 136 | die() { 137 | local st="$?" 138 | if [[ "$1" != *[^0-9]* ]]; then 139 | st="$1" 140 | shift 141 | fi 142 | warn "$@" 143 | exit "$st" 144 | } 145 | 146 | # return context name if $1 has format 'context=name-of-context' else empty string 147 | extract_context_name_from_name_value_arg() { 148 | local before_last_equals=${1%=*} 149 | local after_first_equals=${1#*=} 150 | if [[ "$before_last_equals" == "context" ]]; then 151 | echo "$after_first_equals" 152 | fi 153 | echo '' 154 | } 155 | 156 | derive_context_config_group() { 157 | local context=${1:-} 158 | if [[ ! $context ]] || [[ $context == 'default' ]]; then 159 | echo '' 160 | else 161 | echo ".$context" # Note leading period 162 | fi 163 | } 164 | 165 | # Internal function that returns a list of filenames for encrypted files in the 166 | # repo, where the filenames are verbatim and not quoted in any way even if they 167 | # contain unusual characters like double-quotes, backslash and control 168 | # characters. We must avoid quoting of filenames to support names containing 169 | # double quotes. #173 170 | _list_encrypted_files() { 171 | local strict_context=${1:-} 172 | 173 | IFS=$'\n' 174 | # List files with -z option to disable quoting of filenames, then filter 175 | # for files marked for encryption (git check-attr + grep), then only keep 176 | # filenames (sed) then evaluate escaped characters like double-quotes, 177 | # backslash and control characters (eval) which are part of the output 178 | # regardless of core.quotePath=false as per 179 | # https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath 180 | git -c core.quotePath=false ls-files -z | tr '\0' '\n' | 181 | git -c core.quotePath=false check-attr filter --stdin 2>/dev/null | 182 | { 183 | # Only output names of encrypted files matching the context, either 184 | # strictly (if $1 = "true") or loosely (if $1 is false or unset) 185 | if [[ "$strict_context" == "true" ]]; then 186 | grep ": filter: crypt${CONTEXT_CRYPT_SUFFIX:-}$" || true 187 | else 188 | grep ": filter: crypt${CONTEXT_CRYPT_SUFFIX:-}.*$" || true 189 | fi 190 | } | 191 | sed "s|: filter: crypt${CONTEXT_CRYPT_SUFFIX:-}.*||" | 192 | while read -r file; do eval "echo $file"; done 193 | } 194 | 195 | # Detect OpenSSL major version 3 or later which requires a compatibility 196 | # work-around to include the prefix 'Salted__' and salt value when encrypting. 197 | # 198 | # Note that the LibreSSL project's version of the openssl command does NOT 199 | # require this work-around for major version 3. 200 | # 201 | # See #133 #147 202 | is_salt_prefix_workaround_required() { 203 | openssl_path=$(git config --get --local transcrypt.openssl-path 2>/dev/null || printf '%s' "$openssl_path") 204 | 205 | openssl_project=$($openssl_path version | cut -d' ' -f1) 206 | openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1) 207 | 208 | if [ "$openssl_project" == "OpenSSL" ] && [ "$openssl_major_version" -ge "3" ]; then 209 | echo 'true' 210 | else 211 | echo '' 212 | fi 213 | } 214 | 215 | # The `decryption -> encryption` process on an unchanged file must be 216 | # deterministic for everything to work transparently. To do that, the same 217 | # salt must be used each time we encrypt the same file. An HMAC has been 218 | # proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file 219 | # (keyed with a combination of the filename and transcrypt password), and 220 | # then use the last 16 bytes of that HMAC for the file's unique salt. 221 | 222 | # shellcheck disable=SC2155 223 | readonly IS_PRINTF_BIN_SUPPORTED=$([[ "$(echo -n "41" | sed "s/../\\\\x&/g" | xargs -0 printf "%b")" == "A" ]] && echo 'true' || echo 'false') 224 | 225 | # Apply one of three methods to convert a hex string to binary data, or 226 | hex_to_bin() { 227 | # alternative 1 but xxd often only comes with a vim install 228 | if command -v "xxd" >/dev/null; then 229 | xxd -r -p 230 | # alternative 2, but requires printf that supports "%b" 231 | # (macOS /usr/bin/printf doesn't) 232 | elif $IS_PRINTF_BIN_SUPPORTED; then 233 | sed "s/../\\\\x&/g" | xargs -0 printf "%b" 234 | # alternative 3 as perl is fairly common 235 | elif command -v "perl" >/dev/null; then 236 | perl -pe "s/([0-9A-Fa-f]{2})/chr(hex(\$1))/eg" 237 | else 238 | die 'required command not found: xxd, or printf that supports "%%b", or perl' 239 | fi 240 | } 241 | 242 | git_clean() { 243 | context=$(extract_context_name_from_name_value_arg "$1") 244 | [[ "$context" ]] && shift 245 | 246 | filename=$1 247 | # ignore empty files 248 | if [[ ! -s $filename ]]; then 249 | return 250 | fi 251 | # cache STDIN to test if it's already encrypted 252 | tempfile=$(mktemp 2>/dev/null || mktemp -t tmp) 253 | trap 'rm -f "$tempfile"' EXIT 254 | tee "$tempfile" &>/dev/null 255 | # the first bytes of an encrypted file are always "Salted" in Base64 256 | # The `head + LC_ALL=C tr` command handles binary data in old and new Bash (#116) 257 | firstbytes=$(head -c8 "$tempfile" | LC_ALL=C tr -d '\0') 258 | if [[ $firstbytes == "U2FsdGVk" ]]; then 259 | cat "$tempfile" 260 | else 261 | context_config_group=$(derive_context_config_group "$context") 262 | cipher=$(git config --get --local "transcrypt${context_config_group}.cipher") 263 | password=$(load_password "$context_config_group") 264 | openssl_path=$(git config --get --local transcrypt.openssl-path) 265 | salt=$("${openssl_path}" dgst -hmac "${filename}:${password}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16) 266 | 267 | if [ "$(is_salt_prefix_workaround_required)" == "true" ]; then 268 | # Encrypt the file to base64, ensuring it includes the prefix 'Salted__' with the salt. #133 269 | ( 270 | echo -n "Salted__" && echo -n "$salt" | hex_to_bin && 271 | # Encrypt file to binary ciphertext 272 | ENC_PASS=$password "$openssl_path" enc -e "-${cipher}" -md MD5 -pass env:ENC_PASS -S "$salt" -in "$tempfile" 273 | ) | 274 | openssl base64 275 | else 276 | # Encrypt file to base64 ciphertext 277 | ENC_PASS=$password "$openssl_path" enc -e -a "-${cipher}" -md MD5 -pass env:ENC_PASS -S "$salt" -in "$tempfile" 278 | fi 279 | fi 280 | } 281 | 282 | git_smudge() { 283 | tempfile=$(mktemp 2>/dev/null || mktemp -t tmp) 284 | trap 'rm -f "$tempfile"' EXIT 285 | context=$(extract_context_name_from_name_value_arg "$1") 286 | context_config_group=$(derive_context_config_group "$context") 287 | cipher=$(git config --get --local "transcrypt${context_config_group}.cipher") 288 | password=$(load_password "$context_config_group") 289 | openssl_path=$(git config --get --local transcrypt.openssl-path) 290 | tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc -d "-${cipher}" -md MD5 -pass env:ENC_PASS -a 2>/dev/null || cat "$tempfile" 291 | } 292 | 293 | git_textconv() { 294 | context=$(extract_context_name_from_name_value_arg "$1") 295 | [[ "$context" ]] && shift 296 | 297 | filename=$1 298 | # ignore empty files 299 | if [[ ! -s $filename ]]; then 300 | return 301 | fi 302 | 303 | context_config_group=$(derive_context_config_group "$context") 304 | cipher=$(git config --get --local "transcrypt${context_config_group}.cipher") 305 | password=$(load_password "$context_config_group") 306 | openssl_path=$(git config --get --local transcrypt.openssl-path) 307 | ENC_PASS=$password "$openssl_path" enc -d "-${cipher}" -md MD5 -pass env:ENC_PASS -a -in "$filename" 2>/dev/null || cat "$filename" 308 | } 309 | 310 | # shellcheck disable=SC2005,SC2002,SC2181 311 | git_merge() { 312 | context=$(extract_context_name_from_name_value_arg "$1") 313 | [[ "$context" ]] && shift 314 | 315 | # Get path to transcrypt in this script's directory 316 | TRANSCRYPT_PATH="$(dirname "$0")/transcrypt" 317 | # Look up name of local branch/ref to which changes are being merged 318 | OURS_LABEL=$(git rev-parse --abbrev-ref HEAD) 319 | # Look up name of the incoming "theirs" branch/ref being merged in. 320 | # TODO There must be a better way of doing this than relying on this reflog 321 | # action environment variable, but I don't know what it is 322 | if [[ "${GIT_REFLOG_ACTION:-}" = "merge "* ]]; then 323 | THEIRS_LABEL=$(echo "$GIT_REFLOG_ACTION" | awk '{print $2}') 324 | fi 325 | if [[ ! "${THEIRS_LABEL:-}" ]]; then 326 | THEIRS_LABEL="theirs" 327 | fi 328 | # Decrypt BASE $1, LOCAL $2, and REMOTE $3 versions of file being merged 329 | echo "$(cat "$1" | "${TRANSCRYPT_PATH}" smudge context="$context")" >"$1" 330 | echo "$(cat "$2" | "${TRANSCRYPT_PATH}" smudge context="$context")" >"$2" 331 | echo "$(cat "$3" | "${TRANSCRYPT_PATH}" smudge context="$context")" >"$3" 332 | # Merge the decrypted files to the temp file named by $2 333 | git merge-file --marker-size="$4" -L "$OURS_LABEL" -L base -L "$THEIRS_LABEL" "$2" "$1" "$3" 334 | # If the merge was not successful (has conflicts) exit with an error code to 335 | # leave the partially-merged file in place for a manual merge. 336 | if [[ "$?" != "0" ]]; then 337 | exit 1 338 | fi 339 | # If the merge was successful (no conflicts) re-encrypt the merged temp file $2 340 | # which git will then update in the index in a following "Auto-merging" step. 341 | # We must explicitly encrypt/clean the file, rather than leave Git to do it, 342 | # because we can otherwise trigger safety check failure errors like: 343 | # error: add_cacheinfo failed to refresh for path 'FILE'; merge aborting. 344 | # To re-encrypt we must first copy the merged file to $5 (the name of the 345 | # working-copy file) so the crypt `clean` script can generate the correct hash 346 | # salt based on the file's real name, instead of the $2 temp file name. 347 | cp "$2" "$5" 348 | # Now we use the `clean` script to encrypt the merged file contents back to the 349 | # temp file $2 where Git expects to find the merge result content. 350 | cat "$5" | "${TRANSCRYPT_PATH}" clean context="$context" "$5" >"$2" 351 | } 352 | 353 | # shellcheck disable=SC2155 354 | git_pre_commit() { 355 | # Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64 356 | tmp=$(mktemp) 357 | IFS=$'\n' 358 | slow_mode_if_failed() { 359 | for secret_file in $(_list_encrypted_files); do 360 | # Skip symlinks, they contain the linked target file path not plaintext 361 | if [[ -L $secret_file ]]; then 362 | continue 363 | fi 364 | 365 | # Get prefix of raw file in Git's index using the :FILENAME revision syntax 366 | local firstbytes=$(git show :"${secret_file}" | head -c8) 367 | # An empty file does not need to be, and is not, encrypted 368 | if [[ $firstbytes == "" ]]; then 369 | : # Do nothing 370 | # The first bytes of an encrypted file must be "Salted" in Base64 371 | elif [[ $firstbytes != "U2FsdGVk" ]]; then 372 | printf 'Transcrypt managed file is not encrypted in the Git index: %s\n' "$secret_file" >&2 373 | printf '\n' >&2 374 | printf 'You probably staged this file using a tool that does not apply' >&2 375 | printf ' .gitattribute filters as required by Transcrypt.\n' >&2 376 | printf '\n' >&2 377 | printf 'Fix this by re-staging the file with a compatible tool or with' 378 | printf ' Git on the command line:\n' >&2 379 | printf '\n' >&2 380 | printf ' git rm --cached -- %s\n' "$secret_file" >&2 381 | printf ' git add %s\n' "$secret_file" >&2 382 | printf '\n' >&2 383 | exit 1 384 | fi 385 | done 386 | } 387 | 388 | # validate file to see if it failed or not, We don't care about the filename currently for speed, we only care about pass/fail, slow_mode_if_failed() is for what failed. 389 | validate_file() { 390 | secret_file=${1} 391 | # Skip symlinks, they contain the linked target file path not plaintext 392 | if [[ -L $secret_file ]]; then 393 | return 394 | fi 395 | # Get prefix of raw file in Git's index using the :FILENAME revision syntax 396 | # The first bytes of an encrypted file are always "Salted" in Base64 397 | local firstbytes=$(git show :"${secret_file}" | head -c8) 398 | if [[ $firstbytes == "" ]]; then 399 | : # Do nothing 400 | elif [[ $firstbytes != "U2FsdGVk" ]]; then 401 | echo "true" >>"${tmp}" 402 | fi 403 | } 404 | 405 | # if bash version is greater than or equal to 4.4, fork to number of threads otherwise run normally 406 | if [[ "${BASH_VERSINFO[0]}" -gt 4 ]] || { [[ "${BASH_VERSINFO[0]}" == 4 ]] && [[ "${BASH_VERSINFO[1]}" -ge 4 ]]; }; then 407 | num_procs=$(nproc) 408 | num_jobs="\j" 409 | for secret_file in $(_list_encrypted_files); do 410 | while ((${num_jobs@P} >= num_procs)); do 411 | wait -n 412 | done 413 | validate_file "${secret_file}" & 414 | done 415 | wait 416 | if [[ -s ${tmp} ]]; then 417 | slow_mode_if_failed 418 | rm -f "${tmp}" 419 | exit 1 420 | fi 421 | else 422 | slow_mode_if_failed 423 | fi 424 | 425 | rm -f "${tmp}" 426 | unset IFS 427 | } 428 | 429 | # Add file patterns to .gitattributes 430 | add_pattern() { 431 | for var in "$@"; do 432 | line="$var filter=crypt${CONTEXT_CRYPT_SUFFIX} diff=crypt${CONTEXT_CRYPT_SUFFIX} merge=crypt${CONTEXT_CRYPT_SUFFIX}" 433 | grep -qxF "$line" "${GIT_ATTRIBUTES}" || echo "$line" >>"${GIT_ATTRIBUTES}" 434 | sync 435 | done 436 | } 437 | 438 | # verify that all requirements have been met 439 | run_safety_checks() { 440 | # validate that we're in a git repository 441 | [[ $GIT_DIR ]] || die 'you are not currently in a git repository; did you forget to run "git init"?' 442 | 443 | # Check the context name provided (or 'default') is valid 444 | full_context_regex="^${CONTEXT_REGEX}$" 445 | if [[ ! $CONTEXT =~ $full_context_regex ]]; then 446 | warn "context name '${CONTEXT}' is invalid" 447 | echo "Must be lowercase ASCII, start with a letter, then zero or more letters or numbers. A hyphen (-) seperator is allowed" 448 | echo "Examples: admin admins-only super-user staging production top-secret abc-123-xyz" 449 | exit 1 450 | fi 451 | 452 | # exit if transcrypt is not in the required state 453 | if [[ $requires_existing_config ]] && [[ ! $CONFIGURED_CONTEXTS ]]; then 454 | die 1 'the current repository is not configured' 455 | fi 456 | 457 | # check for dependencies 458 | for cmd in {column,grep,mktemp,"${openssl_path}",sed,tee}; do 459 | command -v "$cmd" >/dev/null || die 'required command "%s" was not found' "$cmd" 460 | done 461 | 462 | # check for a working method to convert a hex string to binary data 463 | if [ "$(is_salt_prefix_workaround_required)" == "true" ]; then 464 | echo -n "41" | hex_to_bin >/dev/null 465 | fi 466 | 467 | # ensure the repository is clean (if it has a HEAD revision) so we can force 468 | # checkout files without the destruction of uncommitted changes 469 | if [[ $requires_clean_repo ]] && [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then 470 | # ensure index is up-to-date before dirty check 471 | git update-index -q --really-refresh 472 | # check if the repo is dirty 473 | if ! git diff-index --quiet HEAD --; then 474 | warn 'the repo is dirty; commit or stash your changes before running transcrypt\n' 475 | # Output a human friendly summary of dirty files, with fallback to 476 | # less friendly output formats if nicer ones aren't supported 477 | git diff-index --name-status HEAD -- 2>/dev/null || 478 | git diff-index --stat HEAD -- 2>/dev/null || 479 | git diff-index HEAD -- 480 | exit 1 481 | fi 482 | fi 483 | } 484 | 485 | # unset the cipher variable if it is not supported by openssl 486 | validate_cipher() { 487 | local list_cipher_commands 488 | if "${openssl_path}" list-cipher-commands &>/dev/null; then 489 | # OpenSSL < v1.1.0 490 | list_cipher_commands="${openssl_path} list-cipher-commands" 491 | else 492 | # OpenSSL >= v1.1.0 493 | list_cipher_commands="${openssl_path} list -cipher-commands" 494 | fi 495 | 496 | local supported 497 | supported=$($list_cipher_commands | tr -s ' ' '\n' | grep -Fx "$cipher") || true 498 | if [[ ! $supported ]]; then 499 | if [[ $interactive ]]; then 500 | printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher" 501 | $list_cipher_commands | column -c 80 502 | printf '\n' 503 | cipher='' 504 | else 505 | # shellcheck disable=SC2016 506 | die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$list_cipher_commands" 507 | fi 508 | fi 509 | } 510 | 511 | # ensure we have a cipher to encrypt with 512 | get_cipher() { 513 | while [[ ! $cipher ]]; do 514 | local answer= 515 | if [[ $interactive ]]; then 516 | printf 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER" 517 | read -r answer 518 | fi 519 | 520 | # use the default cipher if the user gave no answer; 521 | # otherwise verify the given cipher is supported by openssl 522 | if [[ ! $answer ]]; then 523 | cipher=$DEFAULT_CIPHER 524 | else 525 | cipher=$answer 526 | validate_cipher 527 | fi 528 | done 529 | } 530 | 531 | # ensure we have a password to encrypt with 532 | get_password() { 533 | while [[ ! $password ]]; do 534 | local answer= 535 | if [[ $interactive ]]; then 536 | printf 'Generate a random password? [Y/n] ' 537 | read -r -n 1 -s answer 538 | printf '\n' 539 | fi 540 | 541 | # generate a random password if the user answered yes; 542 | # otherwise prompt the user for a password 543 | if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then 544 | local password_length=30 545 | local random_base64 546 | random_base64=$(${openssl_path} rand -base64 $password_length) 547 | password=$random_base64 548 | else 549 | printf 'Password: ' 550 | read -r password 551 | [[ $password ]] || printf 'no password was specified\n' 552 | fi 553 | done 554 | } 555 | 556 | # confirm the transcrypt configuration 557 | confirm_configuration() { 558 | local answer= 559 | 560 | printf '\nRepository metadata:\n\n' 561 | [[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO" 562 | printf ' GIT_DIR: %s\n' "$GIT_DIR" 563 | printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES" 564 | printf 'The following configuration will be saved:\n\n' 565 | printf ' CONTEXT: %s\n' "$CONTEXT" 566 | printf ' CIPHER: %s\n' "$cipher" 567 | printf ' PASSWORD: %s\n\n' "$password" 568 | printf 'Does this look correct? [Y/n] ' 569 | read -r -n 1 -s answer 570 | 571 | # exit if the user did not confirm 572 | if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then 573 | printf '\n\n' 574 | else 575 | printf '\n' 576 | die 1 'configuration has been aborted' 577 | fi 578 | } 579 | 580 | # confirm the rekey configuration 581 | confirm_rekey() { 582 | local answer= 583 | 584 | printf '\nRepository metadata:\n\n' 585 | [[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO" 586 | printf ' GIT_DIR: %s\n' "$GIT_DIR" 587 | printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES" 588 | printf 'The following configuration will be saved:\n\n' 589 | printf ' CONTEXT: %s\n' "$CONTEXT" 590 | printf ' CIPHER: %s\n' "$cipher" 591 | printf ' PASSWORD: %s\n\n' "$password" 592 | printf 'You are about to re-encrypt all encrypted files using new credentials.\n' 593 | printf 'Once you do this, their historical diffs will no longer display in plain text.\n\n' 594 | printf 'Proceed with rekey? [y/N] ' 595 | read -r answer 596 | 597 | # only rekey if the user explicitly confirmed 598 | if [[ $answer =~ $YES_REGEX ]]; then 599 | printf '\n' 600 | else 601 | die 1 'rekeying has been aborted' 602 | fi 603 | } 604 | 605 | # automatically stage rekeyed files in preparation for the user to commit them 606 | stage_rekeyed_files() { 607 | local encrypted_files 608 | encrypted_files=$(git ls-crypt) 609 | if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then 610 | # touch all encrypted files to prevent stale stat info 611 | cd "$REPO" >/dev/null || die 1 'could not change into the "%s" directory' "$REPO" 612 | # shellcheck disable=SC2086 613 | touch $encrypted_files 614 | # shellcheck disable=SC2086 615 | git update-index --add -- $encrypted_files 616 | 617 | printf '*** rekeyed files have been staged ***\n' 618 | printf '*** COMMIT THESE CHANGES RIGHT AWAY! ***\n\n' 619 | fi 620 | } 621 | 622 | # save helper scripts under the repository's git directory 623 | save_helper_scripts() { 624 | mkdir -p "${CRYPT_DIR}" 625 | 626 | local current_transcrypt 627 | current_transcrypt=$(realpath "$0" 2>/dev/null) 628 | cp "$current_transcrypt" "${CRYPT_DIR}/transcrypt" 629 | 630 | # make scripts executable 631 | for script in {transcrypt,}; do 632 | chmod 0755 "${CRYPT_DIR}/${script}" 633 | done 634 | } 635 | 636 | # save helper hooks under the repository's git directory 637 | save_helper_hooks() { 638 | if [[ $rekey ]]; then 639 | return 0 # Bypass helper hook installation on rekey 640 | fi 641 | 642 | # Install pre-commit-crypt hook script 643 | [[ ! -d "${GIT_HOOKS}" ]] && mkdir -p "${GIT_HOOKS}" 644 | pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt" 645 | cat <<-'EOF' >"$pre_commit_hook_installed" 646 | #!/usr/bin/env bash 647 | # Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64 648 | RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '') 649 | CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}") 650 | "${CRYPT_DIR}/transcrypt" pre_commit 651 | EOF 652 | 653 | # Activate hook by copying it to the pre-commit script name, but only if 654 | # the global pre-commit hook is not already present 655 | pre_commit_hook="${GIT_HOOKS}/pre-commit" 656 | if [[ -f "$pre_commit_hook" ]]; then 657 | # Nothing to do if our pre-commit hook is already installed 658 | hook_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook") 659 | installed_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook_installed") 660 | if [[ "$hook_md5" = "$installed_md5" ]]; then 661 | : # no-op 662 | else 663 | printf 'WARNING:\n' >&2 664 | printf 'Cannot install Git pre-commit hook script because file already exists: %s\n' "$pre_commit_hook" >&2 665 | printf 'Please manually install the pre-commit script saved as: %s\n' "$pre_commit_hook_installed" >&2 666 | printf '\n' 667 | fi 668 | else 669 | cp "$pre_commit_hook_installed" "$pre_commit_hook" 670 | chmod 0755 "$pre_commit_hook" 671 | fi 672 | } 673 | 674 | # write the configuration to the repository's git config 675 | save_configuration() { 676 | # prevent clobbering existing configuration 677 | # shellcheck disable=SC2086 678 | if [[ $upgrade ]]; then 679 | : # Bypass safety check on upgrade; we know we just called uninstall_transcrypt 680 | elif [[ $rekey ]]; then 681 | : # Bypass safety check on rekey 682 | elif is_item_in_array "$CONTEXT" ${CONFIGURED_CONTEXTS}; then 683 | if [[ "$CONTEXT" = 'default' ]]; then 684 | die 1 "the current repository is already configured; see 'transcrypt --display'" 685 | else 686 | die 1 "the current repository is already configured${CONTEXT_DESCRIPTION}; see 'transcrypt --context=$CONTEXT --display'" 687 | fi 688 | fi 689 | 690 | save_helper_scripts 691 | save_helper_hooks 692 | 693 | # write the encryption info 694 | git config transcrypt.version "$VERSION" 695 | git config "transcrypt${CONTEXT_CONFIG_GROUP}.cipher" "$cipher" 696 | save_password "$password" "$CONTEXT_CONFIG_GROUP" 697 | git config transcrypt.openssl-path "$openssl_path" 698 | 699 | # write the filter settings. Sorry for the horrific quote escaping below... 700 | # shellcheck disable=SC2016 701 | transcrypt_path='"$(git config transcrypt.crypt-dir 2>/dev/null || printf %s/crypt ""$(git rev-parse --git-dir)"")"/transcrypt' 702 | 703 | # Ensure filter attributes are always set for the default (unspecified) context 704 | git config filter.crypt.clean "$transcrypt_path clean context=default %f" 705 | git config filter.crypt.smudge "$transcrypt_path smudge context=default" 706 | git config diff.crypt.textconv "$transcrypt_path textconv context=default" 707 | git config merge.crypt.driver "$transcrypt_path merge context=default %O %A %B %L %P" 708 | 709 | # Also set filter attributes for a non-default context, if necessary 710 | if [[ ! "$CONTEXT" = 'default' ]]; then 711 | git config filter.crypt"${CONTEXT_CRYPT_SUFFIX}".clean "$transcrypt_path clean context=$CONTEXT %f" 712 | git config filter.crypt"${CONTEXT_CRYPT_SUFFIX}".smudge "$transcrypt_path smudge context=$CONTEXT" 713 | git config diff.crypt"${CONTEXT_CRYPT_SUFFIX}".textconv "$transcrypt_path textconv context=$CONTEXT" 714 | git config merge.crypt"${CONTEXT_CRYPT_SUFFIX}".driver "$transcrypt_path merge context=$CONTEXT %O %A %B %L %P" 715 | fi 716 | 717 | git config filter.crypt.required 'true' 718 | git config diff.crypt.cachetextconv 'true' 719 | git config diff.crypt.binary 'true' 720 | git config merge.renormalize 'true' 721 | git config merge.crypt.name 'Merge transcrypt secret files' 722 | 723 | # add git alias for listing ALL encrypted files regardless of context 724 | git config alias.ls-crypt "!$transcrypt_path --list" 725 | 726 | # add a git alias for listing encrypted files in specific context, including 'default' 727 | if [[ "$CONTEXT" = 'default' ]]; then 728 | # List files with gitattribute 'filter=crypt' 729 | git config alias.ls-crypt-default "!$transcrypt_path --list" 730 | else 731 | # List files with gitattribute 'filter=crypt-' 732 | git config "alias.ls-crypt-${CONTEXT}" "!$transcrypt_path --context=${CONTEXT} --list" 733 | fi 734 | 735 | # Add git alias `add-crypt` to add file pattern to .gitattributes 736 | git config alias.add-crypt "!$transcrypt_path --add" 737 | } 738 | 739 | # display the current configuration settings 740 | display_configuration() { 741 | local current_cipher 742 | current_cipher=$(git config --get --local "transcrypt${CONTEXT_CONFIG_GROUP}.cipher") 743 | local current_password 744 | current_password=$(load_password "$CONTEXT_CONFIG_GROUP") 745 | local escaped_password=${current_password//\'/\'\\\'\'} 746 | 747 | contexts_count="$(count_items_in_list "$CONFIGURED_CONTEXTS")" 748 | 749 | printf 'The current repository was configured using transcrypt version %s\n' "$INSTALLED_VERSION" 750 | printf "and has the following configuration%s:\n\n" "$CONTEXT_DESCRIPTION" 751 | [[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO" 752 | printf ' GIT_DIR: %s\n' "$GIT_DIR" 753 | printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES" 754 | printf ' CONTEXT: %s\n' "$CONTEXT" 755 | printf ' CIPHER: %s\n' "$current_cipher" 756 | printf ' PASSWORD: %s\n\n' "$current_password" 757 | if [[ "$contexts_count" -gt "1" ]]; then 758 | printf 'The repository has %s contexts: %s\n\n' "$contexts_count" "$CONFIGURED_CONTEXTS" 759 | fi 760 | printf "Copy and paste the following command to initialize a cloned repository%s:\n\n" "$CONTEXT_DESCRIPTION" 761 | if [[ $CONTEXT != 'default' ]]; then 762 | printf " transcrypt -C $CONTEXT -c %s -p '%s'\n" "$current_cipher" "$escaped_password" 763 | else 764 | printf " transcrypt -c %s -p '%s'\n" "$current_cipher" "$escaped_password" 765 | fi 766 | } 767 | 768 | # remove transcrypt-related settings from the repository's git config 769 | clean_gitconfig() { 770 | git config --remove-section transcrypt"${CONTEXT_CONFIG_GROUP}" 2>/dev/null || true 771 | 772 | git config --remove-section filter.crypt"${CONTEXT_CRYPT_SUFFIX}" 2>/dev/null || true 773 | git config --remove-section diff.crypt"${CONTEXT_CRYPT_SUFFIX}" 2>/dev/null || true 774 | git config --remove-section merge.crypt"${CONTEXT_CRYPT_SUFFIX}" 2>/dev/null || true 775 | } 776 | 777 | # Remove from the local Git DB any objects containing the cached plaintext of 778 | # secret files, created due to the setting diff.crypt.cachetextconv='true' 779 | remove_cached_plaintext() { 780 | # Delete ref to cached plaintext objects, to leave these objects 781 | # unreferenced and available for removal 782 | git update-ref -d refs/notes/textconv/crypt 783 | 784 | # Remove ANY unreferenced objects in Git's object DB (packed or unpacked), 785 | # to ensure that cached plaintext objects are also removed. 786 | # The vital sub-commands equivalents we require this `gc` command to do are: 787 | # `git prune`, `git repack -ad` 788 | git gc --prune=now --quiet 789 | } 790 | 791 | # force the checkout of any files with the crypt filter applied to them; 792 | # this will decrypt existing encrypted files if you've just cloned a repository, 793 | # or it will encrypt locally decrypted files if you've just flushed the credentials 794 | force_checkout() { 795 | # make sure a HEAD revision exists 796 | if [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then 797 | # this would normally delete uncommitted changes in the working directory, 798 | # but we already made sure the repo was clean during the safety checks 799 | local encrypted_files 800 | encrypted_files=$(git "ls-crypt-${CONTEXT}") 801 | cd "$REPO" >/dev/null || die 1 'could not change into the "%s" directory' "$REPO" 802 | IFS=$'\n' 803 | for file in $encrypted_files; do 804 | rm -f "$file" 805 | git checkout --force HEAD -- "$file" >/dev/null 806 | done 807 | unset IFS 808 | fi 809 | } 810 | 811 | # remove the locally cached encryption credentials and 812 | # re-encrypt any files that had been previously decrypted 813 | flush_credentials() { 814 | local answer= 815 | 816 | if [[ $interactive ]]; then 817 | printf 'You are about to flush the local credentials; make sure you have saved them elsewhere.\n' 818 | printf 'All previously decrypted files will revert to their encrypted form, and your\n' 819 | printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n' 820 | printf 'Proceed with credential flush? [y/N] ' 821 | read -r answer 822 | printf '\n' 823 | else 824 | # although destructive, we should support the --yes option 825 | answer='y' 826 | fi 827 | 828 | # only flush if the user explicitly confirmed 829 | if [[ $answer =~ $YES_REGEX ]]; then 830 | clean_gitconfig 831 | 832 | remove_cached_plaintext 833 | 834 | # re-encrypt any files that had been previously decrypted 835 | force_checkout 836 | 837 | # Unset the ls-crypt alias for the current context 838 | # We must do this after the force checkout, which relies on ls-crypt 839 | git config --unset alias.ls-crypt"${CONTEXT_CRYPT_SUFFIX}" 2>/dev/null || true 840 | 841 | # Also remove ls-crypt-default alias when removing default context 842 | if [[ "$CONTEXT" = 'default' ]]; then 843 | git config --unset alias.ls-crypt-default 2>/dev/null || true 844 | fi 845 | 846 | printf 'The local transcrypt credentials have been successfully flushed.\n' 847 | else 848 | die 1 'flushing of credentials has been aborted' 849 | fi 850 | } 851 | 852 | # remove all transcrypt configuration from the repository 853 | uninstall_transcrypt() { 854 | local answer= 855 | 856 | if [[ $interactive ]]; then 857 | printf 'You are about to remove all transcrypt configuration from your repository.\n' 858 | printf 'All previously encrypted files will remain decrypted in this working copy, but your\n' 859 | printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n' 860 | if [[ $(count_items_in_list "$CONFIGURED_CONTEXTS") -gt "1" ]]; then 861 | printf 'Proceed with uninstall of all contexts (%s)? [y/N] ' "$CONFIGURED_CONTEXTS" 862 | else 863 | printf 'Proceed with uninstall? [y/N] ' 864 | fi 865 | read -r answer 866 | printf '\n' 867 | else 868 | # although destructive, we should support the --yes option 869 | answer='y' 870 | fi 871 | 872 | # only uninstall if the user explicitly confirmed 873 | if [[ $answer =~ $YES_REGEX ]]; then 874 | clean_gitconfig 875 | 876 | if [[ ! $upgrade ]]; then 877 | remove_cached_plaintext 878 | fi 879 | 880 | # touch all encrypted files to prevent stale stat info 881 | local encrypted_files 882 | encrypted_files=$(git ls-crypt) 883 | if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then 884 | cd "$REPO" >/dev/null || die 1 'could not change into the "%s" directory' "$REPO" 885 | # shellcheck disable=SC2086 886 | touch $encrypted_files 887 | fi 888 | 889 | # remove helper scripts 890 | # Keep obsolete clean,smudge,textconv,merge refs here to remove them on upgrade 891 | for script in {transcrypt,clean,smudge,textconv,merge}; do 892 | [[ ! -f "${CRYPT_DIR}/${script}" ]] || rm "${CRYPT_DIR}/${script}" 893 | done 894 | [[ ! -d "${CRYPT_DIR}" ]] || rmdir "${CRYPT_DIR}" 895 | 896 | # rename helper hooks (don't delete, in case user has custom changes) 897 | pre_commit_hook="${GIT_HOOKS}/pre-commit" 898 | pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt" 899 | if [[ -f "$pre_commit_hook" ]]; then 900 | hook_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook") 901 | installed_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook_installed") 902 | if [[ "$hook_md5" = "$installed_md5" ]]; then 903 | rm "$pre_commit_hook" 904 | else 905 | printf 'WARNING: Cannot safely disable Git pre-commit hook %s please check it yourself\n' "$pre_commit_hook" 906 | fi 907 | fi 908 | [[ -f "$pre_commit_hook_installed" ]] && rm "$pre_commit_hook_installed" 909 | 910 | # remove the `git add-crypt` alias. 911 | git config --unset alias.add-crypt 2>/dev/null || true 912 | 913 | # remove context settings: cipher & password config, ls-crypt alias variant, 914 | # crypt filter/diff/merge attributes. We do it here instead of `clean_gitconfig` 915 | # to avoid interfering with flushing of credentials 916 | for context in $CONFIGURED_CONTEXTS; do 917 | git config --unset alias.ls-crypt-"${context}" 2>/dev/null || true 918 | 919 | git config --remove-section transcrypt."${context}" 2>/dev/null || true 920 | 921 | git config --remove-section filter.crypt-"${context}" 2>/dev/null || true 922 | git config --remove-section diff.crypt-"${context}" 2>/dev/null || true 923 | git config --remove-section merge.crypt-"${context}" 2>/dev/null || true 924 | done 925 | 926 | # remove the `git ls-crypt` global alias. We must do this late, because 927 | # we need it to list the files to touch above. 928 | git config --unset alias.ls-crypt 2>/dev/null || true 929 | 930 | # remove the alias section if it's now empty 931 | local alias_values 932 | alias_values=$(git config --get-regex --local 'alias\..*') || true 933 | if [[ ! $alias_values ]]; then 934 | git config --remove-section alias 2>/dev/null || true 935 | fi 936 | 937 | # unset merge.renormalize if all transcrypt configs are now removed 938 | local transcrypt_values 939 | transcrypt_values=$(git config --get-regex --local 'transcrypt\..*') || true 940 | if [[ ! $transcrypt_values ]]; then 941 | git config --unset merge.renormalize 942 | fi 943 | 944 | # remove the merge section if it's now empty 945 | local merge_values 946 | merge_values=$(git config --get-regex --local 'merge\..*') || true 947 | if [[ ! $merge_values ]]; then 948 | git config --remove-section merge 2>/dev/null || true 949 | fi 950 | 951 | # remove any defined crypt patterns in gitattributes 952 | case $OSTYPE in 953 | darwin*) 954 | /usr/bin/sed -i '' "/filter=crypt/d" "$GIT_ATTRIBUTES" 955 | ;; 956 | linux*) 957 | sed -i "/filter=crypt/d" "$GIT_ATTRIBUTES" 958 | ;; 959 | esac 960 | 961 | if [[ ! $upgrade ]]; then 962 | printf 'The transcrypt configuration has been completely removed from the repository.\n' 963 | fi 964 | else 965 | die 1 'uninstallation has been aborted' 966 | fi 967 | } 968 | 969 | # uninstall and re-install transcrypt to upgrade scripts and update configuration 970 | upgrade_transcrypt() { 971 | # Fail with an error if we cannot read the existing config 972 | if [[ ! $INSTALLED_VERSION ]]; then 973 | die 1 'no existing transcrypt configuration found' 974 | fi 975 | 976 | if [[ $interactive ]]; then 977 | printf 'You are about to upgrade the transcrypt scripts in your repository.\n' 978 | printf 'Your configuration settings will not be changed.\n\n' 979 | printf ' Current version: %s\n' "$INSTALLED_VERSION" 980 | printf 'Upgraded version: %s\n\n' "$VERSION" 981 | printf 'Proceed with upgrade? [y/N] ' 982 | read -r answer 983 | printf '\n' 984 | 985 | if [[ $answer =~ $YES_REGEX ]]; then 986 | # User confirmed, don't prompt again 987 | interactive='' 988 | else 989 | # User did not confirm, exit 990 | # Exit if user did not confirm 991 | die 1 'upgrade has been aborted' 992 | fi 993 | fi 994 | 995 | # Keep current cipher and password 996 | cipher=$(git config --get --local "transcrypt${CONTEXT_CONFIG_GROUP}.cipher") 997 | password=$(load_password "$CONTEXT_CONFIG_GROUP") 998 | # Keep current openssl-path, or set to default if no existing value 999 | openssl_path=$(git config --get --local transcrypt.openssl-path 2>/dev/null || printf '%s' "$openssl_path") 1000 | 1001 | # Keep contents of .gitattributes 1002 | ORIG_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES") 1003 | 1004 | # Keep current cipher and password for each context 1005 | ORIG_CONFIGS=() 1006 | for context_name in $CONFIGURED_CONTEXTS; do 1007 | if [[ "$context_name" = 'default' ]]; then 1008 | cipher=$(git config --get --local transcrypt.cipher) 1009 | password=$(git config --get --local transcrypt.password) 1010 | else 1011 | cipher=$(git config --get --local transcrypt."${context_name}".cipher) 1012 | password=$(git config --get --local transcrypt."${context_name}".password) 1013 | fi 1014 | ORIG_CONFIGS+=("${context_name}|${cipher}|${password}") 1015 | done 1016 | 1017 | uninstall_transcrypt 1018 | 1019 | # Reconfigure cipher and password for each context 1020 | for orig_config_line in "${ORIG_CONFIGS[@]}"; do 1021 | context=$(echo "$orig_config_line" | awk 'BEGIN { FS = "|" }; { print $1 }') 1022 | cipher=$(echo "$orig_config_line" | awk 'BEGIN { FS = "|" }; { print $2 }') 1023 | password=$(echo "$orig_config_line" | awk 'BEGIN { FS = "|" }; { print $3 }') 1024 | 1025 | set_context_globals # This prepares all the variables needed to save config 1026 | save_configuration 1027 | done 1028 | 1029 | # Re-instate contents of .gitattributes 1030 | echo "$ORIG_GITATTRIBUTES" >"$GIT_ATTRIBUTES" 1031 | 1032 | # Update .gitattributes for transcrypt'ed files to include "merge=crypt" config 1033 | case $OSTYPE in 1034 | darwin*) 1035 | /usr/bin/sed -i '' 's/diff=crypt$/diff=crypt merge=crypt/' "$GIT_ATTRIBUTES" 1036 | ;; 1037 | linux*) 1038 | sed -i 's/diff=crypt$/diff=crypt merge=crypt/' "$GIT_ATTRIBUTES" 1039 | ;; 1040 | esac 1041 | 1042 | printf 'Upgrade is complete\n' 1043 | 1044 | LATEST_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES") 1045 | if [[ "$LATEST_GITATTRIBUTES" != "$ORIG_GITATTRIBUTES" ]]; then 1046 | printf '\nYour gitattributes file has been updated with the latest recommended values.\n' 1047 | printf 'Please review and commit the new values in:\n' 1048 | printf '%s\n' "$GIT_ATTRIBUTES" 1049 | fi 1050 | } 1051 | 1052 | # list all of the currently encrypted files in the repository 1053 | list_files() { 1054 | if [[ $IS_BARE == 'false' ]]; then 1055 | cd "$REPO" >/dev/null || die 1 'could not change into the "%s" directory' "$REPO" 1056 | 1057 | if [[ -z "$CONTEXT_CRYPT_SUFFIX" ]]; then 1058 | # Non-strict listing of files when context is unset / default 1059 | _list_encrypted_files 1060 | else 1061 | # Strict listing of files when a specific context is set 1062 | _list_encrypted_files true 1063 | fi 1064 | fi 1065 | } 1066 | 1067 | # show the raw file as stored in the git commit object 1068 | show_raw_file() { 1069 | if [[ -f $show_file ]]; then 1070 | # ensure the file is currently being tracked 1071 | local escaped_file=${show_file//\//\\\/} 1072 | file_paths=$(_list_encrypted_files | grep "$escaped_file") 1073 | if [[ -z "$file_paths" ]]; then 1074 | die 1 'the file "%s" is not currently being tracked by git' "$show_file" 1075 | fi 1076 | elif [[ $show_file == '*' ]]; then 1077 | file_paths=$(_list_encrypted_files) 1078 | else 1079 | die 1 'the file "%s" does not exist' "$show_file" 1080 | fi 1081 | 1082 | IFS=$'\n' 1083 | for file in $file_paths; do 1084 | printf '==> %s <==\n' "$file" >&2 1085 | git --no-pager show HEAD:"$file" --no-textconv 1086 | printf '\n' >&2 1087 | done 1088 | unset IFS 1089 | } 1090 | 1091 | # export password and cipher to a gpg encrypted file 1092 | export_gpg() { 1093 | # check for dependencies 1094 | command -v gpg >/dev/null || die 'required command "gpg" was not found' 1095 | 1096 | # ensure the recipient key exists 1097 | if ! gpg --list-keys "$gpg_recipient" 2>/dev/null; then 1098 | die 1 'GPG recipient key "%s" does not exist' "$gpg_recipient" 1099 | fi 1100 | 1101 | local current_cipher 1102 | current_cipher=$(git config --get --local "transcrypt${CONTEXT_CONFIG_GROUP}.cipher") 1103 | local current_password 1104 | current_password=$(load_password "$CONTEXT_CONFIG_GROUP") 1105 | mkdir -p "${CRYPT_DIR}" 1106 | 1107 | local gpg_encrypt_cmd="gpg --batch --recipient $gpg_recipient --trust-model always --yes --armor --quiet --encrypt -" 1108 | printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${CRYPT_DIR}/${gpg_recipient}.asc" 1109 | printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient" 1110 | } 1111 | 1112 | # import password and cipher from a gpg encrypted file 1113 | import_gpg() { 1114 | # check for dependencies 1115 | command -v gpg >/dev/null || die 'required command "gpg" was not found' 1116 | 1117 | local path 1118 | if [[ -f "${CRYPT_DIR}/${gpg_import_file}" ]]; then 1119 | path="${CRYPT_DIR}/${gpg_import_file}" 1120 | elif [[ -f "${CRYPT_DIR}/${gpg_import_file}.asc" ]]; then 1121 | path="${CRYPT_DIR}/${gpg_import_file}.asc" 1122 | elif [[ ! -f $gpg_import_file ]]; then 1123 | die 1 'the file "%s" does not exist' "$gpg_import_file" 1124 | else 1125 | path="$gpg_import_file" 1126 | fi 1127 | 1128 | local configuration='' 1129 | local safety_counter=0 # fix for intermittent 'no secret key' decryption failures 1130 | while [[ ! $configuration ]]; do 1131 | configuration=$(gpg --batch --quiet --decrypt "$path") 1132 | 1133 | safety_counter=$((safety_counter + 1)) 1134 | if [[ $safety_counter -eq 3 ]]; then 1135 | die 1 'unable to decrypt the file "%s"' "$path" 1136 | fi 1137 | done 1138 | 1139 | cipher=$(printf '%s' "$configuration" | grep '^cipher' | cut -d'=' -f 2-) 1140 | password=$(printf '%s' "$configuration" | grep '^password' | cut -d'=' -f 2-) 1141 | } 1142 | 1143 | # Echo space-delimited list of context names defined in the git config 1144 | get_contexts_from_git_config() { 1145 | config_names=$(git config --local --name-only --list | grep transcrypt) 1146 | extract_context_name_regex="transcrypt\.(${CONTEXT_REGEX}).password" 1147 | contexts=() 1148 | for name in $config_names; do 1149 | if [[ "$name" = "transcrypt.password" ]]; then 1150 | contexts+=('default') 1151 | elif [[ $name =~ $extract_context_name_regex ]]; then 1152 | contexts+=("${BASH_REMATCH[1]}") 1153 | fi 1154 | done 1155 | if [[ "${contexts:-}" ]]; then 1156 | trim "$(printf "%q " "${contexts[@]}")" 1157 | fi 1158 | } 1159 | 1160 | # Echo space-delimited list of context names defined in the .gitattributes file 1161 | get_contexts_from_git_attributes() { 1162 | if [[ -f $GIT_ATTRIBUTES ]]; then 1163 | # Capture contents of .gitattributes without comments (leading # hash) 1164 | attr_lines=$(sed '/^.*#/d' <"$GIT_ATTRIBUTES") 1165 | extract_context_name_regex="filter=crypt-(${CONTEXT_REGEX})" 1166 | recognise_crypt_regex="filter=crypt" 1167 | contexts=() 1168 | IFS=$'\n' 1169 | for attr_line in ${attr_lines}; do 1170 | if [[ $attr_line =~ $extract_context_name_regex ]]; then 1171 | contexts+=("${BASH_REMATCH[1]}") 1172 | elif [[ $attr_line =~ $recognise_crypt_regex ]]; then 1173 | contexts+=('default') 1174 | fi 1175 | done 1176 | unset IFS 1177 | if [[ "${contexts:-}" ]]; then 1178 | trim "$(printf "%q " "${contexts[@]}")" 1179 | fi 1180 | fi 1181 | } 1182 | 1183 | # Echo all context names, sorted and distinct, from the git config or .gitattributes 1184 | get_contexts() { 1185 | combined_contexts="$CONFIGURED_CONTEXTS $GITATTRIBUTES_CONTEXTS" 1186 | sorted_contexts=$(echo "$combined_contexts" | tr ' ' '\n' | sort -u | tr '\n' ' ') 1187 | if [[ "${sorted_contexts:-}" ]]; then 1188 | trim "$sorted_contexts" 1189 | fi 1190 | } 1191 | 1192 | # Echo the number of items in a space-delimited list given as $1 1193 | count_items_in_list() { 1194 | local trimmed 1195 | trimmed=$(trim "$1") 1196 | if [[ ! "$trimmed" ]]; then 1197 | return 0 1198 | fi 1199 | local just_spaces="${trimmed//[^ ]/}" 1200 | echo $((${#just_spaces} + 1)) 1201 | } 1202 | 1203 | # Utility function returns a truthy value if value $1 is in bash list or array $2 1204 | # Based on https://stackoverflow.com/a/8574392/4970 1205 | is_item_in_array() { 1206 | local e match="$1" 1207 | shift 1208 | for e; do [[ "$e" == "$match" ]] && return 0; done 1209 | return 1 1210 | } 1211 | 1212 | # Trim leading and trailing whitespace from $1 1213 | # From https://stackoverflow.com/a/3352015/4970 1214 | trim() { 1215 | local var="$*" 1216 | # remove leading whitespace characters 1217 | var="${var#"${var%%[![:space:]]*}"}" 1218 | # remove trailing whitespace characters 1219 | var="${var%"${var##*[![:space:]]}"}" 1220 | printf '%s' "$var" 1221 | } 1222 | 1223 | list_contexts() { 1224 | for context in $(get_contexts); do 1225 | # shellcheck disable=SC2086 1226 | if ! is_item_in_array "$context" ${CONFIGURED_CONTEXTS}; then 1227 | echo "$context (not initialised)" 1228 | elif ! is_item_in_array "$context" ${GITATTRIBUTES_CONTEXTS}; then 1229 | echo "$context (no patterns in .gitattributes)" 1230 | else 1231 | echo "$context" 1232 | fi 1233 | done 1234 | } 1235 | 1236 | # print this script's usage message to stderr 1237 | usage() { 1238 | cat <<-EOF >&2 1239 | usage: transcrypt [-c CIPHER] [-p PASSWORD] [-h] 1240 | EOF 1241 | } 1242 | 1243 | # print this script's help message to stdout 1244 | help() { 1245 | cat <<-EOF 1246 | NAME 1247 | transcrypt -- transparently encrypt files within a git repository 1248 | 1249 | SYNOPSIS 1250 | transcrypt [options...] 1251 | 1252 | DESCRIPTION 1253 | 1254 | transcrypt will configure a Git repository to support the transparent 1255 | encryption/decryption of files by utilizing OpenSSL's symmetric cipher 1256 | routines and Git's built-in clean/smudge filters. It will also add a 1257 | Git alias "ls-crypt" to list all transparently encrypted files within 1258 | the repository. 1259 | 1260 | The transcrypt source code and full documentation may be downloaded 1261 | from https://github.com/elasticdog/transcrypt. 1262 | 1263 | OPTIONS 1264 | -c, --cipher=CIPHER 1265 | the symmetric cipher to utilize for encryption; 1266 | defaults to aes-256-cbc 1267 | 1268 | -p, --password=PASSWORD 1269 | the password to derive the key from; 1270 | defaults to 30 random base64 characters 1271 | 1272 | --set-openssl-path=PATH_TO_OPENSSL 1273 | use OpenSSL at this path; defaults to 'openssl' in \$PATH 1274 | 1275 | -y, --yes 1276 | assume yes and accept defaults for non-specified options 1277 | 1278 | -d, --display 1279 | display the current repository's cipher and password 1280 | 1281 | --add, --add=pattern 1282 | add a file pattern to encrypt to the .gitattributes file 1283 | 1284 | -r, --rekey 1285 | re-encrypt all encrypted files using new credentials 1286 | 1287 | -f, --flush-credentials 1288 | remove the locally cached encryption credentials and re-encrypt 1289 | any files that had been previously decrypted 1290 | 1291 | -F, --force 1292 | ignore whether the git directory is clean, proceed with the 1293 | possibility that uncommitted changes are overwritten 1294 | 1295 | -u, --uninstall 1296 | remove all transcrypt configuration from the repository and 1297 | leave files in the current working copy decrypted 1298 | 1299 | --upgrade 1300 | apply the latest transcrypt scripts in the repository without 1301 | changing your configuration settings 1302 | 1303 | -l, --list 1304 | list all of the transparently encrypted files in the repository, 1305 | relative to the top-level directory 1306 | 1307 | -s, --show-raw=FILE 1308 | show the raw file as stored in the git commit object; use this 1309 | to check if files are encrypted as expected 1310 | 1311 | -e, --export-gpg=RECIPIENT 1312 | export the repository's cipher and password to a file encrypted 1313 | for a gpg recipient 1314 | 1315 | -i, --import-gpg=FILE 1316 | import the password and cipher from a gpg encrypted file 1317 | 1318 | -C, --context=CONTEXT_NAME 1319 | name for a context with a different passphrase and cipher from 1320 | the 'default' context; use this advanced option to encrypt 1321 | different files with different passphrases 1322 | 1323 | --list-contexts 1324 | list all contexts configured in the repository, and warn about 1325 | incompletely configured contexts 1326 | 1327 | -v, --version 1328 | print the version information 1329 | 1330 | -h, --help 1331 | view this help message 1332 | 1333 | EXAMPLES 1334 | 1335 | To initialize a Git repository to support transparent encryption, just 1336 | change into the repo and run the transcrypt script. transcrypt will 1337 | prompt you interactively for all required information if the corre- 1338 | sponding option flags were not given. 1339 | 1340 | $ cd / 1341 | $ transcrypt 1342 | 1343 | Once a repository has been configured with transcrypt, you can trans- 1344 | parently encrypt files by applying the "crypt" filter, diff and merge 1345 | to a pattern in the top-level .gitattributes config. If that pattern 1346 | matches a file in your repository, the file will be transparently 1347 | encrypted once you stage and commit it: 1348 | 1349 | $ transcrypt --add sensitive_file 1350 | 1351 | $ cat .gitattributes 1352 | sensitive_file filter=crypt diff=crypt merge=crypt 1353 | 1354 | $ git add .gitattributes sensitive_file 1355 | $ git commit -m 'Add encrypted version of a sensitive file' 1356 | 1357 | See the gitattributes(5) man page for more information. 1358 | 1359 | If you have just cloned a repository containing files that are 1360 | encrypted, you'll want to configure transcrypt with the same cipher and 1361 | password as the origin repository. Once transcrypt has stored the 1362 | matching credentials, it will force a checkout of any existing 1363 | encrypted files in order to decrypt them. 1364 | 1365 | If the origin repository has just rekeyed, all clones should flush 1366 | their transcrypt credentials, fetch and merge the new encrypted files 1367 | via Git, and then re-configure transcrypt with the new credentials. 1368 | 1369 | ADVANCED 1370 | 1371 | Context names let you encrypt some files with different passwords for a 1372 | different audience, such as super-users. The 'default' context applies 1373 | unless you set a context name. 1374 | 1375 | Add a context by reinitialising transcrypt with a context name then add 1376 | a pattern with crypt- attributes to .gitattributes. 1377 | For example, to encrypt a file 'top-secret' in a "super" context: 1378 | 1379 | # Initialise a new "super" context, and set a different password 1380 | $ transcrypt --context=super 1381 | 1382 | # Add a pattern to .gitattributes with "crypt-super" values 1383 | $ transcrypt --context=super --add=top-secret 1384 | 1385 | $ cat .gitattributes 1386 | top-secret filter=crypt-super diff=crypt-super merge=crypt-super 1387 | 1388 | # Add and commit your top-secret and .gitattribute files 1389 | $ git add .gitattributes top-secret 1390 | $ git commit -m "Add top secret file for super-users only" 1391 | 1392 | # List all contexts 1393 | $ transcrypt --list-contexts 1394 | 1395 | # Display the cipher and password for the "super" context 1396 | $ transcrypt --context=super --display 1397 | 1398 | AUTHOR 1399 | Aaron Bull Schaefer 1400 | 1401 | MAINTAINER 1402 | James Murty 1403 | 1404 | SEE ALSO 1405 | enc(1), gitattributes(5) 1406 | EOF 1407 | } 1408 | 1409 | ##### MAIN 1410 | 1411 | # reset all variables that might be set 1412 | context='' 1413 | cipher='' 1414 | display_config='' 1415 | add_pattern='' 1416 | list_contexts_command='' 1417 | flush_creds='' 1418 | gpg_import_file='' 1419 | gpg_recipient='' 1420 | interactive='true' 1421 | list='' 1422 | password='' 1423 | rekey='' 1424 | show_file='' 1425 | uninstall='' 1426 | upgrade='' 1427 | openssl_path='openssl' 1428 | 1429 | # used to bypass certain safety checks 1430 | requires_existing_config='' 1431 | requires_clean_repo='true' 1432 | 1433 | # parse command line options 1434 | while [[ "${1:-}" != '' ]]; do 1435 | case $1 in 1436 | clean) 1437 | shift 1438 | git_clean "$@" 1439 | exit $? 1440 | ;; 1441 | smudge) 1442 | shift 1443 | git_smudge "$@" 1444 | exit $? 1445 | ;; 1446 | textconv) 1447 | shift 1448 | git_textconv "$@" 1449 | exit $? 1450 | ;; 1451 | merge) 1452 | shift 1453 | git_merge "$@" 1454 | exit $? 1455 | ;; 1456 | pre_commit) 1457 | shift 1458 | git_pre_commit "$@" 1459 | exit $? 1460 | ;; 1461 | -c | --cipher) 1462 | cipher=${2:-} 1463 | [[ $cipher ]] || die 1 'empty cipher' 1464 | shift 1465 | ;; 1466 | --cipher=*) 1467 | cipher=${1#*=} 1468 | ;; 1469 | -p | --password) 1470 | password=${2:-} 1471 | [[ $password ]] || die 1 'empty password' 1472 | shift 1473 | ;; 1474 | --password=*) 1475 | password=${1#*=} 1476 | [[ $password ]] || die 1 'empty password' 1477 | ;; 1478 | --add) 1479 | add_pattern=${2:-1} 1480 | [[ $add_pattern ]] || die 1 'empty pattern' 1481 | requires_clean_repo='' 1482 | shift 1483 | ;; 1484 | --add=*) 1485 | add_pattern=${1#*=} 1486 | [[ $add_pattern ]] || die 1 'empty pattern' 1487 | requires_clean_repo='' 1488 | ;; 1489 | -C | --context) 1490 | context=${2:-} 1491 | [[ $context ]] || die 1 'empty context' 1492 | shift 1493 | ;; 1494 | --context=*) 1495 | context=${1#*=} 1496 | ;; 1497 | --set-openssl-path=*) 1498 | openssl_path=${1#*=} 1499 | # Immediately apply config setting 1500 | git config transcrypt.openssl-path "$openssl_path" 1501 | ;; 1502 | -y | --yes) 1503 | interactive='' 1504 | ;; 1505 | -d | --display) 1506 | display_config='true' 1507 | requires_existing_config='true' 1508 | requires_clean_repo='' 1509 | ;; 1510 | -r | --rekey) 1511 | rekey='true' 1512 | requires_existing_config='true' 1513 | ;; 1514 | -f | --flush-credentials) 1515 | flush_creds='true' 1516 | requires_existing_config='true' 1517 | ;; 1518 | -F | --force) 1519 | requires_clean_repo='' 1520 | ;; 1521 | -u | --uninstall) 1522 | uninstall='true' 1523 | requires_existing_config='true' 1524 | requires_clean_repo='' 1525 | ;; 1526 | --upgrade) 1527 | upgrade='true' 1528 | requires_existing_config='true' 1529 | requires_clean_repo='' 1530 | ;; 1531 | -l | --list) 1532 | list='true' 1533 | requires_clean_repo='' 1534 | ;; 1535 | --list-contexts) 1536 | list_contexts_command='true' 1537 | requires_clean_repo='' 1538 | ;; 1539 | -s | --show-raw) 1540 | show_file=${2:-} 1541 | [[ $show_file ]] || die 1 'empty file path' 1542 | show_raw_file 1543 | exit 0 1544 | ;; 1545 | --show-raw=*) 1546 | show_file=${1#*=} 1547 | show_raw_file 1548 | exit 0 1549 | ;; 1550 | -e | --export-gpg) 1551 | gpg_recipient=${2:-} 1552 | [[ $gpg_recipient ]] || die 1 'empty recipient' 1553 | requires_existing_config='true' 1554 | requires_clean_repo='' 1555 | shift 1556 | ;; 1557 | --export-gpg=*) 1558 | gpg_recipient=${1#*=} 1559 | requires_existing_config='true' 1560 | requires_clean_repo='' 1561 | ;; 1562 | -i | --import-gpg) 1563 | gpg_import_file=${2:-} 1564 | [[ $gpg_import_file ]] || die 1 'empty import file' 1565 | shift 1566 | ;; 1567 | --import-gpg=*) 1568 | gpg_import_file=${1#*=} 1569 | ;; 1570 | -v | --version) 1571 | printf 'transcrypt %s\n' "$VERSION" 1572 | exit 0 1573 | ;; 1574 | -h | --help | -\?) 1575 | help 1576 | exit 0 1577 | ;; 1578 | --*) 1579 | warn 'unknown option -- %s' "${1#--}" 1580 | usage 1581 | exit 1 1582 | ;; 1583 | *) 1584 | warn 'unknown option -- %s' "${1#-}" 1585 | usage 1586 | exit 1 1587 | ;; 1588 | esac 1589 | shift 1590 | done 1591 | 1592 | # Multi-context support, triggered by optional --context / -C command line option 1593 | set_context_globals() { 1594 | if [[ "$context" ]] && [[ ! "$context" = 'default' ]]; then 1595 | CONTEXT="$context" 1596 | CONTEXT_CONFIG_GROUP=".${CONTEXT}" # Note leading period 1597 | CONTEXT_CRYPT_SUFFIX="-${CONTEXT}" # Note leading dash 1598 | CONTEXT_DESCRIPTION=" for context '$CONTEXT'" 1599 | else 1600 | CONTEXT='default' 1601 | # Empty values for default/unset context 1602 | CONTEXT_CONFIG_GROUP="" 1603 | CONTEXT_CRYPT_SUFFIX="" 1604 | CONTEXT_DESCRIPTION="" 1605 | fi 1606 | } 1607 | set_context_globals 1608 | 1609 | gather_repo_metadata 1610 | 1611 | # always run our safety checks 1612 | run_safety_checks 1613 | 1614 | # regular expression used to test user input 1615 | readonly YES_REGEX='^[Yy]$' 1616 | 1617 | # in order to keep behavior consistent no matter what order the options were 1618 | # specified in, we must run these here rather than in the case statement above 1619 | if [[ $list ]]; then 1620 | list_files 1621 | exit 0 1622 | elif [[ $uninstall ]]; then 1623 | uninstall_transcrypt 1624 | exit 0 1625 | elif [[ $add_pattern ]]; then 1626 | add_pattern "$add_pattern" 1627 | exit 0 1628 | elif [[ $upgrade ]]; then 1629 | upgrade_transcrypt 1630 | exit 0 1631 | elif [[ $display_config ]] && [[ $flush_creds ]]; then 1632 | display_configuration 1633 | printf '\n' 1634 | flush_credentials 1635 | exit 0 1636 | elif [[ $display_config ]]; then 1637 | display_configuration 1638 | exit 0 1639 | elif [[ $list_contexts_command ]]; then 1640 | list_contexts 1641 | exit 0 1642 | elif [[ $flush_creds ]]; then 1643 | flush_credentials 1644 | exit 0 1645 | elif [[ $gpg_recipient ]]; then 1646 | export_gpg 1647 | exit 0 1648 | elif [[ $gpg_import_file ]]; then 1649 | import_gpg 1650 | elif [[ $cipher ]]; then 1651 | validate_cipher 1652 | fi 1653 | 1654 | count_dirty_files() { 1655 | { git diff-index --name-status HEAD -- || git diff-index --stat HEAD -- || git diff-index HEAD -- || true; } 2>/dev/null | grep $'\n' -c 1656 | } 1657 | 1658 | # shellcheck disable=SC2155 1659 | readonly GIT_DIRTY_FILES_BEFORE_INIT="$(count_dirty_files)" 1660 | 1661 | # perform function calls to configure transcrypt 1662 | get_cipher 1663 | get_password 1664 | 1665 | if [[ $rekey ]] && [[ $interactive ]]; then 1666 | confirm_rekey 1667 | elif [[ $interactive ]]; then 1668 | confirm_configuration 1669 | fi 1670 | 1671 | save_configuration 1672 | 1673 | if [[ $rekey ]]; then 1674 | stage_rekeyed_files 1675 | else 1676 | force_checkout 1677 | # check for newly modified (dirty) files after transcrypt configuration which could 1678 | # indicate an incorrect password 1679 | if [[ ${GIT_DIRTY_FILES_BEFORE_INIT} -lt $(count_dirty_files) ]]; then 1680 | die 1 'Unexpected new dirty files in the repository when configured by transcrypt%s, please check your password.\n' "$CONTEXT_DESCRIPTION" 1681 | fi 1682 | fi 1683 | 1684 | # ensure the git attributes file exists 1685 | if [[ ! -f $GIT_ATTRIBUTES ]]; then 1686 | mkdir -p "${GIT_ATTRIBUTES%/*}" 1687 | printf '#pattern filter=crypt diff=crypt merge=crypt\n' >"$GIT_ATTRIBUTES" 1688 | fi 1689 | 1690 | printf 'The repository has been successfully configured by transcrypt%s.\n' "$CONTEXT_DESCRIPTION" 1691 | 1692 | exit 0 1693 | --------------------------------------------------------------------------------