cici 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE +19 -0
- data/README.md +212 -0
- data/bin/cici +6 -0
- data/lib/cici.rb +3 -0
- data/lib/cici/cli.rb +92 -0
- data/lib/cici/config.rb +128 -0
- data/lib/cici/constants.rb +6 -0
- data/lib/cici/decrypt.rb +87 -0
- data/lib/cici/encrypt.rb +113 -0
- data/lib/cici/ui.rb +33 -0
- data/lib/cici/util.rb +20 -0
- data/lib/cici/version.rb +9 -0
- metadata +159 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e0e0d27ce6125fb072e36aa5a3e197bd3ad70742d5171c2f2f360b771179d23a
|
4
|
+
data.tar.gz: bd919b3ef9990aa2952461ee646febdb9c803fbafe2c2f298d5168ea0b286731
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d1f5f125bbcfb71b5975f1cec0a5aeb7a442af524ceca952d6e8583da752bfb79627397958768bacd88dc89aa44c4333053f0f8fe76a5de58c6b5a381fddd735
|
7
|
+
data.tar.gz: 71614938679b2d015f7224e5d4d4f754d2d116a8a687fe3a5b36fed1aded3e429720fe3640b8475a7703d4e1acae5e73d17bf68d2cbb320c456d143a3d7e27be
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
## [0.1.0] - 2019-12-17
|
2
|
+
|
3
|
+
### Added
|
4
|
+
- CLI able to compress and encrypt directory.
|
5
|
+
- CLI able to decrypt, uncompress, and copy files to destinations.
|
6
|
+
- CLI reads from yml config file to control whole program.
|
7
|
+
- CLI adds entries to .gitignore so secrets do not get added to repo by accident.
|
8
|
+
- CLI able to encrypt/decrypt a default set of files or a separate collection of files.
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2019 Levi Bostian <levi.bostian@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
[![Gem](https://img.shields.io/gem/v/cici.svg)](https://rubygems.org/gems/cici)
|
2
|
+
[![Travis (.com)](https://travis-ci.com/levibostian/cici.svg?branch=master)](https://travis-ci.com/levibostian/cici)
|
3
|
+
[![GitHub](https://img.shields.io/github/license/levibostian/cici.svg)](https://github.com/levibostian/cici)
|
4
|
+
|
5
|
+
# cici
|
6
|
+
|
7
|
+
*Confidential Information for Continuous Integration (CICI)*
|
8
|
+
|
9
|
+
When environment variables are not enough and you need to store secrets within files, `cici` is your friend. Store secret files in your source code repository with ease.
|
10
|
+
|
11
|
+
*Note: Can be used without a CI server, but tool is primarily designed for your CI server to decrypt your secret files for deployment.*
|
12
|
+
|
13
|
+
# What is cici?
|
14
|
+
|
15
|
+
`cici` is a CLI program where you can encrypt a directory of confidential files on your local machine, then decrypt that directory of files on a CI server with great ease and flexibility. Store secrets in your source code, easily without checking those secrets into source control.
|
16
|
+
|
17
|
+
# Why use cici?
|
18
|
+
|
19
|
+
`cici` was inspired by Travis-CI's ability to encrypt files, but it's only limited to encrypting 1 file, per Travis repository. We can get around the 1 file limitation because we can just compress a directory of files into 1 compressed file using `zip` or `tar`. Well, that's great, but what about when we get to the CI server and we need to decrypt those secret files and then copy them from their original source to their final destination? It can start to get complex.
|
20
|
+
|
21
|
+
It would be awesome if we could simply write 1 command on the CI server: `cici decrypt` and automatically for us, the secret files our project depends on will be decrypted and then each secret file is copied to their destination in the source code. Nice!
|
22
|
+
|
23
|
+
But what if we have a production and a staging server? Easy. `cici decrypt --set production` or `cici decrypt --set staging`. `cici` can be configured with any number of sets of files.
|
24
|
+
|
25
|
+
Besides this simplicity and power, `cici` provides some nice features:
|
26
|
+
1. Use `cici` with any CI service or git hosting service. It's not opinionated. You don't even need to use a CI service, really, if you just want to store private files in source code.
|
27
|
+
2. `cici` will add entries to your `.gitignore` file for you to make sure you don't accidentally add secrets to your git repo.
|
28
|
+
3. Full flexibility of where your secrets are stored with a configuration file you check into source control.
|
29
|
+
|
30
|
+
# Getting started
|
31
|
+
|
32
|
+
* Install this tool:
|
33
|
+
|
34
|
+
```
|
35
|
+
gem install cici
|
36
|
+
```
|
37
|
+
|
38
|
+
* Config. Let's use an example to explain the rest of the guide on getting started.
|
39
|
+
|
40
|
+
Let's say that you're building an app with the following secret files required to compile your project:
|
41
|
+
1. `.env`
|
42
|
+
2. `src/firebase/firebase-secrets.json`
|
43
|
+
3. `App/GoogleService-Info.plist`
|
44
|
+
|
45
|
+
Let's also say that we have a production and a beta app. 2 separate environments that require the same 3 files for each environment.
|
46
|
+
|
47
|
+
All you need to do is...
|
48
|
+
1. Create a `secrets/` directory in your project source code with this file structure:
|
49
|
+
```
|
50
|
+
secrets/
|
51
|
+
.env
|
52
|
+
src/
|
53
|
+
firebase/
|
54
|
+
firebase-secrets.json
|
55
|
+
App/
|
56
|
+
GoogleService-Info.plist
|
57
|
+
beta/
|
58
|
+
.env
|
59
|
+
src/
|
60
|
+
firebase/
|
61
|
+
firebase-secrets.json
|
62
|
+
App/
|
63
|
+
GoogleService-Info.plist
|
64
|
+
```
|
65
|
+
|
66
|
+
2. Create a `.cici.yml` config file in the root of your project with the following:
|
67
|
+
|
68
|
+
```yml
|
69
|
+
default:
|
70
|
+
secrets:
|
71
|
+
- ".env"
|
72
|
+
- "src/firebase/firebase-secrets.json"
|
73
|
+
- "App/GoogleService-Info.plist"
|
74
|
+
sets:
|
75
|
+
beta:
|
76
|
+
```
|
77
|
+
|
78
|
+
This config file here defines a default set of files that are secrets and also states that we have a set of files besides the default for "beta". `cici` requires you state a default set of secret files. It's up to you to decide what that default is. In this example, we decided that production should be the default set. You can have a development environment be your default. Then all other sets you need, define those in `sets` in the config.
|
79
|
+
|
80
|
+
* Time to encrypt!
|
81
|
+
|
82
|
+
On your local development machine, run the command: `cici encrypt`. You will know the command ran successfully when you see "Success!" with further instructions of what to do next.
|
83
|
+
|
84
|
+
Make sure to follow the instructions printed out after the command so you can successfully decrypt. This includes setting *secret* environment variables on your CI machine (or whatever machine you're decrypting the data). Note: Make sure to keep these environment variables a secret. Follow the instructions for your given CI service to create environment variables that are not publicly viewable.
|
85
|
+
|
86
|
+
Here are some instructions for some CI providers. Add yours if you don't see it below:
|
87
|
+
* [Travis-CI](https://docs.travis-ci.com/user/environment-variables/#defining-encrypted-variables-in-travisyml)
|
88
|
+
|
89
|
+
* Now, it's time to decrypt. After you add the secret environment variables above, you need to run one of the following commands on the CI server:
|
90
|
+
|
91
|
+
```
|
92
|
+
cici decrypt
|
93
|
+
```
|
94
|
+
|
95
|
+
...for the default production environment...
|
96
|
+
|
97
|
+
or,
|
98
|
+
|
99
|
+
```
|
100
|
+
cici decrypt --set beta
|
101
|
+
```
|
102
|
+
|
103
|
+
...for the beta environment.
|
104
|
+
|
105
|
+
Done! What `cici` has done is (1) decrypted the encrypted file you made with the encryption step, (2) taken the production set of files or the beta set of files and copied them from the "secrets" directory into your project's source code where they belong.
|
106
|
+
|
107
|
+
So, if you have the following configuration file:
|
108
|
+
|
109
|
+
```yml
|
110
|
+
default:
|
111
|
+
secrets:
|
112
|
+
- ".env"
|
113
|
+
- "src/firebase/firebase-secrets.json"
|
114
|
+
```
|
115
|
+
|
116
|
+
and you run `cici decrypt`, `cici` will perform the following copy operations for you:
|
117
|
+
|
118
|
+
1. `secrets/.env` -> `.env`
|
119
|
+
2. `secrets/src/firebase/firebase-secrets.json` -> `src/firebase/firebase-secrets.json`
|
120
|
+
|
121
|
+
and if you run `cici decrypt --set beta`, `cici` will perform the following copy operations for you:
|
122
|
+
|
123
|
+
1. `secrets/beta/.env` -> `.env`
|
124
|
+
2. `secrets/beta/src/firebase/firebase-secrets.json` -> `src/firebase/firebase-secrets.json`
|
125
|
+
|
126
|
+
You're all done! I hope you enjoy `cici`.
|
127
|
+
|
128
|
+
# Advanced configuration
|
129
|
+
|
130
|
+
Here is a more advanced configuration file including all options the config file has to offer:
|
131
|
+
|
132
|
+
```yml
|
133
|
+
path: "_secrets"
|
134
|
+
default:
|
135
|
+
secrets:
|
136
|
+
- "file.txt"
|
137
|
+
- "path/file2.txt"
|
138
|
+
sets:
|
139
|
+
production:
|
140
|
+
path: "_production"
|
141
|
+
staging:
|
142
|
+
secrets:
|
143
|
+
- "file3.txt"
|
144
|
+
output: "secrets_cici"
|
145
|
+
skip_gitignore: false
|
146
|
+
```
|
147
|
+
|
148
|
+
Here is a breakdown of this file:
|
149
|
+
|
150
|
+
```yml
|
151
|
+
path: (optional, default 'secrets') - the name of the directory your secrets are stored.
|
152
|
+
default: (required) - specifies a default set of files you want to encrypt/decrypt
|
153
|
+
secrets:
|
154
|
+
- "file.txt"
|
155
|
+
- "path/file2.txt"
|
156
|
+
sets: (optional) - specify a unique collection of files to encrypt/decrypt
|
157
|
+
production: name of a set used as CLI argument to decrypt
|
158
|
+
path: (optional, default name of set) - subdirectory within "path" to store files for this set
|
159
|
+
staging: another set
|
160
|
+
secrets: (optional, default is default secrets within subdirectory) set of files to encrypt/decrypt.
|
161
|
+
- "file3.txt"
|
162
|
+
output: "secrets_cici" (optional, default, "secrets") - output file name when secrets compressed
|
163
|
+
skip_gitignore: (optional, default true) - have cici add rules to .gitignore automatically or not for you.
|
164
|
+
```
|
165
|
+
|
166
|
+
## Development
|
167
|
+
|
168
|
+
```bash
|
169
|
+
$> bundle install
|
170
|
+
```
|
171
|
+
|
172
|
+
You're ready to start developing!
|
173
|
+
|
174
|
+
##### Lint
|
175
|
+
|
176
|
+
```
|
177
|
+
bundle exec rake lint
|
178
|
+
```
|
179
|
+
|
180
|
+
##### Build/install
|
181
|
+
|
182
|
+
```
|
183
|
+
bundle exec rake install
|
184
|
+
bundle exec cici # You're running cici!
|
185
|
+
```
|
186
|
+
|
187
|
+
Or,
|
188
|
+
|
189
|
+
```
|
190
|
+
bundle exec rake build; gem install cici*.gem
|
191
|
+
cici # you have installed cici to your whole machine!
|
192
|
+
```
|
193
|
+
|
194
|
+
## Deployment
|
195
|
+
|
196
|
+
This gem is setup automatically to deploy to RubyGems on a git tag deployment.
|
197
|
+
|
198
|
+
* Add `RUBYGEMS_KEY` secret to Travis-CI's settings.
|
199
|
+
* Make a new git tag, push it up to GitHub. Travis will deploy for you.
|
200
|
+
|
201
|
+
## Author
|
202
|
+
|
203
|
+
* Levi Bostian - [GitHub](https://github.com/levibostian), [Twitter](https://twitter.com/levibostian), [Website/blog](http://levibostian.com)
|
204
|
+
|
205
|
+
![Levi Bostian image](https://gravatar.com/avatar/22355580305146b21508c74ff6b44bc5?s=250)
|
206
|
+
|
207
|
+
## Contribute
|
208
|
+
|
209
|
+
cici is open for pull requests. Check out the [list of issues](https://github.com/levibostian/cici/issues) for tasks I am planning on working on. Check them out if you wish to contribute in that way.
|
210
|
+
|
211
|
+
**Want to add features?** Before you decide to take a bunch of time and add functionality to the library, please, [create an issue]
|
212
|
+
(https://github.com/levibostian/cici/issues/new) stating what you wish to add. This might save you some time in case your purpose does not fit well in the use cases of this project.
|
data/bin/cici
ADDED
data/lib/cici.rb
ADDED
data/lib/cici/cli.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorize'
|
4
|
+
require 'optparse'
|
5
|
+
require 'set'
|
6
|
+
require 'pathname'
|
7
|
+
require_relative './ui'
|
8
|
+
require_relative './util'
|
9
|
+
require_relative './encrypt'
|
10
|
+
require_relative './decrypt'
|
11
|
+
require_relative './version'
|
12
|
+
require_relative './config'
|
13
|
+
|
14
|
+
module CICI
|
15
|
+
Options = Struct.new(:verbose, :debug, :help, :set)
|
16
|
+
|
17
|
+
class CLI
|
18
|
+
def initialize
|
19
|
+
@options = parse_options
|
20
|
+
|
21
|
+
@ui = CICI::UI.new(@options.verbose, @options.debug)
|
22
|
+
@ui.debug("Options: #{@options}")
|
23
|
+
|
24
|
+
@config = CICI::Config.new(@ui)
|
25
|
+
@config.load
|
26
|
+
|
27
|
+
run_command
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_command
|
31
|
+
case ARGV[0]
|
32
|
+
when 'encrypt'
|
33
|
+
encrypt
|
34
|
+
when 'decrypt'
|
35
|
+
decrypt
|
36
|
+
else
|
37
|
+
@ui.fail('Command invalid.')
|
38
|
+
print_help(1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def print_help(exit_code)
|
43
|
+
puts @options.help
|
44
|
+
exit exit_code
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_options
|
48
|
+
options = Options.new
|
49
|
+
options.verbose = false
|
50
|
+
options.debug = false
|
51
|
+
|
52
|
+
opt_parser = OptionParser.new do |opts|
|
53
|
+
opts.banner = 'Usage: cici encrypt|decrypt [options]'
|
54
|
+
|
55
|
+
opts.on('-v', '--version', 'Print version') do
|
56
|
+
puts CICI::Version.get
|
57
|
+
exit
|
58
|
+
end
|
59
|
+
opts.on('--verbose', 'Verbose output') do
|
60
|
+
options.verbose = true
|
61
|
+
end
|
62
|
+
opts.on('--debug', 'Debug output (also turns on verbose)') do
|
63
|
+
options.verbose = true
|
64
|
+
options.debug = true
|
65
|
+
end
|
66
|
+
opts.on('--set SET_NAME', 'Set to decrypt (Note: option ignored for encrypt command)') do |set_name|
|
67
|
+
options.set = set_name
|
68
|
+
end
|
69
|
+
opts.on('-h', '--help', 'Prints this help') do
|
70
|
+
puts opts
|
71
|
+
exit
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
help = opt_parser.help
|
76
|
+
options.help = help
|
77
|
+
abort(help) if ARGV.empty?
|
78
|
+
|
79
|
+
opt_parser.parse!(ARGV)
|
80
|
+
|
81
|
+
options
|
82
|
+
end
|
83
|
+
|
84
|
+
def encrypt
|
85
|
+
CICI::Encrypt.new(@ui, @config).start
|
86
|
+
end
|
87
|
+
|
88
|
+
def decrypt
|
89
|
+
CICI::Decrypt.new(@ui, @config).start(@options.set)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/cici/config.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
module CICI
|
7
|
+
class Config
|
8
|
+
def initialize(ui)
|
9
|
+
@ui = ui
|
10
|
+
end
|
11
|
+
|
12
|
+
def load
|
13
|
+
config_file_name = '.cici.yml'
|
14
|
+
|
15
|
+
@ui.fail("Cannot find config file, #{config_file_name} in current directory.") unless File.file?(config_file_name)
|
16
|
+
|
17
|
+
config_file_contents = File.read(config_file_name)
|
18
|
+
@config = YAML.safe_load(config_file_contents)
|
19
|
+
|
20
|
+
@ui.verbose("Loaded config from file, #{config_file_name}")
|
21
|
+
@ui.debug("Config: #{@config}")
|
22
|
+
end
|
23
|
+
|
24
|
+
# Functions below are to pull out parts from the config file
|
25
|
+
|
26
|
+
# Get "path", or default value
|
27
|
+
def base_path
|
28
|
+
@config['path'] || 'secrets'
|
29
|
+
end
|
30
|
+
|
31
|
+
# Gets default array of secrets. Each secrets includes 'base_path' so they each look like:
|
32
|
+
# "secrets/path_to_file/file.txt"
|
33
|
+
def default_secrets
|
34
|
+
default_secrets_without_base_path.map { |secret_path| Pathname.new(base_path).join(secret_path).to_s }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Same as default_secrets(), but omit "base_path" inclusion. So you get raw entires from config file.
|
38
|
+
def default_secrets_without_base_path
|
39
|
+
secrets = []
|
40
|
+
return secrets unless @config.key? 'default'
|
41
|
+
|
42
|
+
return @config['default']['secrets'] if @config['default'].key? 'secrets'
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get a Hash for the set from the config file
|
46
|
+
def set(name)
|
47
|
+
@ui.fail("Set, #{name}, does not exist in config file.") unless @config['sets'].key? name
|
48
|
+
|
49
|
+
set = @config['sets'][name]
|
50
|
+
|
51
|
+
set = {} if set.nil?
|
52
|
+
|
53
|
+
set
|
54
|
+
end
|
55
|
+
|
56
|
+
# Gets the "base_path" for where all secrets will be stored for a set.
|
57
|
+
# If set name is `production` and base path is `secrets/`, this function could return: "secrets/production/"
|
58
|
+
def path_for_set(set_name)
|
59
|
+
set = set(set_name)
|
60
|
+
path = Pathname.new(base_path)
|
61
|
+
|
62
|
+
directory = set.key?('path') ? set['path'] : set_name
|
63
|
+
path = path.join(directory)
|
64
|
+
|
65
|
+
path.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
# Same as secrets_for_set(), but omit "base_path" inclusion. So you get raw entires from config file.
|
69
|
+
def secrets_for_set_without_base_path(set_name)
|
70
|
+
set = set(set_name)
|
71
|
+
return set['secrets'] if set.key? 'secrets'
|
72
|
+
|
73
|
+
default_secrets_without_base_path
|
74
|
+
end
|
75
|
+
|
76
|
+
# Gets array of secrets for a set. Each secrets includes 'base_path' so they each look like:
|
77
|
+
# "secrets/name-of-set/path_to_file/file.txt"
|
78
|
+
def secrets_for_set(set_name)
|
79
|
+
secrets_for_set_without_base_path(set_name).map { |secret_path| Pathname.new(path_for_set(set_name)).join(secret_path).to_s }
|
80
|
+
end
|
81
|
+
|
82
|
+
# Should skip gitignore operation?
|
83
|
+
def skip_gitignore?
|
84
|
+
skip = false
|
85
|
+
return @config['skip_gitignore'] if @config.key? 'skip_gitignore'
|
86
|
+
|
87
|
+
skip
|
88
|
+
end
|
89
|
+
|
90
|
+
# Get array of all secrets, including their base paths. So, a collection of files in the secrets directory to compress.
|
91
|
+
def all_secrets
|
92
|
+
secrets = Set[]
|
93
|
+
secrets.merge(default_secrets)
|
94
|
+
sets.keys.each { |set_key| secrets.merge(secrets_for_set(set_key)) }
|
95
|
+
|
96
|
+
secrets.to_a
|
97
|
+
end
|
98
|
+
|
99
|
+
# Same as all_secrets(), but without base paths. So, a collection of files in their original source locations.
|
100
|
+
def all_secrets_original_paths
|
101
|
+
secrets = Set[]
|
102
|
+
secrets.merge(default_secrets_without_base_path)
|
103
|
+
sets.keys.each { |set_key| secrets.merge(secrets_for_set_without_base_path(set_key)) }
|
104
|
+
|
105
|
+
secrets.to_a
|
106
|
+
end
|
107
|
+
|
108
|
+
# Hash of all sets
|
109
|
+
def sets
|
110
|
+
sets = {}
|
111
|
+
sets = @config['sets'] if @config.key? 'sets'
|
112
|
+
sets
|
113
|
+
end
|
114
|
+
|
115
|
+
# outout file name. Includes file extension
|
116
|
+
def output_file
|
117
|
+
output_file = 'secrets.tar'
|
118
|
+
output_file = "#{@config['output']}.tar" unless @config['output'].nil?
|
119
|
+
|
120
|
+
output_file
|
121
|
+
end
|
122
|
+
|
123
|
+
# output file name plus encryption file extension
|
124
|
+
def output_file_encrypted
|
125
|
+
"#{output_file}.enc"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/cici/decrypt.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorize'
|
4
|
+
require 'optparse'
|
5
|
+
require 'set'
|
6
|
+
require 'pathname'
|
7
|
+
require_relative './ui'
|
8
|
+
require_relative './util'
|
9
|
+
require_relative './constants'
|
10
|
+
require 'base64'
|
11
|
+
require 'openssl'
|
12
|
+
require 'fileutils'
|
13
|
+
|
14
|
+
module CICI
|
15
|
+
class Decrypt
|
16
|
+
include CICI
|
17
|
+
|
18
|
+
def initialize(ui, config)
|
19
|
+
@ui = ui
|
20
|
+
@config = config
|
21
|
+
@util = CICI::Util.new(@ui)
|
22
|
+
end
|
23
|
+
|
24
|
+
def start(set)
|
25
|
+
@set = set
|
26
|
+
|
27
|
+
assert_encrypted_secret_exist
|
28
|
+
decrypt
|
29
|
+
decompress
|
30
|
+
copy_files
|
31
|
+
|
32
|
+
@ui.success('Files successfully decrypted and copied to their destination!')
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def assert_encrypted_secret_exist
|
38
|
+
@ui.fail("Encrypted secrets file, #{@config.output_file_encrypted}, does not exist") unless File.file?(@config.output_file_encrypted)
|
39
|
+
end
|
40
|
+
|
41
|
+
def decrypt
|
42
|
+
@ui.verbose('Decrypting secrets encrypted file.')
|
43
|
+
|
44
|
+
decipher = OpenSSL::Cipher.new('AES-256-CBC')
|
45
|
+
decipher.decrypt
|
46
|
+
decipher.key = Base64.decode64(@util.get_env(CICI::DECRYPT_KEY_ENV_VAR))
|
47
|
+
decipher.iv = Base64.decode64(@util.get_env(CICI::DECRYPT_IV_ENV_VAR))
|
48
|
+
|
49
|
+
plain = decipher.update(File.read(@config.output_file_encrypted)) + decipher.final
|
50
|
+
File.write(@config.output_file, plain)
|
51
|
+
end
|
52
|
+
|
53
|
+
def decompress
|
54
|
+
@ui.verbose('Decompressing compressed file.')
|
55
|
+
|
56
|
+
@util.run_command("tar xvf #{@config.output_file}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def copy_files
|
60
|
+
@ui.verbose('Copying files to their final destination')
|
61
|
+
|
62
|
+
copy_file = lambda { |path, secrets_path|
|
63
|
+
source = Pathname.new(secrets_path).join(path).to_s
|
64
|
+
destination = path
|
65
|
+
|
66
|
+
@ui.verbose("Copying file from #{source} to #{destination}")
|
67
|
+
|
68
|
+
parent_directory = Pathname.new(destination).expand_path.dirname.to_s
|
69
|
+
|
70
|
+
@ui.debug("mkdir -p for: #{parent_directory}")
|
71
|
+
FileUtils.mkdir_p(parent_directory)
|
72
|
+
@ui.debug("cp -r for, source: #{source}, destination: #{destination}")
|
73
|
+
FileUtils.cp_r(source, destination)
|
74
|
+
}
|
75
|
+
|
76
|
+
if @set.nil?
|
77
|
+
@config.default_secrets_without_base_path.each do |secret|
|
78
|
+
copy_file.call(secret, @config.base_path)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
@config.secrets_for_set_without_base_path(@set).each do |secret|
|
82
|
+
copy_file.call(secret, @config.path_for_set(@set))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/cici/encrypt.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorize'
|
4
|
+
require 'optparse'
|
5
|
+
require 'set'
|
6
|
+
require 'pathname'
|
7
|
+
require_relative './ui'
|
8
|
+
require_relative './util'
|
9
|
+
require_relative './constants'
|
10
|
+
require 'openssl'
|
11
|
+
require 'base64'
|
12
|
+
|
13
|
+
module CICI
|
14
|
+
class Encrypt
|
15
|
+
include CICI
|
16
|
+
|
17
|
+
def initialize(ui, config)
|
18
|
+
@ui = ui
|
19
|
+
@config = config
|
20
|
+
@util = CICI::Util.new(@ui)
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
assert_secret_files_exist
|
25
|
+
compress
|
26
|
+
assert_files_in_gitignore
|
27
|
+
encrypt
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def assert_secret_files_exist
|
33
|
+
@ui.verbose('Asserting secret files exist')
|
34
|
+
|
35
|
+
assert_file_exists = lambda { |file|
|
36
|
+
@ui.debug("Checking #{file} exists...")
|
37
|
+
|
38
|
+
@ui.fail("File or directory at path #{file} does not exist. Can't encrypt your secrets with missing secrets.") unless File.exist?(file)
|
39
|
+
}
|
40
|
+
|
41
|
+
@ui.verbose("Checking secrets exist in #{@config.base_path} directory.")
|
42
|
+
|
43
|
+
@config.all_secrets.each do |file|
|
44
|
+
assert_file_exists.call(file)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def compress
|
49
|
+
@ui.verbose('Compressing secrets...')
|
50
|
+
|
51
|
+
@util.run_command("tar cvf #{@config.output_file} #{@config.base_path}")
|
52
|
+
end
|
53
|
+
|
54
|
+
def encrypt
|
55
|
+
@ui.verbose("Encrypting #{@config.output_file} to file #{@config.output_file_encrypted}")
|
56
|
+
|
57
|
+
aes = OpenSSL::Cipher.new('AES-256-CBC')
|
58
|
+
data = File.binread(@config.output_file)
|
59
|
+
aes.encrypt
|
60
|
+
key = aes.random_key
|
61
|
+
iv = aes.random_iv
|
62
|
+
File.write(@config.output_file_encrypted, aes.update(data) + aes.final)
|
63
|
+
|
64
|
+
@ui.success('Success! Now, you need to follow these last few steps:')
|
65
|
+
@ui.success("1. Make sure to add #{@config.output_file_encrypted} to your source code repository")
|
66
|
+
@ui.success("2. Create a *secret* environment variable with key: #{CICI::DECRYPT_KEY_ENV_VAR} with value: #{Base64.encode64(key).strip}")
|
67
|
+
@ui.success("3. Create a *secret* environment variable with key: #{CICI::DECRYPT_IV_ENV_VAR} with value: #{Base64.encode64(iv).strip}")
|
68
|
+
end
|
69
|
+
|
70
|
+
def assert_files_in_gitignore
|
71
|
+
ignore_file_name = '.gitignore'
|
72
|
+
|
73
|
+
if @config.skip_gitignore? || !File.exist?(ignore_file_name)
|
74
|
+
@ui.verbose('Skipping adding entries to .gitignore file')
|
75
|
+
return
|
76
|
+
end
|
77
|
+
|
78
|
+
@ui.verbose("Adding entries to #{ignore_file_name} file")
|
79
|
+
|
80
|
+
current_gitignore_file_contents = Set[]
|
81
|
+
File.foreach(ignore_file_name).with_index do |line, _line_num|
|
82
|
+
line = line.strip
|
83
|
+
current_gitignore_file_contents = current_gitignore_file_contents.add(line)
|
84
|
+
end
|
85
|
+
@ui.debug("current contents of #{ignore_file_name}: #{current_gitignore_file_contents}")
|
86
|
+
|
87
|
+
new_gitignore_additions = current_gitignore_file_contents.clone
|
88
|
+
add_to_gitignore = lambda { |file|
|
89
|
+
new_gitignore_additions = new_gitignore_additions.add(file)
|
90
|
+
}
|
91
|
+
|
92
|
+
# Add all but the encrypted output file as that is required for decryption
|
93
|
+
add_to_gitignore.call(@config.output_file)
|
94
|
+
add_to_gitignore.call(@config.base_path)
|
95
|
+
@config.all_secrets_original_paths.each do |secret_file|
|
96
|
+
add_to_gitignore.call(secret_file)
|
97
|
+
end
|
98
|
+
|
99
|
+
new_gitignore_additions -= current_gitignore_file_contents
|
100
|
+
@ui.debug("additions to #{ignore_file_name}: #{new_gitignore_additions}")
|
101
|
+
|
102
|
+
new_gitignore_additions = new_gitignore_additions.to_a
|
103
|
+
unless new_gitignore_additions.empty? # only write if something to add
|
104
|
+
@ui.debug("writing new #{ignore_file_name} additions: #{new_gitignore_additions}")
|
105
|
+
gitignore_file_prepended_additions = new_gitignore_additions.join("\n") + "\n\n" + File.read(ignore_file_name)
|
106
|
+
|
107
|
+
File.write(ignore_file_name, gitignore_file_prepended_additions)
|
108
|
+
end
|
109
|
+
|
110
|
+
@ui.verbose("Done adding entries to #{ignore_file_name}")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/cici/ui.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorize'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
module CICI
|
7
|
+
class UI
|
8
|
+
def initialize(verbose, debug)
|
9
|
+
@verbose = verbose
|
10
|
+
@debug = debug
|
11
|
+
end
|
12
|
+
|
13
|
+
def success(message)
|
14
|
+
puts message.colorize(:green)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fail(message)
|
18
|
+
abort(message.colorize(:red))
|
19
|
+
end
|
20
|
+
|
21
|
+
def warning(message)
|
22
|
+
puts message.to_s.colorize(:yellow)
|
23
|
+
end
|
24
|
+
|
25
|
+
def verbose(message)
|
26
|
+
puts message.to_s if @verbose
|
27
|
+
end
|
28
|
+
|
29
|
+
def debug(message)
|
30
|
+
puts message.to_s.colorize(:light_blue) if @debug
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/cici/util.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CICI
|
4
|
+
class Util
|
5
|
+
def initialize(ui)
|
6
|
+
@ui = ui
|
7
|
+
end
|
8
|
+
|
9
|
+
def run_command(command)
|
10
|
+
@ui.warning("Running command: #{command}")
|
11
|
+
success = system(command)
|
12
|
+
@ui.fail("\nCommand failed. Fix issue and try again.") unless success
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_env(name)
|
16
|
+
@ui.fail("Forgot to specify environment variable, #{name}") unless ENV[name]
|
17
|
+
ENV[name]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/cici/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cici
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Levi Bostian
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-12-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: colorize
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.8.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.8'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.8.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rubocop
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.58'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 0.58.2
|
43
|
+
type: :development
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0.58'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 0.58.2
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rake
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '12.3'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 12.3.1
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '12.3'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 12.3.1
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: rspec
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 3.8.0
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.8'
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.8.0
|
90
|
+
- - "~>"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '3.8'
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: rspec_junit_formatter
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0.4'
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 0.4.1
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.4'
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 0.4.1
|
113
|
+
description: When environment variables are not enough and you need to store secrets
|
114
|
+
within files, cici is your friend. Store secret files in your source code repository
|
115
|
+
with ease. Can be used without a CI server, but tool is primarily designed for your
|
116
|
+
CI server to decrypt these secret files for deployment.
|
117
|
+
email: levi.bostian@gmail.com
|
118
|
+
executables:
|
119
|
+
- cici
|
120
|
+
extensions: []
|
121
|
+
extra_rdoc_files: []
|
122
|
+
files:
|
123
|
+
- CHANGELOG.md
|
124
|
+
- LICENSE
|
125
|
+
- README.md
|
126
|
+
- bin/cici
|
127
|
+
- lib/cici.rb
|
128
|
+
- lib/cici/cli.rb
|
129
|
+
- lib/cici/config.rb
|
130
|
+
- lib/cici/constants.rb
|
131
|
+
- lib/cici/decrypt.rb
|
132
|
+
- lib/cici/encrypt.rb
|
133
|
+
- lib/cici/ui.rb
|
134
|
+
- lib/cici/util.rb
|
135
|
+
- lib/cici/version.rb
|
136
|
+
homepage: https://github.com/levibostian/cici
|
137
|
+
licenses:
|
138
|
+
- MIT
|
139
|
+
metadata: {}
|
140
|
+
post_install_message:
|
141
|
+
rdoc_options: []
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
requirements: []
|
155
|
+
rubygems_version: 3.0.6
|
156
|
+
signing_key:
|
157
|
+
specification_version: 4
|
158
|
+
summary: Confidential Information for Continuous Integration (CICI)
|
159
|
+
test_files: []
|