attr_keyring 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/dependabot.yml +15 -0
- data/.github/workflows/tests.yml +65 -0
- data/.rubocop.yml +13 -64
- data/Gemfile +2 -0
- data/README.md +126 -51
- data/Rakefile +3 -1
- data/attr_keyring.gemspec +9 -4
- data/bin/console +1 -0
- data/examples/active_record_sample.rb +2 -0
- data/examples/keyring_sample.rb +2 -0
- data/examples/sequel_sample.rb +2 -0
- data/gemfiles/6_0.gemfile +5 -0
- data/gemfiles/7_0.gemfile +5 -0
- data/lib/attr_keyring/active_record.rb +2 -0
- data/lib/attr_keyring/sequel.rb +2 -0
- data/lib/attr_keyring/version.rb +3 -1
- data/lib/attr_keyring.rb +33 -13
- data/lib/keyring/encryptor/aes.rb +5 -1
- data/lib/keyring/key.rb +7 -3
- data/lib/keyring.rb +26 -6
- metadata +26 -8
- data/.travis.yml +0 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d02280da9cb28259980ea283b6030672957657ead8b895594176eef09d78382b
|
|
4
|
+
data.tar.gz: 88ddda0bb1d9a85869246e46ab49a8f4889bc0c398d616be9dd162eec239a177
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a3108027fac7dbfee097e7afa3227f58f33560e540f19bc0f0b63cb4e482e8315ffafdbd4360bc31724426c9150109c6b9e46e9ecdf70526ca13cb34a8bee93
|
|
7
|
+
data.tar.gz: 16a5ca41d03b434dbf2eb4a5751af6595e7d00c93867159c3990a3e1d0a104d170dfd434580eff0420b1b966afe0877ec70362c6d66cad34e89e0f5fed8867d4
|
data/.github/FUNDING.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Documentation:
|
|
3
|
+
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
4
|
+
|
|
5
|
+
version: 2
|
|
6
|
+
updates:
|
|
7
|
+
- package-ecosystem: "github-actions"
|
|
8
|
+
directory: "/"
|
|
9
|
+
schedule:
|
|
10
|
+
interval: "daily"
|
|
11
|
+
|
|
12
|
+
- package-ecosystem: bundler
|
|
13
|
+
directory: "/"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: "daily"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Tests
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
push:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
name: Tests with Ruby ${{ matrix.ruby }} with ${{ matrix.gemfile }}
|
|
11
|
+
runs-on: "ubuntu-latest"
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
ruby: ["2.7", "3.0"]
|
|
16
|
+
gemfile:
|
|
17
|
+
- gemfiles/7_0.gemfile
|
|
18
|
+
- gemfiles/6_0.gemfile
|
|
19
|
+
|
|
20
|
+
services:
|
|
21
|
+
postgres:
|
|
22
|
+
image: postgres:11.5
|
|
23
|
+
ports: ["5432:5432"]
|
|
24
|
+
options:
|
|
25
|
+
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
|
26
|
+
--health-retries 5
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v1
|
|
30
|
+
|
|
31
|
+
- uses: actions/cache@v2
|
|
32
|
+
with:
|
|
33
|
+
path: vendor/bundle
|
|
34
|
+
key: >
|
|
35
|
+
${{ runner.os }}-${{ matrix.ruby }}-gems-${{
|
|
36
|
+
hashFiles('**/attr_keyring.gemspec') }}
|
|
37
|
+
restore-keys: >
|
|
38
|
+
${{ runner.os }}-${{ matrix.ruby }}-gems-${{
|
|
39
|
+
hashFiles('**/attr_keyring.gemspec') }}
|
|
40
|
+
|
|
41
|
+
- name: Set up Ruby
|
|
42
|
+
uses: ruby/setup-ruby@v1
|
|
43
|
+
with:
|
|
44
|
+
ruby-version: ${{ matrix.ruby }}
|
|
45
|
+
|
|
46
|
+
- name: Install PostgreSQL 11 client
|
|
47
|
+
run: |
|
|
48
|
+
sudo apt-get -yqq install libpq-dev
|
|
49
|
+
|
|
50
|
+
- name: Install gem dependencies
|
|
51
|
+
env:
|
|
52
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
|
53
|
+
run: |
|
|
54
|
+
gem install bundler
|
|
55
|
+
bundle config path vendor/bundle
|
|
56
|
+
bundle update --jobs 4 --retry 3
|
|
57
|
+
|
|
58
|
+
- name: Run Tests
|
|
59
|
+
env:
|
|
60
|
+
PGHOST: localhost
|
|
61
|
+
PGUSER: postgres
|
|
62
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
|
63
|
+
run: |
|
|
64
|
+
psql -U postgres -c "create database test"
|
|
65
|
+
bundle exec rake
|
data/.rubocop.yml
CHANGED
|
@@ -1,73 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
inherit_gem:
|
|
3
|
+
rubocop-fnando: .rubocop.yml
|
|
4
|
+
|
|
1
5
|
AllCops:
|
|
2
6
|
TargetRubyVersion: 2.5
|
|
3
|
-
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
- '**/*.jbuilder'
|
|
7
|
-
- '**/*.rake'
|
|
8
|
-
- '**/*.opal'
|
|
9
|
-
- '**/config.ru'
|
|
10
|
-
- '**/Gemfile'
|
|
11
|
-
- '**/Rakefile'
|
|
12
|
-
- '**/Capfile'
|
|
13
|
-
- '**/Guardfile'
|
|
14
|
-
- '**/Podfile'
|
|
15
|
-
- '**/Thorfile'
|
|
16
|
-
- '**/Vagrantfile'
|
|
17
|
-
- '**/Berksfile'
|
|
18
|
-
- '**/Cheffile'
|
|
19
|
-
- '**/Vagabondfile'
|
|
20
|
-
|
|
21
|
-
Style/Documentation:
|
|
22
|
-
Enabled: false
|
|
23
|
-
|
|
24
|
-
Style/StringLiterals:
|
|
25
|
-
EnforcedStyle: double_quotes
|
|
26
|
-
|
|
27
|
-
Layout/SpaceInsideBlockBraces:
|
|
28
|
-
EnforcedStyle: space
|
|
29
|
-
EnforcedStyleForEmptyBraces: space
|
|
30
|
-
SpaceBeforeBlockParameters: false
|
|
31
|
-
|
|
32
|
-
Layout/SpaceInsideHashLiteralBraces:
|
|
33
|
-
EnforcedStyle: no_space
|
|
34
|
-
EnforcedStyleForEmptyBraces: no_space
|
|
7
|
+
Exclude:
|
|
8
|
+
- vendor/**/*
|
|
9
|
+
- gemfiles/**/*
|
|
35
10
|
|
|
36
|
-
|
|
37
|
-
Enabled: true
|
|
38
|
-
|
|
39
|
-
Layout/FirstHashElementLineBreak:
|
|
40
|
-
Enabled: true
|
|
41
|
-
|
|
42
|
-
Style/SymbolArray:
|
|
43
|
-
Enabled: true
|
|
44
|
-
|
|
45
|
-
Style/PercentLiteralDelimiters:
|
|
46
|
-
PreferredDelimiters:
|
|
47
|
-
'%': '[]'
|
|
48
|
-
'%i': '[]'
|
|
49
|
-
'%q': '[]'
|
|
50
|
-
'%Q': '[]'
|
|
51
|
-
'%r': '{}'
|
|
52
|
-
'%s': '[]'
|
|
53
|
-
'%w': '[]'
|
|
54
|
-
'%W': '[]'
|
|
55
|
-
'%x': '[]'
|
|
56
|
-
|
|
57
|
-
Metrics/LineLength:
|
|
11
|
+
Metrics/AbcSize:
|
|
58
12
|
Enabled: false
|
|
59
13
|
|
|
60
|
-
|
|
61
|
-
|
|
14
|
+
Layout/LineLength:
|
|
15
|
+
Exclude:
|
|
16
|
+
- test/**/*
|
|
62
17
|
|
|
63
|
-
|
|
18
|
+
Metrics/MethodLength:
|
|
64
19
|
Enabled: false
|
|
65
20
|
|
|
66
|
-
|
|
67
|
-
EnforcedStyle: inline
|
|
68
|
-
|
|
69
|
-
Style/Alias:
|
|
70
|
-
EnforcedStyle: prefer_alias_method
|
|
71
|
-
|
|
72
|
-
Style/TrailingUnderscoreVariable:
|
|
21
|
+
Metrics/ClassLength:
|
|
73
22
|
Enabled: false
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-

|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="https://
|
|
4
|
+
<a href="https://github.com/fnando/attr_keyring/actions?query=workflow%3ATests"><img src="https://github.com/fnando/attr_keyring/workflows/Tests/badge.svg" alt="Tests"></a>
|
|
5
5
|
<a href="https://codeclimate.com/github/fnando/attr_keyring"><img src="https://codeclimate.com/github/fnando/attr_keyring/badges/gpa.svg" alt="Code Climate"></a>
|
|
6
|
-
<a href="https://codeclimate.com/github/fnando/attr_keyring/coverage"><img src="https://codeclimate.com/github/fnando/attr_keyring/badges/coverage.svg" alt="Test Coverage"></a>
|
|
7
6
|
<a href="https://rubygems.org/gems/attr_keyring"><img src="https://img.shields.io/gem/v/attr_keyring.svg" alt="Gem"></a>
|
|
8
7
|
<a href="https://rubygems.org/gems/attr_keyring"><img src="https://img.shields.io/gem/dt/attr_keyring.svg" alt="Gem"></a>
|
|
9
8
|
</p>
|
|
10
9
|
|
|
11
|
-
N.B.: attr_keyring is
|
|
10
|
+
N.B.: attr_keyring is not for encrypting passwords--for that, you should use
|
|
11
|
+
something like [bcrypt](https://github.com/codahale/bcrypt-ruby). It's meant for
|
|
12
|
+
encrypting sensitive data you will need to access in plain text (e.g. storing
|
|
13
|
+
OAuth token from users). Passwords do not fall in that category.
|
|
12
14
|
|
|
13
|
-
This library is heavily inspired by
|
|
15
|
+
This library is heavily inspired by
|
|
16
|
+
[attr_vault](https://github.com/uhoh-itsmaciek/attr_vault), and can read
|
|
17
|
+
encrypted messages if you encode them in base64 (e.g.
|
|
18
|
+
`Base64.strict_encode64(encrypted_by_attr_vault)`).
|
|
14
19
|
|
|
15
20
|
## Installation
|
|
16
21
|
|
|
@@ -36,7 +41,10 @@ Or install it yourself as:
|
|
|
36
41
|
gem "attr_keyring"
|
|
37
42
|
require "keyring"
|
|
38
43
|
|
|
39
|
-
keyring = Keyring.new(
|
|
44
|
+
keyring = Keyring.new(
|
|
45
|
+
{"1" => "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M="},
|
|
46
|
+
digest_salt: "<custom salt>"
|
|
47
|
+
)
|
|
40
48
|
|
|
41
49
|
# STEP 1: Encrypt message using latest encryption key.
|
|
42
50
|
encrypted, keyring_id, digest = keyring.encrypt("super secret")
|
|
@@ -52,30 +60,37 @@ puts "✉️ #{decrypted}"
|
|
|
52
60
|
|
|
53
61
|
#### Change encryption algorithm
|
|
54
62
|
|
|
55
|
-
You can choose between `AES-128-CBC`, `AES-192-CBC` and `AES-256-CBC`. By
|
|
56
|
-
|
|
57
|
-
To specify the encryption algorithm, set the `encryption` option. The following example uses `AES-256-CBC`.
|
|
63
|
+
You can choose between `AES-128-CBC`, `AES-192-CBC` and `AES-256-CBC`. By
|
|
64
|
+
default, `AES-128-CBC` will be used.
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
To specify the encryption algorithm, set the `encryption` option. The following
|
|
67
|
+
example uses `AES-256-CBC`.
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
```ruby
|
|
70
|
+
keyring = Keyring.new(
|
|
71
|
+
"1" => "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=",
|
|
72
|
+
encryptor: Keyring::Encryptor::AES::AES256CBC,
|
|
73
|
+
digest_salt: "<custom salt>"
|
|
74
|
+
)
|
|
64
75
|
```
|
|
65
76
|
|
|
66
77
|
### Configuration
|
|
67
78
|
|
|
68
79
|
As far as database schema goes:
|
|
69
80
|
|
|
70
|
-
1. You'll need a column to track the key that was used for encryption; by
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
1. You'll need a column to track the key that was used for encryption; by
|
|
82
|
+
default it's called `keyring_id`.
|
|
83
|
+
2. Every encrypted column must follow the name `encrypted_<column name>`.
|
|
84
|
+
3. Optionally, you can also have a `<column name>_digest` to help with searching
|
|
85
|
+
(see Lookup section below).
|
|
73
86
|
|
|
74
|
-
As far as model configuration goes, they're pretty similar, as you can see
|
|
87
|
+
As far as model configuration goes, they're pretty similar, as you can see
|
|
88
|
+
below:
|
|
75
89
|
|
|
76
90
|
#### ActiveRecord
|
|
77
91
|
|
|
78
|
-
From Rails 5+, ActiveRecord models now inherit from `ApplicationRecord` instead.
|
|
92
|
+
From Rails 5+, ActiveRecord models now inherit from `ApplicationRecord` instead.
|
|
93
|
+
This is how you set it up:
|
|
79
94
|
|
|
80
95
|
```ruby
|
|
81
96
|
class ApplicationRecord < ActiveRecord::Base
|
|
@@ -86,7 +101,8 @@ end
|
|
|
86
101
|
|
|
87
102
|
#### Sequel
|
|
88
103
|
|
|
89
|
-
Sequel doesn't have an abstract model class (but it could), so you can set up
|
|
104
|
+
Sequel doesn't have an abstract model class (but it could), so you can set up
|
|
105
|
+
the model class directly like the following:
|
|
90
106
|
|
|
91
107
|
```ruby
|
|
92
108
|
class User < Sequel::Model
|
|
@@ -96,16 +112,20 @@ end
|
|
|
96
112
|
|
|
97
113
|
### Defining encrypted attributes
|
|
98
114
|
|
|
99
|
-
To set up your model, you have to define the keyring (set of encryption keys)
|
|
115
|
+
To set up your model, you have to define the keyring (set of encryption keys)
|
|
116
|
+
and the attributes that will be encrypted. Both ActiveRecord and Sequel have the
|
|
117
|
+
same API, so the examples below work for both ORMs.
|
|
100
118
|
|
|
101
119
|
```ruby
|
|
102
120
|
class User < ApplicationRecord
|
|
103
|
-
attr_keyring ENV["USER_KEYRING"]
|
|
121
|
+
attr_keyring ENV["USER_KEYRING"],
|
|
122
|
+
digest_salt: "<custom salt>"
|
|
104
123
|
attr_encrypt :twitter_oauth_token, :social_security_number
|
|
105
124
|
end
|
|
106
125
|
```
|
|
107
126
|
|
|
108
|
-
The code above will encrypt your columns with the current key. If you're
|
|
127
|
+
The code above will encrypt your columns with the current key. If you're
|
|
128
|
+
updating a record, then the column will be migrated to the latest key available.
|
|
109
129
|
|
|
110
130
|
You can use the model as you would normally do.
|
|
111
131
|
|
|
@@ -126,20 +146,27 @@ user.encrypted_email
|
|
|
126
146
|
|
|
127
147
|
### Encryption
|
|
128
148
|
|
|
129
|
-
By default, AES-128-CBC is the algorithm used for encryption. This algorithm
|
|
149
|
+
By default, AES-128-CBC is the algorithm used for encryption. This algorithm
|
|
150
|
+
uses 16 bytes keys, but you're required to use a key that's double the size
|
|
151
|
+
because half of that keys will be used to generate the HMAC. The first 16 bytes
|
|
152
|
+
will be used as the encryption key, and the last 16 bytes will be used to
|
|
153
|
+
generate the HMAC.
|
|
130
154
|
|
|
131
|
-
Using random data base64-encoded is the recommended way. You can easily generate
|
|
155
|
+
Using random data base64-encoded is the recommended way. You can easily generate
|
|
156
|
+
keys by using the following command:
|
|
132
157
|
|
|
133
158
|
```console
|
|
134
159
|
$ dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64 -A
|
|
135
160
|
qUjOJFgZsZbTICsN0TMkKqUvSgObYxnkHDsazTqE5tM=
|
|
136
161
|
```
|
|
137
162
|
|
|
138
|
-
Include the result of this command in the `value` section of the key description
|
|
163
|
+
Include the result of this command in the `value` section of the key description
|
|
164
|
+
in the keyring. Half this key is used for encryption, and half for the HMAC.
|
|
139
165
|
|
|
140
166
|
#### Key size
|
|
141
167
|
|
|
142
|
-
The key size depends on the algorithm being used. The key size should be double
|
|
168
|
+
The key size depends on the algorithm being used. The key size should be double
|
|
169
|
+
the size as half of it is used for HMAC computation.
|
|
143
170
|
|
|
144
171
|
- `aes-128-cbc`: 16 bytes (encryption) + 16 bytes (HMAC).
|
|
145
172
|
- `aes-192-cbc`: 24 bytes (encryption) + 24 bytes (HMAC).
|
|
@@ -147,13 +174,25 @@ The key size depends on the algorithm being used. The key size should be double
|
|
|
147
174
|
|
|
148
175
|
#### About the encrypted message
|
|
149
176
|
|
|
150
|
-
Initialization vectors (IV) should be unpredictable and unique; ideally, they
|
|
177
|
+
Initialization vectors (IV) should be unpredictable and unique; ideally, they
|
|
178
|
+
will be cryptographically random. They do not have to be secret: IVs are
|
|
179
|
+
typically just added to ciphertext messages unencrypted. It may sound
|
|
180
|
+
contradictory that something has to be unpredictable and unique, but does not
|
|
181
|
+
have to be secret; it is important to remember that an attacker must not be able
|
|
182
|
+
to predict ahead of time what a given IV will be.
|
|
151
183
|
|
|
152
|
-
With that in mind, _attr_keyring_ uses
|
|
184
|
+
With that in mind, _attr_keyring_ uses
|
|
185
|
+
`base64(hmac(unencrypted iv + encrypted message) + unencrypted iv + encrypted message)`
|
|
186
|
+
as the final message. If you're planning to migrate from other encryption
|
|
187
|
+
mechanisms or read encrypted values from the database without using
|
|
188
|
+
_attr_keyring_, make sure you account for this. The HMAC is 32-bytes long and
|
|
189
|
+
the IV is 16-bytes long.
|
|
153
190
|
|
|
154
191
|
### Keyring
|
|
155
192
|
|
|
156
|
-
Keys are managed through a keyring--a short JSON document describing your
|
|
193
|
+
Keys are managed through a keyring--a short JSON document describing your
|
|
194
|
+
encryption keys. The keyring must be a JSON object mapping numeric ids of the
|
|
195
|
+
keys to the key values. A keyring must have at least one key. For example:
|
|
157
196
|
|
|
158
197
|
```json
|
|
159
198
|
{
|
|
@@ -162,24 +201,30 @@ Keys are managed through a keyring--a short JSON document describing your encryp
|
|
|
162
201
|
}
|
|
163
202
|
```
|
|
164
203
|
|
|
165
|
-
The `id` is used to track which key encrypted which piece of data; a key with a
|
|
204
|
+
The `id` is used to track which key encrypted which piece of data; a key with a
|
|
205
|
+
larger id is assumed to be newer. The value is the actual bytes of the
|
|
206
|
+
encryption key.
|
|
166
207
|
|
|
167
208
|
#### Dynamically loading keyring
|
|
168
209
|
|
|
169
|
-
If you're using Rails 5.2+, you can use credentials to define your keyring. Your
|
|
210
|
+
If you're using Rails 5.2+, you can use credentials to define your keyring. Your
|
|
211
|
+
`credentials.yml` must be define like the following:
|
|
170
212
|
|
|
171
213
|
```yaml
|
|
214
|
+
---
|
|
172
215
|
user_keyring:
|
|
173
|
-
1: "QSXyoiRDPoJmfkJUZ4hJeQ=="
|
|
174
|
-
2: "r6AfOeilPDJomFsiOXLdfQ=="
|
|
216
|
+
"1": "QSXyoiRDPoJmfkJUZ4hJeQ=="
|
|
217
|
+
"2": "r6AfOeilPDJomFsiOXLdfQ=="
|
|
175
218
|
```
|
|
176
219
|
|
|
177
|
-
Then you can setup your model by using
|
|
220
|
+
Then you can setup your model by using
|
|
221
|
+
`attr_keyring Rails.application.credentials.user_keyring`.
|
|
178
222
|
|
|
179
|
-
Other possibilities (e.g. the keyring file is provided by configuration
|
|
223
|
+
Other possibilities (e.g. the keyring file is provided by configuration
|
|
224
|
+
management):
|
|
180
225
|
|
|
181
|
-
- `attr_keyring YAML.load_file(keyring_file)`
|
|
182
|
-
- `attr_keyring JSON.parse(File.read(keyring_file))`.
|
|
226
|
+
- `attr_keyring YAML.load_file(keyring_file), digest_salt: "<custom salt>"`
|
|
227
|
+
- `attr_keyring JSON.parse(File.read(keyring_file)), digest_salt: "<custom salt>"`.
|
|
183
228
|
|
|
184
229
|
### Lookup
|
|
185
230
|
|
|
@@ -189,17 +234,25 @@ One tricky aspect of encryption is looking up records by known secret. E.g.,
|
|
|
189
234
|
User.where(email: "john@example.com")
|
|
190
235
|
```
|
|
191
236
|
|
|
192
|
-
is trivial with plain text fields, but impossible with the model defined as
|
|
237
|
+
is trivial with plain text fields, but impossible with the model defined as
|
|
238
|
+
above.
|
|
193
239
|
|
|
194
|
-
If a column `<attribute>_digest` exists, then a SHA1 digest from the value will
|
|
240
|
+
If a column `<attribute>_digest` exists, then a SHA1 digest from the value will
|
|
241
|
+
be saved. This will allow you to lookup by that value instead and add unique
|
|
242
|
+
indexes. You don't have to use a hashing salt, but it's highly recommended; this
|
|
243
|
+
way you can avoid leaking your users' info via rainbow tables.
|
|
195
244
|
|
|
196
245
|
```ruby
|
|
197
|
-
User.where(email:
|
|
246
|
+
User.where(email: User.keyring.digest("john@example.com")).first
|
|
198
247
|
```
|
|
199
248
|
|
|
200
249
|
### Key Rotation
|
|
201
250
|
|
|
202
|
-
Because attr_keyring uses a keyring, with access to multiple keys at once, key
|
|
251
|
+
Because attr_keyring uses a keyring, with access to multiple keys at once, key
|
|
252
|
+
rotation is fairly straightforward: if you add a key to the keyring with a
|
|
253
|
+
higher id than any other key, that key will automatically be used for encryption
|
|
254
|
+
when records are either created or updated. Any keys that are no longer in use
|
|
255
|
+
can be safely removed from the keyring.
|
|
203
256
|
|
|
204
257
|
To check if an existing key with id `123` is still in use, run:
|
|
205
258
|
|
|
@@ -208,7 +261,8 @@ To check if an existing key with id `123` is still in use, run:
|
|
|
208
261
|
User.where(keyring_id: 123).empty?
|
|
209
262
|
```
|
|
210
263
|
|
|
211
|
-
You may not want to wait for records to be updated (e.g. key leaking). In that
|
|
264
|
+
You may not want to wait for records to be updated (e.g. key leaking). In that
|
|
265
|
+
case, you can rollout a key rotation:
|
|
212
266
|
|
|
213
267
|
```ruby
|
|
214
268
|
User.where(keyring_id: 1234).find_each do |user|
|
|
@@ -218,12 +272,18 @@ end
|
|
|
218
272
|
|
|
219
273
|
### What if I don't use ActiveRecord/Sequel?
|
|
220
274
|
|
|
221
|
-
You can also leverage the encryption mechanism of `attr_keyring` totally
|
|
275
|
+
You can also leverage the encryption mechanism of `attr_keyring` totally
|
|
276
|
+
decoupled from ActiveRecord/Sequel. First, make sure you load `keyring` instead.
|
|
277
|
+
Then you can create a keyring to encrypt/decrypt strings, without even touching
|
|
278
|
+
the database.
|
|
222
279
|
|
|
223
280
|
```ruby
|
|
224
281
|
require "keyring"
|
|
225
282
|
|
|
226
|
-
keyring = Keyring.new(
|
|
283
|
+
keyring = Keyring.new(
|
|
284
|
+
{"1" => "QSXyoiRDPoJmfkJUZ4hJeQ=="},
|
|
285
|
+
digest_salt: "<custom salt>"
|
|
286
|
+
)
|
|
227
287
|
|
|
228
288
|
encrypted, keyring_id, digest = keyring.encrypt("super secret")
|
|
229
289
|
|
|
@@ -244,26 +304,41 @@ puts decrypted
|
|
|
244
304
|
|
|
245
305
|
### Exchange data with Node.js
|
|
246
306
|
|
|
247
|
-
If you use Node.js, you may be interested in
|
|
307
|
+
If you use Node.js, you may be interested in
|
|
308
|
+
<https://github.com/fnando/keyring-node>, which is able to read and write
|
|
309
|
+
messages using the same format.
|
|
248
310
|
|
|
249
311
|
## Development
|
|
250
312
|
|
|
251
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
313
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
314
|
+
`rake test` to run the tests. You can also run `bin/console` for an interactive
|
|
315
|
+
prompt that will allow you to experiment.
|
|
252
316
|
|
|
253
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
317
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
318
|
+
release a new version, update the version number in `version.rb`, and then run
|
|
319
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
|
320
|
+
git commits and tags, and push the `.gem` file to
|
|
321
|
+
[rubygems.org](https://rubygems.org).
|
|
254
322
|
|
|
255
323
|
## Contributing
|
|
256
324
|
|
|
257
|
-
Bug reports and pull requests are welcome on GitHub at
|
|
325
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
326
|
+
https://github.com/fnando/attr_keyring. This project is intended to be a safe,
|
|
327
|
+
welcoming space for collaboration, and contributors are expected to adhere to
|
|
328
|
+
the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
258
329
|
|
|
259
330
|
## License
|
|
260
331
|
|
|
261
|
-
The gem is available as open source under the terms of the
|
|
332
|
+
The gem is available as open source under the terms of the
|
|
333
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
|
262
334
|
|
|
263
335
|
## Icon
|
|
264
336
|
|
|
265
|
-
Icon made by [Icongeek26](https://www.flaticon.com/authors/icongeek26) from
|
|
337
|
+
Icon made by [Icongeek26](https://www.flaticon.com/authors/icongeek26) from
|
|
338
|
+
[Flaticon](https://www.flaticon.com/) is licensed by Creative Commons BY 3.0.
|
|
266
339
|
|
|
267
340
|
## Code of Conduct
|
|
268
341
|
|
|
269
|
-
Everyone interacting in the attr_keyring project’s codebases, issue trackers,
|
|
342
|
+
Everyone interacting in the attr_keyring project’s codebases, issue trackers,
|
|
343
|
+
chat rooms and mailing lists is expected to follow the
|
|
344
|
+
[code of conduct](https://github.com/fnando/attr_keyring/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "bundler/gem_tasks"
|
|
2
4
|
require "rake/testtask"
|
|
3
5
|
|
|
@@ -14,4 +16,4 @@ task(:rubocop) do
|
|
|
14
16
|
RuboCop::CLI.new.run(["--config", File.join(__dir__, ".rubocop.yml")])
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
task :
|
|
19
|
+
task default: %i[test rubocop]
|
data/attr_keyring.gemspec
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "./lib/attr_keyring/version"
|
|
2
4
|
|
|
3
5
|
Gem::Specification.new do |spec|
|
|
@@ -10,12 +12,14 @@ Gem::Specification.new do |spec|
|
|
|
10
12
|
spec.description = spec.summary
|
|
11
13
|
spec.homepage = "https://github.com/fnando/attr_keyring"
|
|
12
14
|
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
18
|
+
`git ls-files -z`
|
|
19
|
+
.split("\x0")
|
|
20
|
+
.reject {|f| f.match(%r{^(test|spec|features)/}) }
|
|
18
21
|
end
|
|
22
|
+
|
|
19
23
|
spec.bindir = "exe"
|
|
20
24
|
spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
|
|
21
25
|
spec.require_paths = ["lib"]
|
|
@@ -29,6 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
29
33
|
spec.add_development_dependency "pry-meta"
|
|
30
34
|
spec.add_development_dependency "rake"
|
|
31
35
|
spec.add_development_dependency "rubocop"
|
|
36
|
+
spec.add_development_dependency "rubocop-fnando"
|
|
32
37
|
spec.add_development_dependency "sequel"
|
|
33
38
|
spec.add_development_dependency "simplecov"
|
|
34
39
|
end
|
data/bin/console
CHANGED
data/examples/keyring_sample.rb
CHANGED
data/examples/sequel_sample.rb
CHANGED
data/lib/attr_keyring/sequel.rb
CHANGED
data/lib/attr_keyring/version.rb
CHANGED
data/lib/attr_keyring.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module AttrKeyring
|
|
2
4
|
require "attr_keyring/version"
|
|
3
5
|
require "keyring"
|
|
@@ -18,13 +20,11 @@ module AttrKeyring
|
|
|
18
20
|
include InstanceMethods
|
|
19
21
|
|
|
20
22
|
class << self
|
|
21
|
-
attr_accessor :encrypted_attributes
|
|
22
|
-
attr_accessor :keyring
|
|
23
|
-
attr_accessor :keyring_column_name
|
|
23
|
+
attr_accessor :encrypted_attributes, :keyring, :keyring_column_name
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
self.encrypted_attributes = []
|
|
27
|
-
self.keyring = Keyring.new({})
|
|
27
|
+
self.keyring = Keyring.new({}, digest_salt: "")
|
|
28
28
|
self.keyring_column_name = :keyring_id
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -38,8 +38,8 @@ module AttrKeyring
|
|
|
38
38
|
subclass.keyring_column_name = keyring_column_name
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def attr_keyring(keyring,
|
|
42
|
-
self.keyring = Keyring.new(keyring,
|
|
41
|
+
def attr_keyring(keyring, options = {})
|
|
42
|
+
self.keyring = Keyring.new(keyring, options)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def attr_encrypt(*attributes)
|
|
@@ -73,37 +73,54 @@ module AttrKeyring
|
|
|
73
73
|
value = value.to_s
|
|
74
74
|
|
|
75
75
|
previous_keyring_id = public_send(self.class.keyring_column_name)
|
|
76
|
-
encrypted_value, keyring_id, digest =
|
|
76
|
+
encrypted_value, keyring_id, digest =
|
|
77
|
+
self.class.keyring.encrypt(value, previous_keyring_id)
|
|
77
78
|
|
|
78
79
|
public_send("#{self.class.keyring_column_name}=", keyring_id)
|
|
79
80
|
public_send("encrypted_#{attribute}=", encrypted_value)
|
|
80
|
-
|
|
81
|
+
|
|
82
|
+
return unless respond_to?("#{attribute}_digest=")
|
|
83
|
+
|
|
84
|
+
public_send("#{attribute}_digest=", digest)
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
private def attr_decrypt_column(attribute)
|
|
84
88
|
cache_name = :"@#{attribute}"
|
|
85
|
-
|
|
89
|
+
if instance_variable_defined?(cache_name)
|
|
90
|
+
return instance_variable_get(cache_name)
|
|
91
|
+
end
|
|
86
92
|
|
|
87
93
|
encrypted_value = public_send("encrypted_#{attribute}")
|
|
94
|
+
|
|
88
95
|
return unless encrypted_value
|
|
89
96
|
|
|
90
|
-
decrypted_value = self.class.keyring.decrypt(
|
|
97
|
+
decrypted_value = self.class.keyring.decrypt(
|
|
98
|
+
encrypted_value,
|
|
99
|
+
public_send(self.class.keyring_column_name)
|
|
100
|
+
)
|
|
91
101
|
|
|
92
102
|
instance_variable_set(cache_name, decrypted_value)
|
|
93
103
|
end
|
|
94
104
|
|
|
95
105
|
private def clear_decrypted_column_cache(attribute)
|
|
96
106
|
cache_name = :"@#{attribute}"
|
|
97
|
-
|
|
107
|
+
|
|
108
|
+
return unless instance_variable_defined?(cache_name)
|
|
109
|
+
|
|
110
|
+
remove_instance_variable(cache_name)
|
|
98
111
|
end
|
|
99
112
|
|
|
100
113
|
private def reset_encrypted_column(attribute)
|
|
101
114
|
public_send("encrypted_#{attribute}=", nil)
|
|
102
|
-
|
|
115
|
+
if respond_to?("#{attribute}_digest=")
|
|
116
|
+
public_send("#{attribute}_digest=", nil)
|
|
117
|
+
end
|
|
103
118
|
nil
|
|
104
119
|
end
|
|
105
120
|
|
|
106
121
|
private def migrate_to_latest_encryption_key
|
|
122
|
+
return unless self.class.keyring.current_key
|
|
123
|
+
|
|
107
124
|
keyring_id = self.class.keyring.current_key.id
|
|
108
125
|
|
|
109
126
|
self.class.encrypted_attributes.each do |attribute|
|
|
@@ -113,7 +130,10 @@ module AttrKeyring
|
|
|
113
130
|
encrypted_value, _, digest = self.class.keyring.encrypt(value)
|
|
114
131
|
|
|
115
132
|
public_send("encrypted_#{attribute}=", encrypted_value)
|
|
116
|
-
|
|
133
|
+
|
|
134
|
+
if respond_to?("#{attribute}_digest")
|
|
135
|
+
public_send("#{attribute}_digest=", digest)
|
|
136
|
+
end
|
|
117
137
|
end
|
|
118
138
|
|
|
119
139
|
public_send("#{self.class.keyring_column_name}=", keyring_id)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Keyring
|
|
2
4
|
module Encryptor
|
|
3
5
|
module AES
|
|
@@ -35,7 +37,9 @@ module Keyring
|
|
|
35
37
|
|
|
36
38
|
expected_hmac = hmac_digest(key.signing_key, encrypted_payload)
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
unless verify_signature(expected_hmac, hmac)
|
|
41
|
+
raise InvalidAuthentication, "Expected HMAC to be #{Base64.strict_encode64(expected_hmac)}; got #{Base64.strict_encode64(hmac)} instead" # rubocop:disable Layout/LineLength
|
|
42
|
+
end
|
|
39
43
|
|
|
40
44
|
cipher.iv = iv
|
|
41
45
|
cipher.key = key.encryption_key
|
data/lib/keyring/key.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Keyring
|
|
2
4
|
class Key
|
|
3
5
|
attr_reader :id, :signing_key, :encryption_key
|
|
4
6
|
|
|
5
7
|
def initialize(id, key, key_size)
|
|
6
|
-
@id = Integer(id)
|
|
8
|
+
@id = Integer(id.to_s)
|
|
7
9
|
@key_size = key_size
|
|
8
10
|
@encryption_key, @signing_key = parse_key(key)
|
|
9
11
|
end
|
|
@@ -11,13 +13,15 @@ module Keyring
|
|
|
11
13
|
def to_s
|
|
12
14
|
"#<Keyring::Key id=#{id.inspect}>"
|
|
13
15
|
end
|
|
14
|
-
|
|
16
|
+
alias inspect to_s
|
|
15
17
|
|
|
16
18
|
private def parse_key(key)
|
|
17
19
|
expected_key_size = @key_size * 2
|
|
18
20
|
secret = decode_key(key, expected_key_size)
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
unless secret.bytesize == expected_key_size
|
|
23
|
+
raise InvalidSecret, "Secret must be #{expected_key_size} bytes, instead got #{secret.bytesize}" # rubocop:disable Layout/LineLength
|
|
24
|
+
end
|
|
21
25
|
|
|
22
26
|
signing_key = secret[0...@key_size]
|
|
23
27
|
encryption_key = secret[@key_size..-1]
|
data/lib/keyring.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Keyring
|
|
2
4
|
require "openssl"
|
|
3
5
|
require "base64"
|
|
@@ -10,10 +12,19 @@ module Keyring
|
|
|
10
12
|
InvalidSecret = Class.new(StandardError)
|
|
11
13
|
EmptyKeyring = Class.new(StandardError)
|
|
12
14
|
InvalidAuthentication = Class.new(StandardError)
|
|
15
|
+
MissingDigestSalt = Class.new(StandardError) do
|
|
16
|
+
def message
|
|
17
|
+
%w[
|
|
18
|
+
Please provide :digest_salt;
|
|
19
|
+
you can disable this error by explicitly passing an empty string.
|
|
20
|
+
].join(" ")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
13
23
|
|
|
14
24
|
class Base
|
|
15
|
-
def initialize(keyring,
|
|
16
|
-
@encryptor = encryptor
|
|
25
|
+
def initialize(keyring, options)
|
|
26
|
+
@encryptor = options[:encryptor]
|
|
27
|
+
@digest_salt = options[:digest_salt]
|
|
17
28
|
@keyring = keyring.map do |id, value|
|
|
18
29
|
Key.new(id, value, @encryptor.key_size)
|
|
19
30
|
end
|
|
@@ -42,13 +53,12 @@ module Keyring
|
|
|
42
53
|
|
|
43
54
|
def encrypt(message, keyring_id = nil)
|
|
44
55
|
keyring_id ||= current_key&.id
|
|
45
|
-
digest = Digest::SHA1.hexdigest(message)
|
|
46
56
|
key = self[keyring_id]
|
|
47
57
|
|
|
48
58
|
[
|
|
49
59
|
@encryptor.encrypt(key, message),
|
|
50
60
|
keyring_id,
|
|
51
|
-
digest
|
|
61
|
+
digest(message)
|
|
52
62
|
]
|
|
53
63
|
end
|
|
54
64
|
|
|
@@ -56,9 +66,19 @@ module Keyring
|
|
|
56
66
|
key = self[keyring_id]
|
|
57
67
|
@encryptor.decrypt(key, message)
|
|
58
68
|
end
|
|
69
|
+
|
|
70
|
+
def digest(message)
|
|
71
|
+
Digest::SHA1.hexdigest("#{message}#{@digest_salt}")
|
|
72
|
+
end
|
|
59
73
|
end
|
|
60
74
|
|
|
61
|
-
def self.new(keyring,
|
|
62
|
-
|
|
75
|
+
def self.new(keyring, options = {})
|
|
76
|
+
options = {
|
|
77
|
+
encryptor: Encryptor::AES::AES128CBC
|
|
78
|
+
}.merge(options)
|
|
79
|
+
|
|
80
|
+
raise MissingDigestSalt if options[:digest_salt].nil?
|
|
81
|
+
|
|
82
|
+
Base.new(keyring, options)
|
|
63
83
|
end
|
|
64
84
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: attr_keyring
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nando Vieira
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-12-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -136,6 +136,20 @@ dependencies:
|
|
|
136
136
|
- - ">="
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
138
|
version: '0'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: rubocop-fnando
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0'
|
|
139
153
|
- !ruby/object:Gem::Dependency
|
|
140
154
|
name: sequel
|
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -171,9 +185,11 @@ executables: []
|
|
|
171
185
|
extensions: []
|
|
172
186
|
extra_rdoc_files: []
|
|
173
187
|
files:
|
|
188
|
+
- ".github/FUNDING.yml"
|
|
189
|
+
- ".github/dependabot.yml"
|
|
190
|
+
- ".github/workflows/tests.yml"
|
|
174
191
|
- ".gitignore"
|
|
175
192
|
- ".rubocop.yml"
|
|
176
|
-
- ".travis.yml"
|
|
177
193
|
- CODE_OF_CONDUCT.md
|
|
178
194
|
- Gemfile
|
|
179
195
|
- LICENSE.txt
|
|
@@ -187,6 +203,8 @@ files:
|
|
|
187
203
|
- examples/active_record_sample.rb
|
|
188
204
|
- examples/keyring_sample.rb
|
|
189
205
|
- examples/sequel_sample.rb
|
|
206
|
+
- gemfiles/6_0.gemfile
|
|
207
|
+
- gemfiles/7_0.gemfile
|
|
190
208
|
- lib/attr_keyring.rb
|
|
191
209
|
- lib/attr_keyring/active_record.rb
|
|
192
210
|
- lib/attr_keyring/sequel.rb
|
|
@@ -198,7 +216,7 @@ homepage: https://github.com/fnando/attr_keyring
|
|
|
198
216
|
licenses:
|
|
199
217
|
- MIT
|
|
200
218
|
metadata: {}
|
|
201
|
-
post_install_message:
|
|
219
|
+
post_install_message:
|
|
202
220
|
rdoc_options: []
|
|
203
221
|
require_paths:
|
|
204
222
|
- lib
|
|
@@ -206,15 +224,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
206
224
|
requirements:
|
|
207
225
|
- - ">="
|
|
208
226
|
- !ruby/object:Gem::Version
|
|
209
|
-
version:
|
|
227
|
+
version: 2.5.0
|
|
210
228
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
211
229
|
requirements:
|
|
212
230
|
- - ">="
|
|
213
231
|
- !ruby/object:Gem::Version
|
|
214
232
|
version: '0'
|
|
215
233
|
requirements: []
|
|
216
|
-
rubygems_version: 3.
|
|
217
|
-
signing_key:
|
|
234
|
+
rubygems_version: 3.2.32
|
|
235
|
+
signing_key:
|
|
218
236
|
specification_version: 4
|
|
219
237
|
summary: Simple encryption-at-rest plugin for ActiveRecord.
|
|
220
238
|
test_files: []
|
data/.travis.yml
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
language: ruby
|
|
2
|
-
cache: bundler
|
|
3
|
-
sudo: false
|
|
4
|
-
notifications:
|
|
5
|
-
email: false
|
|
6
|
-
rvm:
|
|
7
|
-
- 2.5.3
|
|
8
|
-
before_script:
|
|
9
|
-
- createdb test
|
|
10
|
-
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
|
11
|
-
- chmod +x ./cc-test-reporter
|
|
12
|
-
- "./cc-test-reporter before-build"
|
|
13
|
-
after_script:
|
|
14
|
-
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
|
|
15
|
-
env:
|
|
16
|
-
global:
|
|
17
|
-
secure: c0y7opFgX78UQL0dVq2gciMr3Ca4y4Aw4cSbQMnUwGecwuzOPUhjV98yy4b6EpQ0bLVbVcSPtx/PCVV750nxJPQsz9tWS0yGxQPBXuh2w0AX+ErYJVYaF6+hTjovEiHB86Q9g8YCD29CIMLZs2yeUrB+ORJWQcuAn8fw475Zskk8d8BWqR8CDdonFKlwS0Bx6rOqkyVy0JiNbOM4+trV/RzrNC+dc1geqOo45ceTYiGzkkMU1XANjNhzl/v0DYtCWLF/Dj1s8da96btqU6msZDfsBM73zKWtu0KJMnzqa8Ba4Tjc39kd2ro6Zb22cELBdXOFBvNCAEjbmZIaJ2OC45fES1OGZnB66SjAScdVdxKy2jOWjlFvrRiHu3Zrbl5tFTEaJ/PMHueQn4AzneK1wU2kzjq5iCwBZtMp/iJtCvz0V6qBt77qJe65YuENhcj26cDMqQkhKd0QBTWNs8r02KY3HFKcprgM+2TXxVSvfDu2cbiMInvc3K+uFNnEbu/1piTyStKWGd64WHixV6CEFpHxLU04IUNB62mSvUZtZ6V782X9kawoRyUg6lWvXmnGUUvczdJdpSR5/3gVXOWHireYy/qA6Zqoup27PPoaNgnKCa/fWvN/aJDvrGJb9OWpiK8DGi6T35V5gtDF+vd8mVzyPnYJznlWLgA5m7FSzLg=
|