ndr_pseudonymise 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://github.com/publichealthengland/ndr_pseudonymise/workflows/Test/badge.svg)](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
|