├── .editorconfig ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── shard.yml ├── spec ├── cmd_crosstests.sh ├── nanvault_spec.cr ├── spec_helper.cr └── testfiles │ ├── NOTES.md │ ├── test1.enc │ └── test1.yml └── src ├── cli.cr └── nanvault.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: nanvault tests 2 | run-name: nanvault tests 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | 12 | build_linux: 13 | runs-on: ubuntu-latest 14 | 15 | container: 16 | image: crystallang/crystal:latest-alpine 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | # this is for bash and ansible 22 | - name: Apk update 23 | run: apk update 24 | - name: Pre-req - bash 25 | run: apk add bash 26 | - name: Pre-req - pip 27 | run: apk add py3-pip 28 | - name: Pre-req - libffi-dev 29 | run: apk add libffi-dev 30 | - name: Pre-req - python3-dev 31 | run: apk add python3-dev 32 | - name: Pre-req - rust cargo # this is for pyca/cryptography Ansible dep 33 | run: apk add rust cargo 34 | - name: Pre-req - create and activate venv 35 | run: mkdir /venvtest && python3 -m venv /venvtest && . /venvtest/bin/activate 36 | - name: Pre-req - upgrade pip 37 | run: . /venvtest/bin/activate && pip3 install --upgrade pip 38 | - name: Pre-req - ansible 39 | run: . /venvtest/bin/activate && pip3 install ansible 40 | 41 | # nanvault tests 42 | - name: Install dependencies 43 | run: shards install 44 | - name: Run unittests 45 | run: crystal spec 46 | - name: Build 47 | run: shards build 48 | - name: Run cmd_crosstests 49 | run: . /venvtest/bin/activate && bash ./spec/cmd_crosstests.sh 50 | 51 | build_macos: 52 | runs-on: macos-latest 53 | 54 | steps: 55 | - uses: actions/checkout@v2 56 | 57 | # install Crystal 58 | - name: Brew update 59 | run: brew update 60 | - name: Install Crystal 61 | run: brew install crystal 62 | - name: Install pipx 63 | run: brew install pipx 64 | 65 | # this is for ansible 66 | - name: Pre-req - ansible 67 | run: pipx install ansible 68 | 69 | # nanvault tests 70 | - name: Install dependencies 71 | run: shards install 72 | - name: Run unittests 73 | run: crystal spec 74 | - name: Build 75 | run: shards build 76 | - name: Run cmd_crosstests 77 | run: bash ./spec/cmd_crosstests.sh 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Upload Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | 10 | build_linux: 11 | name: Build for GNU/Linux 12 | runs-on: ubuntu-latest 13 | 14 | # we build statically linked binaries using the official 15 | # Crystal alpine-linux docker image 16 | # (we need musl-libc, hence we use Alpine) 17 | # https://crystal-lang.org/2020/02/02/alpine-based-docker-images.html 18 | container: 19 | image: crystallang/crystal:latest-alpine 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Install dependencies 25 | run: shards install 26 | - name: Build 27 | run: shards build --production --static 28 | 29 | - name: Upload build artifact 30 | uses: actions/upload-artifact@v1 31 | with: 32 | name: nanvault-linux 33 | path: ./bin/nanvault 34 | 35 | build_macos: 36 | name: Build for macOS 37 | runs-on: macos-latest 38 | 39 | steps: 40 | - uses: actions/checkout@v2 41 | 42 | - name: Brew update 43 | run: brew update 44 | - name: Install Crystal 45 | run: brew install crystal 46 | 47 | - name: Install dependencies 48 | run: shards install 49 | - name: Build 50 | run: shards build --production # no static linking on macOS: https://developer.apple.com/library/archive/qa/qa1118/_index.html 51 | 52 | - name: Upload build artifact 53 | uses: actions/upload-artifact@v1 54 | with: 55 | name: nanvault-darwin 56 | path: ./bin/nanvault 57 | 58 | create_release: 59 | needs: [build_linux, build_macos] 60 | name: Create Release 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - name: Create Release 65 | id: create_release 66 | uses: actions/create-release@v1 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | tag_name: ${{ github.ref }} 71 | release_name: Release ${{ github.ref }} 72 | draft: false 73 | prerelease: false 74 | - name: Get the version # https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32163/highlight/true#M1024 75 | id: get_version 76 | run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3) 77 | 78 | # get artifacts containing the builds 79 | - name: Download nanvault-linux 80 | uses: actions/download-artifact@v4 81 | with: 82 | name: nanvault-linux 83 | - name: Download nanvault-darwin 84 | uses: actions/download-artifact@v4 85 | with: 86 | name: nanvault-darwin 87 | 88 | # create tar archives 89 | - shell: bash 90 | run: | 91 | chmod +x ./nanvault-linux/nanvault 92 | chmod +x ./nanvault-darwin/nanvault 93 | tar --owner root --group root -czf nanvault-${{ steps.get_version.outputs.VERSION }}-linux-amd64.tar.gz -C ./nanvault-linux . 94 | tar --owner root --group root -czf nanvault-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.tar.gz -C ./nanvault-darwin . 95 | 96 | # upload archives 97 | - name: Upload Release Asset Linux 98 | id: upload-release-asset-linux 99 | uses: actions/upload-release-asset@v1 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | with: 103 | upload_url: ${{ steps.create_release.outputs.upload_url }} 104 | asset_path: ./nanvault-${{ steps.get_version.outputs.VERSION }}-linux-amd64.tar.gz 105 | asset_name: nanvault-${{ steps.get_version.outputs.VERSION }}-linux-amd64.tar.gz 106 | asset_content_type: application/gzip 107 | - name: Upload Release Asset macOS 108 | id: upload-release-asset-macos 109 | uses: actions/upload-release-asset@v1 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | with: 113 | upload_url: ${{ steps.create_release.outputs.upload_url }} 114 | asset_path: ./nanvault-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.tar.gz 115 | asset_name: nanvault-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.tar.gz 116 | asset_content_type: application/gzip 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Marco Bellaccini 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 | # nanvault 2 | 3 | ![nanvault tests](https://github.com/marcobellaccini/nanvault/workflows/nanvault%20tests/badge.svg) 4 | [![GitHub release](https://img.shields.io/github/release/marcobellaccini/nanvault.svg)](https://github.com/marcobellaccini/nanvault/releases) 5 | 6 | **nanvault** is not-ansible-vault. 7 | 8 | It is a standalone CLI tool to encrypt and decrypt files in the [Ansible® Vault](https://docs.ansible.com/ansible/latest/user_guide/vault.html) format. 9 | 10 | **Powerful**: *has UNIX-style composability - you can play with [pipes](https://en.wikipedia.org/wiki/Pipeline_(Unix))!* 11 | 12 | **Smart**: *it guesses what you want to do, based on piped input.* 13 | 14 | **Batteries-included**: *it features a safe password generator and a YAML-string mode.* 15 | 16 | **Thoroughly-tested**: *at the time of writing, there are more lines of code devoted to tests than to the program itself.* 17 | 18 | **Free and open-source**: *released under the MIT license.* 19 | 20 | [![asciicast](https://asciinema.org/a/302642.svg)](https://asciinema.org/a/302642) 21 | 22 | ## Installation 23 | 24 | ### GNU/Linux 25 | 26 | You can download the latest binary from the [releases page](https://github.com/marcobellaccini/nanvault/releases). 27 | 28 | ### macOS 29 | 30 | You can get the latest *darwin* build from the [releases page](https://github.com/marcobellaccini/nanvault/releases). 31 | 32 | ### Windows 33 | 34 | Until the [Crystal Windows porting](https://github.com/crystal-lang/crystal/wiki/Porting-to-Windows) is completed, 35 | you can go with [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install-win10). 36 | 37 | ### From sources 38 | 39 | If you prefer, you can [build the program straight from the sources](#Building-from-sources). 40 | 41 | ## Usage 42 | 43 | Generate a *vault password file*, then encrypt and decrypt files: 44 | ``` 45 | $ nanvault -g > passfile 46 | $ echo "coolstuff" > test.txt 47 | $ cat test.txt | nanvault -p passfile > test.enc 48 | $ cat test.enc | nanvault -p passfile > decrypted_test.txt 49 | 50 | ``` 51 | 52 | Of course, you can provide your own *ansible-vault password files*. 53 | 54 | If the *NANVAULT_PASSFILE* environment variable is set, the *vault password file* option may be omitted: 55 | ``` 56 | $ export NANVAULT_PASSFILE="passfile" 57 | $ nanvault -g > $NANVAULT_PASSFILE 58 | $ echo "Encrypt this! ^_^ " | nanvault 59 | $ANSIBLE_VAULT;1.1;AES256 60 | 643439633661336237356434383036353... 61 | 62 | ``` 63 | 64 | If you want to provide a vault-id label, just use the right option: 65 | ``` 66 | $ echo "Encrypt this! ^_^ " | nanvault -l mylabel 67 | $ANSIBLE_VAULT;1.2;AES256;mylabel 68 | 623466656431303538633462666133333935... 69 | ``` 70 | 71 | You can also convert data to and from YAML (this is compatible with [*ansible-vault encrypt_string*](https://docs.ansible.com/ansible/latest/user_guide/vault.html#use-encrypt-string-to-create-encrypted-variables-to-embed-in-yaml)): 72 | ``` 73 | $ echo "Encrypt this! ^_^ " | nanvault | nanvault -y mystuff 74 | mystuff: !vault | 75 | $ANSIBLE_VAULT;1.1;AES256 76 | 653936313063303031376236373231336... 77 | $ echo "Encrypt this! ^_^ " | nanvault | nanvault -y mystuff > my.yml 78 | $ cat my.yml | nanvault -Y 79 | $ANSIBLE_VAULT;1.1;AES256 80 | 6534346535376538306330623363653... 81 | $ cat my.yml | nanvault -Y | nanvault 82 | Encrypt this! ^_^ 83 | ``` 84 | 85 | 86 | Get help and discover all the options: 87 | ``` 88 | $ nanvault -h 89 | 90 | ``` 91 | 92 | ## Development 93 | 94 | **nanvault** is proudly programmed in [Crystal](https://crystal-lang.org/). 95 | 96 | *<>* 97 | 98 | ### Building from sources 99 | 100 | 1. [Install Crystal](https://crystal-lang.org/install/). 101 | **Please make sure to install *libssl-dev* and *libyaml-dev* too.** 102 | 2. Clone this repo (`git clone https://github.com/marcobellaccini/nanvault`) 103 | 3. Build with *shards* (`shards build`) 104 | 105 | Instead, if you have Docker, you can compile a statically-linked binary (using 106 | the official Crystal Alpine-Linux Docker images) by running the build script: 107 | ``` 108 | ./build.sh [debug/release] 109 | ``` 110 | 111 | ## Contributing 112 | 113 | 1. Fork it () 114 | 2. Create your feature branch (`git checkout -b my-new-feature`) 115 | 3. Commit your changes (`git commit -am 'Add some feature'`) 116 | 4. Push to the branch (`git push origin my-new-feature`) 117 | 5. Create a new Pull Request 118 | 119 | ## Contributors 120 | 121 | - [Marco Bellaccini](https://github.com/marcobellaccini) - creator, maintainer and cool guy 122 | 123 | --- 124 | 125 | Ansible® is a registered trademark of Red Hat, Inc. in the United States and other countries. 126 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # script to automatically compile statically-linked bin 4 | # 5 | 6 | if [ "$1" = "debug" ] || [ "$1" = "release" ] 7 | then 8 | BUILD_TYPE=$1 9 | else 10 | echo "Usage: ./build.sh [debug/release]" 11 | exit 1 12 | fi 13 | 14 | if [ BUILD_TYPE = "release" ] 15 | then 16 | PRODOPT="--production" 17 | else 18 | PRODOPT="" 19 | fi 20 | 21 | docker run --rm -it -v $PWD:/workspace -w /workspace crystallang/crystal:latest-alpine \ 22 | shards build $PRODOPT --static 23 | 24 | if [ "$?" -ne "0" ] 25 | then 26 | echo "Build failed" 27 | exit 1 28 | fi 29 | 30 | exit 0 31 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: nanvault 2 | version: 0.2.3 3 | 4 | authors: 5 | - Marco Bellaccini 6 | 7 | targets: 8 | nanvault: 9 | main: src/cli.cr 10 | 11 | crystal: 0.32.1 12 | 13 | license: MIT 14 | -------------------------------------------------------------------------------- /spec/cmd_crosstests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Simple script to perform command line program and cross-tools tests 4 | # 5 | TEST_WORKDIR="tmp_crosstests" 6 | VAULTPASS_FILE="${TEST_WORKDIR}/vaultpassfile" 7 | PLAINTEXT_FILE="${TEST_WORKDIR}/plaintext" 8 | WORK_FILE="${TEST_WORKDIR}/work" 9 | OUT_FILE="${TEST_WORKDIR}/outfile" 10 | 11 | NANVAULT="./bin/nanvault" 12 | 13 | # cleanup function 14 | cleanup () 15 | { 16 | rm -r $TEST_WORKDIR 17 | } 18 | 19 | # exiterror function 20 | exiterror () 21 | { 22 | cleanup 23 | exit 1; 24 | } 25 | 26 | # checkfiles function 27 | checkfiles () 28 | { 29 | diff $PLAINTEXT_FILE $OUT_FILE 30 | if [ "$?" -ne "0" ] 31 | then 32 | echo "CROSS-TESTS FAILED." 33 | exiterror 34 | fi 35 | # delete outfile 36 | rm $OUT_FILE 37 | # refresh workfile 38 | cp $PLAINTEXT_FILE $WORK_FILE 39 | } 40 | 41 | # create tests workdir 42 | mkdir $TEST_WORKDIR 43 | 44 | # generate vault password file and plaintext file 45 | $NANVAULT -g > $VAULTPASS_FILE 46 | echo -n "test plaintext" > $PLAINTEXT_FILE 47 | 48 | # check password file length 49 | PAS_SIZE=$(wc -m < "$VAULTPASS_FILE") 50 | if [ "$PAS_SIZE" -ne "21" ] 51 | then 52 | echo "PASSWORD FILE GENERATION FAILED." 53 | exiterror 54 | fi 55 | 56 | # generate workfile 57 | cp $PLAINTEXT_FILE $WORK_FILE 58 | 59 | ## CRYPTO - NO LABEL 60 | echo "#crypto-no-label" 61 | 62 | # encrypt with nanvault, decrypt with nanvault 63 | echo "encrypt with nanvault, decrypt with nanvault" 64 | cat $WORK_FILE | $NANVAULT -p $VAULTPASS_FILE | $NANVAULT -p $VAULTPASS_FILE > $OUT_FILE 65 | checkfiles 66 | 67 | # encrypt with nanvault, decrypt with ansible-vault 68 | echo "encrypt with nanvault, decrypt with ansible-vault" 69 | cat $WORK_FILE | $NANVAULT -p $VAULTPASS_FILE > $OUT_FILE 70 | ansible-vault decrypt --vault-password-file $VAULTPASS_FILE $OUT_FILE 71 | checkfiles 72 | 73 | # encrypt with ansible-vault, decrypt with nanvault 74 | echo "encrypt with ansible-vault, decrypt with nanvault" 75 | ansible-vault encrypt --vault-password-file $VAULTPASS_FILE $WORK_FILE 76 | cat $WORK_FILE | $NANVAULT -p $VAULTPASS_FILE > $OUT_FILE 77 | checkfiles 78 | 79 | ## LABEL 80 | echo "#crypto-label" 81 | 82 | # encrypt with nanvault, decrypt with nanvault 83 | echo "encrypt with nanvault, decrypt with nanvault" 84 | cat $WORK_FILE | $NANVAULT -p $VAULTPASS_FILE -l mylabel | $NANVAULT -p $VAULTPASS_FILE > $OUT_FILE 85 | checkfiles 86 | 87 | # encrypt with nanvault, decrypt with ansible-vault 88 | echo "encrypt with nanvault, decrypt with ansible-vault" 89 | cat $WORK_FILE | $NANVAULT -p $VAULTPASS_FILE -l mylabel > $OUT_FILE 90 | ansible-vault decrypt --vault-password-file $VAULTPASS_FILE $OUT_FILE 91 | checkfiles 92 | 93 | # encrypt with ansible-vault, decrypt with nanvault 94 | echo "encrypt with ansible-vault, decrypt with nanvault" 95 | ansible-vault encrypt --vault-id $VAULTPASS_FILE $WORK_FILE 96 | cat $WORK_FILE | $NANVAULT -p $VAULTPASS_FILE > $OUT_FILE 97 | checkfiles 98 | 99 | ## YAML strings 100 | echo "#yaml" 101 | 102 | # to YAML with nanvault, from YAML with nanvault 103 | echo "to YAML with nanvault, from YAML with nanvault" 104 | Y_MYVAL="foo" 105 | YEI_OUT=$(echo -n "$Y_MYVAL" | $NANVAULT -y mystuff | $NANVAULT -Y) 106 | if [ "$YEI_OUT" != "$Y_MYVAL" ] 107 | then 108 | echo "YAML TESTS FAILED." 109 | exiterror 110 | fi 111 | 112 | # to YAML with ansible-vault, from YAML with nanvault 113 | echo "to YAML with ansible-vault, from YAML with nanvault" 114 | Y_MYVAL="foo" 115 | YEI_OUT=$(echo -n "$Y_MYVAL" | ansible-vault encrypt_string --vault-password-file $VAULTPASS_FILE --stdin-name 'mystuff' | $NANVAULT -Y | $NANVAULT -p $VAULTPASS_FILE) 116 | if [ "$YEI_OUT" != "$Y_MYVAL" ] 117 | then 118 | echo "YAML TESTS FAILED." 119 | exiterror 120 | fi 121 | 122 | # success, cleanup 123 | cleanup 124 | echo "SUCCESS!" 125 | exit 0; 126 | -------------------------------------------------------------------------------- /spec/nanvault_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Nanvault do 4 | # Nanvault tests 5 | 6 | describe Nanvault::Encrypted do 7 | header_ok = "$ANSIBLE_VAULT;1.1;AES256\n" 8 | 9 | body_ok = "34393465386232383131386237626532306236396636396135393664323834383838313035666331\n" \ 10 | "6564353662313632616133366237393830393036303833320a356631363739393737316664313765\n" \ 11 | "63336362376661303365386566363361306630323639326161313166613564363561306133643662\n" \ 12 | "6466666165383365640a646365656164633362346630396335396365313231303238643039303937\n" \ 13 | "64393735663933666330366466393366376164306531313238393334633266646165" 14 | 15 | cdata_ok = header_ok + body_ok 16 | 17 | body_no_ctext = "34393465386232383131386237626532306236396636396135393664323834383838313035666331" \ 18 | "6564353662313632616133366237393830393036303833320a356631363739393737316664313765" \ 19 | "63336362376661303365386566363361306630323639326161313166613564363561306133643662" \ 20 | "6466666165383365640a" 21 | 22 | body_no_hmac = "34393465386232383131386237626532306236396636396135393664323834383838313035666331" \ 23 | "6564353662313632616133366237393830393036303833320a" 24 | 25 | body_ok_bytes = Bytes[52, 57, 52, 101, 56, 98, 50, 56, 49, 49, 56, 98, 55, 98, 101, 50, 48, 98, 54, 26 | 57, 102, 54, 57, 97, 53, 57, 54, 100, 50, 56, 52, 56, 56, 56, 49, 48, 53, 27 | 102, 99, 49, 101, 100, 53, 54, 98, 49, 54, 50, 97, 97, 51, 54, 98, 55, 57, 28 | 56, 48, 57, 48, 54, 48, 56, 51, 50, 10, 53, 102, 49, 54, 55, 57, 57, 55, 55, 29 | 49, 102, 100, 49, 55, 101, 99, 51, 99, 98, 55, 102, 97, 48, 51, 101, 56, 30 | 101, 102, 54, 51, 97, 48, 102, 48, 50, 54, 57, 50, 97, 97, 49, 49, 102, 97, 31 | 53, 100, 54, 53, 97, 48, 97, 51, 100, 54, 98, 100, 102, 102, 97, 101, 56, 51, 32 | 101, 100, 10, 100, 99, 101, 101, 97, 100, 99, 51, 98, 52, 102, 48, 57, 99, 33 | 53, 57, 99, 101, 49, 50, 49, 48, 50, 56, 100, 48, 57, 48, 57, 55, 100, 57, 34 | 55, 53, 102, 57, 51, 102, 99, 48, 54, 100, 102, 57, 51, 102, 55, 97, 100, 48, 35 | 101, 49, 49, 50, 56, 57, 51, 52, 99, 50, 102, 100, 97, 101] 36 | 37 | body_ok_salt = Bytes[73, 78, 139, 40, 17, 139, 123, 226, 11, 105, 246, 154, 89, 109, 40, 72, 38 | 136, 16, 95, 193, 237, 86, 177, 98, 170, 54, 183, 152, 9, 6, 8, 50] 39 | 40 | body_ok_hmac = Bytes[95, 22, 121, 151, 113, 253, 23, 236, 60, 183, 250, 3, 232, 239, 99, 160, 41 | 240, 38, 146, 170, 17, 250, 93, 101, 160, 163, 214, 189, 255, 174, 131, 237] 42 | 43 | body_ok_ctext = Bytes[220, 238, 173, 195, 180, 240, 156, 89, 206, 18, 16, 40, 208, 144, 151, 217, 44 | 117, 249, 63, 192, 109, 249, 63, 122, 208, 225, 18, 137, 52, 194, 253, 174] 45 | 46 | password = "foo" 47 | 48 | ptext = Bytes[45, 45, 45, 10, 35, 32, 84, 101, 115, 116, 32, 102, 105, 108, 101, 10, 45, 49 | 32, 79, 110, 101, 10, 45, 32, 84, 119, 111, 10] 50 | 51 | ptext_str = "---\n# Test file\n- One\n- Two\n" 52 | 53 | describe "#initialize" do 54 | it "correctly loads header and body" do 55 | enc = Nanvault::Encrypted.new "HEADER\nBODY1\nBODY2" 56 | enc.header.should eq("HEADER") 57 | enc.body.should eq("BODY1BODY2") 58 | end 59 | 60 | it "correctly handles empty string" do 61 | expect_raises(Nanvault::BadData, "Invalid input data") do 62 | enc = Nanvault::Encrypted.new "" 63 | end 64 | end 65 | 66 | it "correctly handles header-only data" do 67 | expect_raises(Nanvault::BadData, "Invalid input data") do 68 | enc = Nanvault::Encrypted.new "HEADER" 69 | end 70 | end 71 | 72 | it "load whole real data" do 73 | enc_str = File.read("spec/testfiles/test1.enc") 74 | enc = Nanvault::Encrypted.new enc_str 75 | enc.header.should eq(header_ok.rstrip("\n")) 76 | enc.body.should eq body_ok.delete("\n") 77 | end 78 | end 79 | describe "#parse" do 80 | it "correctly parse ok header" do 81 | head = "$ANSIBLE_VAULT;1.2;AES256;vault-id-label\n" 82 | enc = Nanvault::Encrypted.new(head + body_ok) 83 | enc.parse 84 | exp_vault_info = {"version" => "1.2", "alg" => "AES256", "label" => "vault-id-label"} 85 | enc.vault_info.should eq exp_vault_info 86 | end 87 | 88 | it "correctly parse ok-nolabel header" do 89 | head = "$ANSIBLE_VAULT;1.1;AES256\n" 90 | enc = Nanvault::Encrypted.new cdata_ok 91 | enc.parse 92 | exp_vault_info = {"version" => "1.1", "alg" => "AES256", "label" => nil} 93 | enc.vault_info.should eq exp_vault_info 94 | end 95 | 96 | it "correctly handles incomplete header" do 97 | head = "$ANSIBLE_VAULT;1.1\n" 98 | enc = Nanvault::Encrypted.new(head + body_ok) 99 | expect_raises(Nanvault::BadData, "Invalid input data: bad header") do 100 | enc.parse 101 | end 102 | end 103 | 104 | it "correctly handles unsupported header - with newline" do 105 | head = "FOOFILEHEAD\n" 106 | enc = Nanvault::Encrypted.new(head + body_ok) 107 | expect_raises(Nanvault::BadData, "Invalid input data: bad header") do 108 | enc.parse 109 | end 110 | end 111 | 112 | it "correctly handles unsupported header - no newline" do 113 | head = "FOOFILEHEAD" 114 | expect_raises(Nanvault::BadData, "Invalid input data") do 115 | enc = Nanvault::Encrypted.new(head) 116 | end 117 | end 118 | 119 | it "correctly handles unsupported version" do 120 | head = "$ANSIBLE_VAULT;1.0;AES\n" 121 | enc = Nanvault::Encrypted.new(head + body_ok) 122 | expect_raises(Nanvault::BadData, "Sorry: format version 1.0 is not supported") do 123 | enc.parse 124 | end 125 | end 126 | 127 | it "correctly parse ok hex body" do 128 | enc = Nanvault::Encrypted.new cdata_ok 129 | enc.parse 130 | enc.bbody.should eq body_ok_bytes 131 | end 132 | 133 | it "correctly handles non-hex body" do 134 | enc = Nanvault::Encrypted.new(header_ok + "11ZZ11") 135 | expect_raises(Nanvault::BadData, "Invalid encoding in input data body") do 136 | enc.parse 137 | end 138 | end 139 | 140 | it "correctly parse ok body" do 141 | enc = Nanvault::Encrypted.new cdata_ok 142 | enc.parse 143 | enc.bbody.should eq body_ok_bytes 144 | enc.salt.should eq body_ok_salt 145 | enc.hmac.should eq body_ok_hmac 146 | enc.ctext.should eq body_ok_ctext 147 | end 148 | 149 | it "correctly handles short body - no ctext" do 150 | enc = Nanvault::Encrypted.new(header_ok + body_no_ctext) 151 | expect_raises(Nanvault::BadData, "Invalid input data body") do 152 | enc.parse 153 | end 154 | end 155 | 156 | it "correctly handles short body - no hmac" do 157 | enc = Nanvault::Encrypted.new(header_ok + body_no_hmac) 158 | expect_raises(Nanvault::BadData, "Invalid input data body") do 159 | enc.parse 160 | end 161 | end 162 | 163 | it "correctly handles short body - empty salt" do 164 | enc = Nanvault::Encrypted.new(header_ok + "\n") 165 | expect_raises(Nanvault::BadData, "Invalid input data body") do 166 | enc.parse 167 | end 168 | end 169 | 170 | it "correctly handles short body - all empty" do 171 | enc = Nanvault::Encrypted.new(header_ok) 172 | expect_raises(Nanvault::BadData, "Invalid input data body") do 173 | enc.parse 174 | end 175 | end 176 | 177 | describe "#decrypt" do 178 | it "correctly decrypts data - ok" do 179 | enc = Nanvault::Encrypted.new cdata_ok 180 | enc.password = password 181 | enc.decrypt 182 | enc.ptext.should eq ptext 183 | end 184 | 185 | it "correctly decrypts data - bad password" do 186 | enc = Nanvault::Encrypted.new cdata_ok 187 | enc.password = "badpassword" 188 | expect_raises(Nanvault::BadData, "Bad HMAC: wrong password or corrupted data") do 189 | enc.decrypt 190 | end 191 | end 192 | end 193 | 194 | describe "#plaintext_string" do 195 | it "correctly return plaintext string" do 196 | enc = Nanvault::Encrypted.new cdata_ok 197 | enc.password = password 198 | enc.decrypt 199 | enc.plaintext_str.should eq ptext_str 200 | end 201 | end 202 | end 203 | end 204 | 205 | describe Nanvault::Plaintext do 206 | ptext_str = "---\n# Test file\n- One\n- Two\n" 207 | password = "foo" 208 | 209 | salt = Bytes[73, 78, 139, 40, 17, 139, 123, 226, 11, 105, 246, 154, 89, 109, 40, 72, 210 | 136, 16, 95, 193, 237, 86, 177, 98, 170, 54, 183, 152, 9, 6, 8, 50] 211 | 212 | hmac = Bytes[95, 22, 121, 151, 113, 253, 23, 236, 60, 183, 250, 3, 232, 239, 99, 160, 213 | 240, 38, 146, 170, 17, 250, 93, 101, 160, 163, 214, 189, 255, 174, 131, 237] 214 | 215 | ctext = Bytes[220, 238, 173, 195, 180, 240, 156, 89, 206, 18, 16, 40, 208, 144, 151, 217, 216 | 117, 249, 63, 192, 109, 249, 63, 122, 208, 225, 18, 137, 52, 194, 253, 174] 217 | 218 | header_nolabel = "$ANSIBLE_VAULT;1.1;AES256\n" 219 | 220 | header_label = "$ANSIBLE_VAULT;1.2;AES256;mylabel\n" 221 | 222 | body_str = "34393465386232383131386237626532306236396636396135393664323834383838313035666331\n" \ 223 | "6564353662313632616133366237393830393036303833320a356631363739393737316664313765\n" \ 224 | "63336362376661303365386566363361306630323639326161313166613564363561306133643662\n" \ 225 | "6466666165383365640a646365656164633362346630396335396365313231303238643039303937\n" \ 226 | "64393735663933666330366466393366376164306531313238393334633266646165\n" 227 | 228 | describe "#encrypt_unsafe" do 229 | it "correctly encrypts data - ok" do 230 | pt = Nanvault::Plaintext.new(ptext_str) 231 | pt.password = password 232 | pt.salt = salt 233 | pt.encrypt_unsafe 234 | pt.ctext.should eq ctext 235 | pt.hmac.should eq hmac 236 | end 237 | 238 | it "correctly handles empty password" do 239 | pt = Nanvault::Plaintext.new(ptext_str) 240 | pt.password = "" 241 | pt.salt = salt 242 | expect_raises(ArgumentError, "Cannot encrypt with empty password") do 243 | pt.encrypt_unsafe 244 | end 245 | end 246 | end 247 | 248 | describe "#encrypted_str" do 249 | it "correctly computes encrypted string - no label" do 250 | pt = Nanvault::Plaintext.new(ptext_str) 251 | pt.password = password 252 | pt.salt = salt 253 | pt.encrypt_unsafe 254 | pt.encrypted_str.should eq (header_nolabel + body_str) 255 | end 256 | 257 | it "correctly computes encrypted string - label" do 258 | pt = Nanvault::Plaintext.new(ptext_str) 259 | pt.password = password 260 | pt.label = "mylabel" 261 | pt.salt = salt 262 | pt.encrypt_unsafe 263 | pt.encrypted_str.should eq (header_label + body_str) 264 | end 265 | end 266 | end 267 | 268 | describe Nanvault::Crypto do 269 | salt_hex = Bytes[52, 57, 52, 101, 56, 98, 50, 56, 49, 270 | 49, 56, 98, 55, 98, 101, 50, 48, 98, 54, 271 | 57, 102, 54, 57, 97, 53, 57, 54, 100, 50, 272 | 56, 52, 56, 56, 56, 49, 48, 53, 273 | 102, 99, 49, 101, 100, 53, 54, 98, 49, 274 | 54, 50, 97, 97, 51, 54, 98, 55, 57, 275 | 56, 48, 57, 48, 54, 48, 56, 51, 50] 276 | 277 | salt_hex_arr = salt_hex.to_a 278 | 279 | salt_hex_arr_chr = salt_hex_arr.map { |x| x.as(UInt8).chr } 280 | 281 | salt = salt_hex_arr_chr.join.hexbytes 282 | 283 | password = "foo" 284 | 285 | passbytes = password.to_slice 286 | 287 | cipher_key = Bytes[242, 148, 14, 232, 107, 148, 161, 161, 231, 163, 33, 126, 76, 248, 288 | 111, 156, 80, 25, 167, 128, 63, 196, 218, 59, 57, 127, 201, 253, 289 | 181, 244, 183, 59] 290 | 291 | hmac_key = Bytes[87, 24, 4, 143, 42, 152, 187, 104, 86, 91, 63, 212, 89, 61, 132, 54, 292 | 14, 218, 203, 106, 210, 14, 14, 151, 1, 125, 248, 53, 191, 53, 199, 108] 293 | 294 | cipher_iv = Bytes[251, 188, 216, 52, 195, 36, 99, 37, 67, 211, 168, 145, 210, 132, 3, 235] 295 | 296 | hmac = Bytes[95, 22, 121, 151, 113, 253, 23, 236, 60, 183, 250, 3, 232, 239, 99, 160, 297 | 240, 38, 146, 170, 17, 250, 93, 101, 160, 163, 214, 189, 255, 174, 131, 237] 298 | 299 | hmac_bad = Bytes[96, 23, 121, 151, 113, 253, 23, 236, 60, 183, 250, 3, 232, 239, 99, 160, 300 | 240, 38, 146, 170, 17, 250, 93, 101, 160, 163, 214, 189, 255, 174, 131, 237] 301 | 302 | ptext = Bytes[45, 45, 45, 10, 35, 32, 84, 101, 115, 116, 32, 102, 105, 108, 101, 10, 45, 303 | 32, 79, 110, 101, 10, 45, 32, 84, 119, 111, 10] 304 | 305 | ptext_mul_blksize = Bytes[45, 45, 45, 10, 35, 32, 84, 101, 115, 116, 32, 102, 105, 108, 101, 10, 45, 306 | 32, 79, 110, 101, 10, 45, 32, 84, 119, 111, 10, 45, 45, 45, 101] 307 | 308 | ctext = Bytes[220, 238, 173, 195, 180, 240, 156, 89, 206, 18, 16, 40, 208, 144, 151, 217, 309 | 117, 249, 63, 192, 109, 249, 63, 122, 208, 225, 18, 137, 52, 194, 253, 174] 310 | 311 | ctext_mul_blksize = Bytes[220, 238, 173, 195, 180, 240, 156, 89, 206, 18, 16, 40, 208, 144, 312 | 151, 217, 117, 249, 63, 192, 109, 249, 63, 122, 208, 225, 18, 137, 29, 235, 313 | 212, 207, 89, 168, 217, 149, 128, 129, 246, 245, 67, 45, 106, 29, 111, 124, 314 | 66, 19] 315 | 316 | describe "#get_keys_iv" do 317 | it "correctly get keys and iv" do 318 | com_cipher_key, com_hmac_key, com_cipher_iv = Nanvault::Crypto.get_keys_iv(salt, passbytes) 319 | com_cipher_key.should eq cipher_key 320 | com_hmac_key.should eq hmac_key 321 | com_cipher_iv.should eq cipher_iv 322 | end 323 | end 324 | 325 | describe "#check_hmac" do 326 | it "correctly checks valid hmac" do 327 | Nanvault::Crypto.check_hmac(ctext, hmac_key, hmac).should eq true 328 | end 329 | it "correctly handles invalid hmac" do 330 | expect_raises(Nanvault::BadData, "Bad HMAC: wrong password or corrupted data") do 331 | Nanvault::Crypto.check_hmac(ctext, hmac_key, hmac_bad) 332 | end 333 | end 334 | end 335 | 336 | describe "#decrypt" do 337 | it "correctly decrypts data" do 338 | com_ptext = Nanvault::Crypto.decrypt(cipher_iv, cipher_key, ctext) 339 | com_ptext.should eq ptext 340 | end 341 | it "correctly decrypts data - block-size multiple" do 342 | com_ptext = Nanvault::Crypto.decrypt(cipher_iv, cipher_key, ctext_mul_blksize) 343 | com_ptext.should eq ptext_mul_blksize 344 | end 345 | end 346 | 347 | describe "#encrypt" do 348 | it "correctly encrypts data" do 349 | com_ctext = Nanvault::Crypto.encrypt(cipher_iv, cipher_key, ptext) 350 | com_ctext.should eq ctext 351 | end 352 | it "correctly encrypts data - block-size multiple" do 353 | com_ctext = Nanvault::Crypto.encrypt(cipher_iv, cipher_key, ptext_mul_blksize) 354 | com_ctext.should eq ctext_mul_blksize 355 | end 356 | end 357 | 358 | describe "#genpass" do 359 | it "correctly generate password of the right length" do 360 | pass = Nanvault::Crypto.genpass 361 | pass.size.should eq 21 # 21 is (128/Math.log2(MAX_CHAR - MIN_CHAR - 1)).ceil.to_i + newline 362 | end 363 | end 364 | end 365 | 366 | describe Nanvault::VarUtil do 367 | hex = Bytes[52, 57, 52, 101, 56, 98, 50, 56, 49, 368 | 49, 56, 98, 55, 98, 101, 50, 48, 98, 54, 369 | 57, 102, 54, 57, 97, 53, 57, 54, 100, 50, 370 | 56, 52, 56, 56, 56, 49, 48, 53, 371 | 102, 99, 49, 101, 100, 53, 54, 98, 49, 372 | 54, 50, 97, 97, 51, 54, 98, 55, 57, 373 | 56, 48, 57, 48, 54, 48, 56, 51, 50] 374 | 375 | plain_bytes = Bytes[73, 78, 139, 40, 17, 139, 123, 226, 11, 105, 246, 154, 89, 109, 40, 72, 376 | 136, 16, 95, 193, 237, 86, 177, 98, 170, 54, 183, 152, 9, 6, 8, 50] 377 | 378 | describe "#unhexlify" do 379 | it "correctly unhexlifies data" do 380 | Nanvault::VarUtil.unhexlify(hex).should eq plain_bytes 381 | end 382 | it "correctly handles invalid hex data" do 383 | expect_raises(Nanvault::BadData, "Bad data: invalid hex") do 384 | Nanvault::VarUtil.unhexlify(Bytes[52, 252]) 385 | end 386 | end 387 | end 388 | 389 | describe "#cat_sl" do 390 | slice1 = Slice[1_u8, 1_u8] 391 | slice2 = Slice[2_u8, 2_u8] 392 | slice_res = Slice[1_u8, 1_u8, 2_u8, 2_u8] 393 | 394 | it "correctly concatenate slices" do 395 | Nanvault::VarUtil.cat_sl_u8(slice1, slice2).should eq slice_res 396 | end 397 | it "correctly concatenate slices - empty slice" do 398 | Nanvault::VarUtil.cat_sl_u8(slice1, Slice(UInt8).new 0).should eq slice1 399 | end 400 | end 401 | end 402 | 403 | describe Nanvault::YAMLString do 404 | valid_yaml = "the_secret: !vault |\n" \ 405 | " $ANSIBLE_VAULT;1.1;AES256\n" \ 406 | " 62313365396662343061393464336163383764373764613633653634306231386433626436623361\n" \ 407 | " 6134333665353966363534333632666535333761666131620a663537646436643839616531643561\n" \ 408 | " 63396265333966386166373632626539326166353965363262633030333630313338646335303630\n" \ 409 | " 3438626666666137650a353638643435666633633964366338633066623234616432373231333331\n" \ 410 | " 6564\n" 411 | 412 | other_valid_yaml = "another_secret: !vault |\n" \ 413 | " $ANSIBLE_VAULT;1.1;AES256\n" \ 414 | " 62313365396662343061393464336163383764373764613633653634306231386433626436623361\n" \ 415 | " 6134333665353966363534333632666535333761666131620a663537646436643839616531643561\n" \ 416 | " 63396265333966386166373632626539326166353965363262633030333630313338646335303630\n" \ 417 | " 3438626666666137650a353638643435666633633964366338633066623234616432373231333331\n" \ 418 | " 6564\n" 419 | 420 | invalid_yaml = "foofoo" 421 | 422 | value = "$ANSIBLE_VAULT;1.1;AES256\n" \ 423 | "62313365396662343061393464336163383764373764613633653634306231386433626436623361\n" \ 424 | "6134333665353966363534333632666535333761666131620a663537646436643839616531643561\n" \ 425 | "63396265333966386166373632626539326166353965363262633030333630313338646335303630\n" \ 426 | "3438626666666137650a353638643435666633633964366338633066623234616432373231333331\n" \ 427 | "6564\n" 428 | 429 | describe "#get_value" do 430 | it "correctly gets value from valid yaml" do 431 | Nanvault::YAMLString.get_value(valid_yaml).should eq value 432 | end 433 | it "correctly handles bad yaml string" do 434 | expect_raises(ArgumentError, "Bad yaml string") do 435 | Nanvault::YAMLString.get_value(invalid_yaml) 436 | end 437 | end 438 | it "correctly handles bad yaml string - multiple kv" do 439 | expect_raises(ArgumentError, "Bad yaml string: cannot handle multiple key-value pairs") do 440 | Nanvault::YAMLString.get_value(valid_yaml + "\n" + other_valid_yaml) 441 | end 442 | end 443 | end 444 | 445 | describe "#get_yaml" do 446 | it "correctly gets yaml from valid k-v" do 447 | Nanvault::YAMLString.get_yaml("the_secret", value).should eq valid_yaml 448 | end 449 | end 450 | end 451 | end 452 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/nanvault" 3 | -------------------------------------------------------------------------------- /spec/testfiles/NOTES.md: -------------------------------------------------------------------------------- 1 | Files are encrypted by *ansible-vault* with password *"foo"*. 2 | -------------------------------------------------------------------------------- /spec/testfiles/test1.enc: -------------------------------------------------------------------------------- 1 | $ANSIBLE_VAULT;1.1;AES256 2 | 34393465386232383131386237626532306236396636396135393664323834383838313035666331 3 | 6564353662313632616133366237393830393036303833320a356631363739393737316664313765 4 | 63336362376661303365386566363361306630323639326161313166613564363561306133643662 5 | 6466666165383365640a646365656164633362346630396335396365313231303238643039303937 6 | 64393735663933666330366466393366376164306531313238393334633266646165 7 | -------------------------------------------------------------------------------- /spec/testfiles/test1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Test file 3 | - One 4 | - Two 5 | -------------------------------------------------------------------------------- /src/cli.cr: -------------------------------------------------------------------------------- 1 | require "option_parser" 2 | require "./nanvault" 3 | 4 | # environment variable key for vault password file 5 | ENV_NAME = "NANVAULT_PASSFILE" 6 | 7 | # password file short option 8 | PASSFILE_SHORT_OPT = "-p PASSFILE" 9 | 10 | label = "" 11 | op = :none 12 | password = "" 13 | infile = "" 14 | outfile = "" 15 | pass_file_passed = "" 16 | pass_file = "" 17 | 18 | OptionParser.parse do |parser| 19 | parser.banner = "nanvault: a standalone CLI tool to encrypt and decrypt files in the Ansible Vault format.\n" \ 20 | "More information, usage examples and candies at:\n" \ 21 | "https://github.com/marcobellaccini/nanvault\n" \ 22 | "Usage: nanvault" 23 | parser.on(PASSFILE_SHORT_OPT, "--vault-password-file=PASSFILE", "Specifies the vault password file") { |p| pass_file_passed = p } 24 | parser.on("-g", "--generate", "Password-generation mode: generates safe password") { write_stdout(Nanvault::Crypto.genpass); exit(0) } 25 | parser.on("-y KEYNAME", "--to-yaml=KEYNAME", "YAML-string mode: to YAML") { |k| to_yaml_mode(k) } 26 | parser.on("-Y", "--from-yaml", "YAML-string mode: from YAML") { from_yaml_mode() } 27 | parser.on("-l LABEL", "--label=LABEL", "Specifies the vault-id-label") { |l| label = l } 28 | parser.on("--version", "Print version") { puts "nanvault version #{Nanvault::VERSION}"; exit(0) } 29 | parser.on("-h", "--help", "Show this help") { puts parser; exit(0) } 30 | parser.unknown_args do |args| 31 | # filter out unknown options 32 | unk_args = args.find { |s| !s.starts_with?("-") } 33 | if !unk_args.nil? 34 | STDERR.puts "ERROR: this program does not need arguments!" 35 | STDERR.puts parser 36 | exit(1) 37 | end 38 | end 39 | parser.invalid_option do |flag| 40 | STDERR.puts "ERROR: #{flag} is not a valid option." 41 | STDERR.puts parser 42 | exit(1) 43 | end 44 | parser.missing_option do |flag| 45 | STDERR.puts "ERROR: incomplete or missing option '#{flag}'." 46 | STDERR.puts parser 47 | exit(1) 48 | end 49 | end 50 | 51 | # method to handle to-yaml mode 52 | def to_yaml_mode(yaml_key) 53 | in_data = read_stdin() 54 | begin 55 | out_data = Nanvault::YAMLString.get_yaml(yaml_key, in_data) 56 | rescue ex 57 | STDERR.puts "ERROR: #{ex.message}" 58 | exit(1) 59 | end 60 | write_stdout(out_data) 61 | exit(0) 62 | end 63 | 64 | # method to handle from-yaml mode 65 | def from_yaml_mode 66 | in_data = read_stdin() 67 | begin 68 | out_data = Nanvault::YAMLString.get_value(in_data) 69 | rescue ex 70 | STDERR.puts "ERROR: #{ex.message}" 71 | exit(1) 72 | end 73 | write_stdout(out_data) 74 | exit(0) 75 | end 76 | 77 | # method to read from stdin 78 | def read_stdin 79 | # read input from stdin 80 | begin 81 | return STDIN.gets_to_end 82 | rescue 83 | STDERR.puts "ERROR: unable to get input data." 84 | exit(1) 85 | end 86 | end 87 | 88 | # method to write to stdout 89 | def write_stdout(data) 90 | # write to stdout without trailing newline 91 | STDOUT.print "#{data}" 92 | STDOUT.flush 93 | end 94 | 95 | # if a password file was specified via command-line option 96 | if pass_file_passed != "" 97 | pass_file = pass_file_passed 98 | # if a password file was not specified 99 | # but the ENV_NAME env var is available 100 | elsif ENV.has_key?(ENV_NAME) 101 | pass_file = ENV[ENV_NAME] 102 | else 103 | STDERR.puts "ERROR: no password file is available." 104 | STDERR.puts "Please specify a password file through the '#{PASSFILE_SHORT_OPT}' " \ 105 | "command-line option or the '#{ENV_NAME}' environment variable." 106 | exit(1) 107 | end 108 | 109 | # read password file 110 | if pass_file != "" 111 | begin 112 | # in order to mimic ansible-vault behavior, newline should be chomped 113 | password = File.read(pass_file).chomp 114 | rescue 115 | STDERR.puts "ERROR: unable to read vault password file '#{pass_file}'" 116 | exit(1) 117 | end 118 | end 119 | 120 | # read input from stdin 121 | in_data = read_stdin() 122 | 123 | # determine whether to encrypt or decrypt 124 | # (by checking if input data is already encrypted) 125 | if in_data.starts_with?("$ANSIBLE_VAULT") 126 | op = :decrypt 127 | else 128 | op = :encrypt 129 | end 130 | 131 | begin 132 | case op 133 | when :encrypt 134 | pt = Nanvault::Plaintext.new in_data 135 | pt.password = password || "" 136 | if label != "" 137 | pt.label = label 138 | end 139 | pt.encrypt 140 | out_data = pt.encrypted_str 141 | when :decrypt 142 | enc = Nanvault::Encrypted.new in_data 143 | enc.password = password || "" 144 | enc.decrypt 145 | out_data = enc.plaintext_str 146 | end 147 | rescue ex 148 | STDERR.puts "ERROR: #{ex.message}" 149 | exit(1) 150 | else 151 | # write output to stdout 152 | write_stdout(out_data) 153 | exit(0) 154 | end 155 | -------------------------------------------------------------------------------- /src/nanvault.cr: -------------------------------------------------------------------------------- 1 | require "openssl" 2 | require "openssl/hmac" 3 | require "yaml" 4 | 5 | # `Nanvault` module 6 | module Nanvault 7 | VERSION = "0.2.3" 8 | 9 | # Encrypted data class 10 | class Encrypted 11 | # these initializations also prevent this: 12 | # https://github.com/crystal-lang/crystal/issues/5931 13 | property header = "", body = "" 14 | property bbody = Slice(UInt8).new 1 15 | property salt = Slice(UInt8).new 1 16 | property hmac = Slice(UInt8).new 1 17 | property ctext = Slice(UInt8).new 1 18 | property ptext = Slice(UInt8).new 1 19 | property vault_info = Hash(String, String | Nil).new 20 | 21 | setter password : String 22 | 23 | def initialize(ctext_str : String) 24 | # initialize password 25 | @password = "" 26 | begin 27 | ctext_lines = ctext_str.split("\n") 28 | @header = ctext_lines[0] 29 | # this also handles the header-only data case 30 | if ctext_lines[1] 31 | @body = ctext_lines[1..-1].join 32 | end 33 | # rescue for bad data 34 | rescue ex : IndexError 35 | raise BadData.new("Invalid input data") 36 | end 37 | end 38 | 39 | # parse method 40 | def parse 41 | parse_header() 42 | 43 | case @vault_info["version"] 44 | when "1.1", "1.2" 45 | parse_body() 46 | else 47 | raise BadData.new("Sorry: format version #{@vault_info["version"]} is not supported") 48 | end 49 | end 50 | 51 | # parse header method 52 | private def parse_header 53 | header_re = /^\$ANSIBLE_VAULT;(?[^;\n\s]+);(?[^;\n\s]+);?(?