ndr_pseudonymise 0.4.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 +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +44 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +64 -0
- data/Rakefile +14 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/ndr_pseudonymise/client.rb +115 -0
- data/lib/ndr_pseudonymise/demographics_only_pseudonymiser.rb +88 -0
- data/lib/ndr_pseudonymise/engine.rb +6 -0
- data/lib/ndr_pseudonymise/ndr_encrypt/command_line.rb +194 -0
- data/lib/ndr_pseudonymise/ndr_encrypt/encrypted_object.rb +124 -0
- data/lib/ndr_pseudonymise/ndr_encrypt/remote_repository.rb +44 -0
- data/lib/ndr_pseudonymise/ndr_encrypt/repository.rb +165 -0
- data/lib/ndr_pseudonymise/ndr_encrypt.rb +10 -0
- data/lib/ndr_pseudonymise/prescription_pseudonymiser.rb +71 -0
- data/lib/ndr_pseudonymise/progress_printer.rb +53 -0
- data/lib/ndr_pseudonymise/pseudonymisation_specification.rb +379 -0
- data/lib/ndr_pseudonymise/pseudonymised_file_converter.rb +92 -0
- data/lib/ndr_pseudonymise/pseudonymised_file_wrapper.rb +96 -0
- data/lib/ndr_pseudonymise/simple_pseudonymisation.rb +125 -0
- data/lib/ndr_pseudonymise/version.rb +3 -0
- data/lib/ndr_pseudonymise.rb +16 -0
- data/lib/rsa_aes_cbc.rb +114 -0
- data/ndr_pseudonymise.gemspec +36 -0
- data/script/ndr_encrypt/README.md +154 -0
- data/script/ndr_encrypt/ndr_encrypt +4 -0
- metadata +197 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cdbd09c95ec2fa5a5fdb791a177654ae9c9cc520a1461c1df54cc5035c001c66
|
4
|
+
data.tar.gz: 3bb60274d7eed27d033bb7efca0262d48c0e8d322b6708eb0d6ddb37a9673a2a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7180d2bf8a99f0b5493e0204bf9f3dff7100f8663f1560b2b207a05fa235431674b60490093cbd6eeef5e350ad7460f470ad895d71d3506b71d73dd29e52ad3b
|
7
|
+
data.tar.gz: 3261dcbbd12a6770d7d03e76bb84aa6e327de4b0acaec762e0650e5d8ba4425f5780ef051e8ef2a7063d5327ba8e28b1539ad4587520d0484d239377916d1555
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
* No unreleased changes
|
3
|
+
|
4
|
+
## 0.4.1 / 2022-10-18
|
5
|
+
## Added
|
6
|
+
# Add ndr_encrypt utility script for image encryption.
|
7
|
+
|
8
|
+
## 0.4.0 / 2020-07-08
|
9
|
+
## Changed
|
10
|
+
* Update client to reflect API updates
|
11
|
+
|
12
|
+
## 0.3.0 / 2020-07-08
|
13
|
+
## Added
|
14
|
+
* Added pseudonymisation service client
|
15
|
+
|
16
|
+
## 0.2.11 / 2019-03-15
|
17
|
+
## Added
|
18
|
+
* SimplePseudonymisation should include decryption methods.
|
19
|
+
|
20
|
+
# 0.2.10 / 2019-03-06
|
21
|
+
## Fixed
|
22
|
+
* Pseudonymisation: allow blank dates of birth.
|
23
|
+
|
24
|
+
# 0.2.9 / 2018-12-10
|
25
|
+
## Changed
|
26
|
+
* # Include row numbers in CSV pseudonymisation errors.
|
27
|
+
|
28
|
+
# 0.2.8 / 2018-12-07
|
29
|
+
## Fixed
|
30
|
+
* Allow null NHS numbers for DemographicsOnlyPseudonymiser
|
31
|
+
|
32
|
+
# 0.2.7 / Unreleased
|
33
|
+
|
34
|
+
# 0.2.6 / 2018-08-30
|
35
|
+
## Fixed
|
36
|
+
* Remove unnecessary pry runtime dependency
|
37
|
+
|
38
|
+
# 0.2.5 / 2018-07-24
|
39
|
+
## Added
|
40
|
+
* Support pseudonymising data with multiple demographics columns, and various date formats
|
41
|
+
|
42
|
+
# 0.2.4 / 2018-07-24
|
43
|
+
## Added
|
44
|
+
* Add helper methods for reformatting pseudonymised data into ordinary CSV columns.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2011-2016 Public Health England
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
## NdrPseudonymise [](https://github.com/publichealthengland/ndr_pseudonymise/actions?query=workflow%3Atest)
|
2
|
+
|
3
|
+
Pseudonymise confidential data, in CSV format, with specifications for which fields to be encrypted.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'ndr_pseudonymise'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install ndr_pseudonymise
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Example input file:
|
24
|
+
nhsnumber,birthdate,postcode,surname,data1,date2
|
25
|
+
1234567881,1955-01-01,CB22 3AD,SMITH,xyz,abc
|
26
|
+
,01/02/1955,,JONES,zzz,aaa
|
27
|
+
|
28
|
+
outfile.yml:
|
29
|
+
|
30
|
+
File version:
|
31
|
+
'something-1'
|
32
|
+
|
33
|
+
Pseudonymised keys:
|
34
|
+
[sha1('nhsnumber_1234567881' + salt1), 'n-random-uuencoded-bits-1']
|
35
|
+
[sha1('birthdate_postcode_1955-01-01_CB22 3AD' + salt1), 'n-random-uuencoded-bits-2']
|
36
|
+
[sha1('birthdate_postcode_1955-02-01_' + salt1), 'n-random-uuencoded-bits-3']
|
37
|
+
|
38
|
+
Encrypted demographics
|
39
|
+
[sha1('nhsnumber_1234567881' + salt1), encrypt(['surname' => 'SMITH'], 'nhsnumber_1234567881' + 'n-random-uuencoded-bits-1' + salt2, 'n-random-uuencoded-bits2-1']
|
40
|
+
[sha1('birthdate_postcode_1955-01-01_CB22 3AD' + salt1), encrypt(['surname' => 'SMITH'], 'birthdate_postcode_1955-01-01_CB22 3AD' + 'n-random-uuencoded-bits-2' + salt2, 'n-random-uuencoded-bits2-2']
|
41
|
+
[sha1('birthdate_postcode_1955-02-01_' + salt1), encrypt(['surname' => 'JONES'], 'birthdate_postcode_1955-02-01_' + 'n-random-uuencoded-bits-2' + salt2, 'n-random-uuencoded-bits2-3']
|
42
|
+
|
43
|
+
Encrypted data
|
44
|
+
[sha1('nhsnumber_1234567881' + salt1), encrypt(['xyz','abc'], 1', 'nhsnumber_1234567881' + 'n-random-uuencoded-bits2-1' + salt2)]
|
45
|
+
[sha1('birthdate_postcode_1955-01-01_CB22 3AD' + salt1), encrypt(['xyz','abc'], 1', 'birthdate_postcode_1955-01-01_CB22 3AD' + 'n-random-uuencoded-bits2-2' + salt2)]
|
46
|
+
[sha1('birthdate_postcode_1955-02-01_' + salt1), encrypt(['zzz','aaa'], 1', 'birthdate_postcode_1955-02-01_' + 'n-random-uuencoded-bits2-3' + salt2)]
|
47
|
+
|
48
|
+
Encrypted meta-data, header row?
|
49
|
+
|
50
|
+
TODO: Replace text e.g. 'nhsnumber_1234567881' on RHS above with e.g. sha1('extraprefix_' + 'nhsnumber_1234567881') i.e. something non-disclosive, derivable from the original data.
|
51
|
+
TODO: Put original versions of e.g. nhsnumber, birthdate, postcode into "Encrypted demographics"
|
52
|
+
TODO: Maybe salt2 could be retained, to allow some fuzzy demographic matching, without the possibility of brute forcing the main identifiers??? Or maybe you can do pseudonymised matching without any salt...
|
53
|
+
|
54
|
+
Open questions:
|
55
|
+
Standard date / postcode normalisation
|
56
|
+
Do you escape underscores in field values
|
57
|
+
Do we need salt2, or re-use salt1?
|
58
|
+
Can we remove n-random-uuencoded-bits2 into the pseudonymised keys hash?
|
59
|
+
|
60
|
+
## Development
|
61
|
+
|
62
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
63
|
+
|
64
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'ndr_dev_support/tasks'
|
4
|
+
# require 'ndr_pseudonymise/tasks'
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << 'test'
|
8
|
+
t.libs << 'lib'
|
9
|
+
t.test_files = FileList['test/**/*_test.rb']
|
10
|
+
t.verbose = false
|
11
|
+
t.warning = false
|
12
|
+
end
|
13
|
+
|
14
|
+
task default: :test
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'ndr_pseudonymise'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require 'pry'
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module NdrPseudonymise
|
5
|
+
# A class to wrap interactions with a remote pseudonymisation service.
|
6
|
+
#
|
7
|
+
# Sample usage, against a local pseudonymisation service:
|
8
|
+
#
|
9
|
+
# client = NdrPseudonymise::Client.new(
|
10
|
+
# host: 'localhost', port: 3000, use_ssl: false,
|
11
|
+
# token: 'your_name:some_token', context: 'just testing'
|
12
|
+
# )
|
13
|
+
#
|
14
|
+
# client.pseudonymise(identifiers: { nhs_number: '0123456789' })
|
15
|
+
#
|
16
|
+
class Client
|
17
|
+
attr_accessor :context, :host
|
18
|
+
|
19
|
+
def initialize(host:, token:, port: 443, use_ssl: true, root_path: '/api/v1', context: nil)
|
20
|
+
@host = Net::HTTP.new(host, port).tap { |http| http.use_ssl = use_ssl }
|
21
|
+
@header = "Token token=#{token.inspect}"
|
22
|
+
@root_path = root_path
|
23
|
+
@context = context
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns a list of pseudonymisation keys that the current user is able to use.
|
27
|
+
#
|
28
|
+
# Sample usage:
|
29
|
+
#
|
30
|
+
# client.keys #=> [{name"=>"key one", "supported_variants"=>[1]}, ...]
|
31
|
+
#
|
32
|
+
def keys
|
33
|
+
get('/keys')
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a list of variants that the current user is able to use.
|
37
|
+
#
|
38
|
+
# Sample usage:
|
39
|
+
#
|
40
|
+
# client.variants #=> [{"variant"=>"1", "required_identifiers" => ["nhs_number"]}, ...]
|
41
|
+
#
|
42
|
+
def variants
|
43
|
+
get('/variants')
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns pseudonymised identifiers for the supplied identifiers.
|
47
|
+
# By default, the pseudonymisation service requests all ID variants
|
48
|
+
# from all available keys; this can be filtered by using `variants`
|
49
|
+
# and `key_names` respectively.
|
50
|
+
#
|
51
|
+
# Sample usage:
|
52
|
+
#
|
53
|
+
# client.pseudonymise(identifiers: { nhs_number: '0123456789' }) #=>
|
54
|
+
# [
|
55
|
+
# { "key_name"=>"key one", "variant"=>1, "pseudoid"=>"b549ef342...", "identifiers"=>... },
|
56
|
+
# { "key_name"=>"key two", "variant"=>1, "pseudoid"=>"0ebd91c13...", "identifiers"=>... },
|
57
|
+
# ]
|
58
|
+
#
|
59
|
+
# client.pseudonymise(identifiers: { postcode: 'CB22 3AD', birth_date: '2000-01-01' }, key_names: ['key two']) #=>
|
60
|
+
# [
|
61
|
+
# { "key_name"=>"key two", "variant"=>2, "pseudoid"=>"043d5fc1a...", "identifiers"=>... },
|
62
|
+
# ]
|
63
|
+
#
|
64
|
+
def pseudonymise(identifiers:, key_names: [], variants: [], context: @context)
|
65
|
+
raise ArgumentError, 'you must supply context!' if context.blank?
|
66
|
+
|
67
|
+
data = { identifiers: identifiers, context: context }
|
68
|
+
data[:key_names] = key_names if key_names.present?
|
69
|
+
data[:variants] = variants if variants.present?
|
70
|
+
|
71
|
+
post('/pseudonymise', data)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def get(endpoint)
|
77
|
+
handle_response { request(build_get_request(endpoint)) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def post(endpoint, params)
|
81
|
+
handle_response { request(build_post_request(endpoint, params)) }
|
82
|
+
end
|
83
|
+
|
84
|
+
delegate :request, to: :@host
|
85
|
+
|
86
|
+
def handle_response
|
87
|
+
response = yield
|
88
|
+
data = response.body.present? ? JSON.parse(response.body) : {}
|
89
|
+
|
90
|
+
case response.code.to_i
|
91
|
+
when 200
|
92
|
+
data
|
93
|
+
else
|
94
|
+
raise <<~MESSAGE
|
95
|
+
An error occured trying to use the pseudonymisation service. Details:
|
96
|
+
#{data.fetch('errors', []).join(', ')}
|
97
|
+
MESSAGE
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_get_request(endpoint)
|
102
|
+
Net::HTTP::Get.new(@root_path + endpoint).tap do |request|
|
103
|
+
request['Authorization'] = @header
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def build_post_request(endpoint, params)
|
108
|
+
Net::HTTP::Post.new(@root_path + endpoint).tap do |request|
|
109
|
+
request['Authorization'] = @header
|
110
|
+
request['Content-Type'] = 'application/json'
|
111
|
+
request.body = JSON.dump(params)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# Fast, simple pseudonymisation of prescription data with a very controlled
|
2
|
+
# format.
|
3
|
+
# Only the first 2 fields are potentially identifiable: nhs number and date of
|
4
|
+
# birth.
|
5
|
+
|
6
|
+
require 'ndr_pseudonymise/simple_pseudonymisation'
|
7
|
+
require 'ndr_pseudonymise/pseudonymisation_specification'
|
8
|
+
|
9
|
+
require 'json'
|
10
|
+
|
11
|
+
module NdrPseudonymise
|
12
|
+
# Pseudonymise prescription data
|
13
|
+
class DemographicsOnlyPseudonymiser < PseudonymisationSpecification
|
14
|
+
PREAMBLE_V2_DEMOG_ONLY = 'Pseudonymised matching data v2.0-demog-only'.freeze
|
15
|
+
|
16
|
+
# Pseudonymise a row of prescription data, returning an array of a single row:
|
17
|
+
# [[packed_pseudoid_and_demographics, clinical_data1, ...]]
|
18
|
+
# Where packed_pseudoid_and_demographics consists of
|
19
|
+
# "pseudo_id1 (key_bundle) packed_pseudoid_and_demographics"
|
20
|
+
def pseudonymise_row(row)
|
21
|
+
@key_cache ||= {} # Cache pseudonymisation keys for more compact import
|
22
|
+
# all_demographics = { 'nhsnumber' => row[0], 'birthdate' => row[1] }
|
23
|
+
all_demographics_hash = {}
|
24
|
+
demographics_cols = @format_spec[:demographics]
|
25
|
+
row.each_with_index do |x, i|
|
26
|
+
row_spec = @format_spec[:columns][i]
|
27
|
+
all_demographics_hash[row_spec[:title]] = x if demographics_cols.include?(i)
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: Refactor date handling into parent class's all_demographics_hash method
|
31
|
+
demographics_cols = @format_spec[:demographics]
|
32
|
+
row.each_with_index do |x, i|
|
33
|
+
row_spec = @format_spec[:columns][i]
|
34
|
+
if row_spec[:canonical_title]
|
35
|
+
if x.present? && row_spec[:strptime] && row_spec[:strftime]
|
36
|
+
# :strptime can contain a String (single format) or Array (a list of formats)
|
37
|
+
# :strftime contains a single string format
|
38
|
+
datetime = false
|
39
|
+
[row_spec[:strptime]].flatten(1).each do |format|
|
40
|
+
begin
|
41
|
+
datetime = DateTime.strptime(x, format)
|
42
|
+
break
|
43
|
+
rescue ArgumentError # Keep trying after invalid date formats
|
44
|
+
end
|
45
|
+
end
|
46
|
+
raise ArgumentError.new('Invalid date') if datetime == false # No formats matched
|
47
|
+
val = datetime.strftime(row_spec[:strftime])
|
48
|
+
else
|
49
|
+
val = x
|
50
|
+
end
|
51
|
+
all_demographics_hash[row_spec[:canonical_title]] = val
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# Ensure NHS number is empty string (expected by SimplePseudonymisation), not nil
|
57
|
+
all_demographics_hash['nhsnumber'] = all_demographics_hash['nhsnumber'].to_s
|
58
|
+
nhsnumber = all_demographics_hash['nhsnumber']
|
59
|
+
birthdate = all_demographics_hash['birthdate']
|
60
|
+
key = all_demographics_hash.to_json
|
61
|
+
if @key_cache.key?(key)
|
62
|
+
pseudo_id1, key_bundle, demog_key = @key_cache[key]
|
63
|
+
else
|
64
|
+
pseudo_id1, key_bundle, demog_key = NdrPseudonymise::SimplePseudonymisation.
|
65
|
+
generate_keys_nhsnumber_demog_only(@salt1, @salt2, nhsnumber)
|
66
|
+
if !nhsnumber.to_s.empty? && !birthdate.to_s.empty? # && false to stop caching
|
67
|
+
@key_cache = {} if @key_cache.size > 1000 # Limit cache size
|
68
|
+
@key_cache[key] = [pseudo_id1, key_bundle, demog_key]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
encrypted_demographics = NdrPseudonymise::SimplePseudonymisation.
|
72
|
+
encrypt_data64(demog_key, all_demographics_hash.to_json)
|
73
|
+
packed_pseudoid_and_demographics = format('%s (%s) %s', pseudo_id1, key_bundle,
|
74
|
+
encrypted_demographics)
|
75
|
+
[[packed_pseudoid_and_demographics] + clinical_data(row)]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Header row for CSV data
|
79
|
+
def csv_header_row
|
80
|
+
[PREAMBLE_V2_DEMOG_ONLY]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Append the output of pseudonymise_row to a CSV file
|
84
|
+
def emit_csv_rows(out_csv, pseudonymised_row)
|
85
|
+
out_csv << pseudonymised_row[0]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module NdrPseudonymise
|
4
|
+
module NdrEncrypt
|
5
|
+
# ndr_encrypt command line utility entry points.
|
6
|
+
module CommandLine
|
7
|
+
# ndr_encrypt tool needs to support ruby 2.0
|
8
|
+
# rubocop:disable Layout/HeredocIndentation, Rails/Exit, Style/SlicingWithRange
|
9
|
+
USAGE = <<-USAGE.gsub(/^ /, '').freeze
|
10
|
+
usage: ndr_encrypt [-v | --version] [-h | --help]
|
11
|
+
<command> [<args>]
|
12
|
+
|
13
|
+
These are common ndr_encrypt commands used in various situations:
|
14
|
+
|
15
|
+
start a working area
|
16
|
+
init Create an empty Git ndr_encrypt working copy
|
17
|
+
|
18
|
+
work with files
|
19
|
+
add Add file contents to the encrypted store and index
|
20
|
+
rm TODO: Remove files from the encrypted store and index
|
21
|
+
|
22
|
+
encryption key rotation and repository maintenance
|
23
|
+
gc Cleanup unnecessary index entries and optimize the encrypted store
|
24
|
+
resilver TODO
|
25
|
+
retire-key TODO
|
26
|
+
|
27
|
+
decrypt data
|
28
|
+
cat-files TODO: Retrieve local file based on git_blobid
|
29
|
+
cat-remote Retrieve remote file based on git_blobid
|
30
|
+
get Retrieve local file(s) based on path in CSV index
|
31
|
+
TODO: decrypt single file without git_blobid
|
32
|
+
|
33
|
+
Low-level Commands / Interrogators
|
34
|
+
cat-file TODO: Provide content or type and size information for encrypted objects
|
35
|
+
ls-files TODO: Show information about files in the index
|
36
|
+
|
37
|
+
Low-level Commands / Manipulators
|
38
|
+
hash-object TODO: Compute object ID and optionally create an encrypted object from a file
|
39
|
+
USAGE
|
40
|
+
|
41
|
+
COMMANDS = %w[init add cat-remote gc get].freeze
|
42
|
+
|
43
|
+
def self.run! # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
44
|
+
options = {}
|
45
|
+
parser = OptionParser.new do |opts|
|
46
|
+
# Hide TODOs in usage message
|
47
|
+
# rubocop:disable Style/SelectByRegexp
|
48
|
+
usage = USAGE.split("\n").reject { |s| s =~ /TODO/ }.join("\n")
|
49
|
+
# rubocop:enable Style/SelectByRegexp
|
50
|
+
opts.banner = "#{usage.chomp}\n\nAdditional options:"
|
51
|
+
|
52
|
+
opts.on('--base_url=URL', 'Remote repository URL') do |url|
|
53
|
+
options[:base_url] = url
|
54
|
+
end
|
55
|
+
opts.on('--key_name=NAME', /[A-Z0-9_-]*/i, 'Key name') do |name|
|
56
|
+
options[:key_name] = name
|
57
|
+
end
|
58
|
+
opts.on('--private_key=NAME', 'Private key filename') do |name|
|
59
|
+
options[:private_key] = name
|
60
|
+
end
|
61
|
+
opts.on('--pub_key=NAME', 'Public key filename') do |name|
|
62
|
+
options[:pub_key] = name
|
63
|
+
end
|
64
|
+
opts.on('--passin=OPTIONS', 'Pass in private key passphrase') do |name|
|
65
|
+
options[:passin] = name
|
66
|
+
end
|
67
|
+
opts.on('-p', 'Print downloaded object') do |v|
|
68
|
+
options[:pretty_print] = v
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
begin
|
73
|
+
parser.parse!
|
74
|
+
rescue OptionParser::ParseError => e
|
75
|
+
warn e
|
76
|
+
exit 1
|
77
|
+
end
|
78
|
+
|
79
|
+
parser.parse('--help') if ARGV.empty?
|
80
|
+
command = ARGV[0]
|
81
|
+
unless COMMANDS.include?(command)
|
82
|
+
warn <<-UNKNOWN_CMD
|
83
|
+
ndr_encrypt: '#{command}' is not an ndr_encrypt command. See 'ndr_encrypt --help'.
|
84
|
+
UNKNOWN_CMD
|
85
|
+
exit 1
|
86
|
+
end
|
87
|
+
send(command.gsub('-', '_'), ARGV[1..-1], options)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.init(args, options) # rubocop:disable Metrics/MethodLength
|
91
|
+
required_options = %i[]
|
92
|
+
allowed_options = required_options
|
93
|
+
unless (options.keys - allowed_options).empty? &&
|
94
|
+
required_options.all? { |sym| options.key?(sym) } &&
|
95
|
+
args.size <= 1
|
96
|
+
warn <<-USAGE
|
97
|
+
usage: ndr_encrypt init [<path>]
|
98
|
+
USAGE
|
99
|
+
exit 1
|
100
|
+
end
|
101
|
+
|
102
|
+
path = args[0] || Dir.pwd
|
103
|
+
action = if NdrEncrypt::Repository.new(repo_dir: path).init
|
104
|
+
'Initialized empty'
|
105
|
+
else
|
106
|
+
'Reinitialized existing'
|
107
|
+
end
|
108
|
+
$stdout.puts "#{action} ndr_encrypted encrypted store in " \
|
109
|
+
"#{File.join(File.expand_path(path), NdrEncrypt::Repository::ENCRYPTED_DIR)}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.add(args, options) # rubocop:disable Metrics/MethodLength
|
113
|
+
required_options = %i[key_name pub_key]
|
114
|
+
allowed_options = required_options
|
115
|
+
unless (options.keys - allowed_options).empty? &&
|
116
|
+
required_options.all? { |sym| options.key?(sym) }
|
117
|
+
warn <<-USAGE
|
118
|
+
usage: ndr_encrypt add --key_name=<keyname> --pub_key=<path> [<path>...]
|
119
|
+
USAGE
|
120
|
+
exit 1
|
121
|
+
end
|
122
|
+
if args.empty?
|
123
|
+
warn 'Nothing specified, nothing added.'
|
124
|
+
return
|
125
|
+
end
|
126
|
+
|
127
|
+
path = Dir.pwd
|
128
|
+
repo = NdrEncrypt::Repository.new(repo_dir: path)
|
129
|
+
repo.add(args, key_name: options[:key_name], pub_key: options[:pub_key])
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.cat_remote(args, options) # rubocop:disable Metrics/MethodLength
|
133
|
+
# TODO: Add -e option to check whether remote file exists
|
134
|
+
required_options = %i[key_name private_key base_url pretty_print]
|
135
|
+
allowed_options = required_options + %i[passin]
|
136
|
+
unless (options.keys - allowed_options).empty? &&
|
137
|
+
required_options.all? { |sym| options.key?(sym) } &&
|
138
|
+
args.size == 1
|
139
|
+
warn <<-USAGE
|
140
|
+
usage: ndr_encrypt cat-remote --key_name=<keyname> --private_key=<path> --base_url=<url>
|
141
|
+
-p <git_blobid>
|
142
|
+
USAGE
|
143
|
+
exit 1
|
144
|
+
end
|
145
|
+
|
146
|
+
git_blobid = args[0]
|
147
|
+
remote_store = NdrEncrypt::RemoteRepository.new(base_url: options[:base_url])
|
148
|
+
blob = remote_store.cat_remote(git_blobid, key_name: options[:key_name],
|
149
|
+
private_key: options[:private_key],
|
150
|
+
passin: options[:passin])
|
151
|
+
# TODO: Add error handling, for connection issues / file not found
|
152
|
+
$stdout.print blob
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.gc(args, options)
|
156
|
+
required_options = %i[]
|
157
|
+
allowed_options = required_options
|
158
|
+
unless (options.keys - allowed_options).empty? &&
|
159
|
+
required_options.all? { |sym| options.key?(sym) } &&
|
160
|
+
args.empty?
|
161
|
+
warn <<-USAGE
|
162
|
+
usage: ndr_encrypt gc
|
163
|
+
USAGE
|
164
|
+
exit 1
|
165
|
+
end
|
166
|
+
|
167
|
+
path = Dir.pwd
|
168
|
+
NdrEncrypt::Repository.new(repo_dir: path).gc(output_stream: $stdout)
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.get(args, options) # rubocop:disable Metrics/MethodLength
|
172
|
+
required_options = %i[key_name private_key]
|
173
|
+
allowed_options = required_options + %i[passin]
|
174
|
+
unless (options.keys - allowed_options).empty? &&
|
175
|
+
required_options.all? { |sym| options.key?(sym) }
|
176
|
+
warn <<-USAGE
|
177
|
+
usage: ndr_encrypt get --key_name=<keyname> --private_key=<path> [<path>...]
|
178
|
+
USAGE
|
179
|
+
exit 1
|
180
|
+
end
|
181
|
+
if args.empty?
|
182
|
+
warn 'Nothing specified, nothing to get.'
|
183
|
+
return
|
184
|
+
end
|
185
|
+
|
186
|
+
path = Dir.pwd
|
187
|
+
repo = NdrEncrypt::Repository.new(repo_dir: path)
|
188
|
+
repo.get(args, key_name: options[:key_name], private_key: options[:private_key],
|
189
|
+
passin: options[:passin])
|
190
|
+
end
|
191
|
+
# rubocop:enable Layout/HeredocIndentation, Rails/Exit, Style/SlicingWithRange
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|