pod4 0.9.3 → 0.10.0
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 +5 -5
- data/.hgtags +1 -0
- data/README.md +8 -0
- data/lib/pod4/encrypting.rb +225 -0
- data/lib/pod4/typecasting.rb +280 -14
- data/lib/pod4/version.rb +1 -1
- data/md/typecasting.md +80 -0
- data/spec/common/model_plus_encrypting_spec.rb +379 -0
- data/spec/common/model_plus_typecasting_spec.rb +434 -22
- metadata +16 -11
@@ -0,0 +1,379 @@
|
|
1
|
+
require "date"
|
2
|
+
require "openssl"
|
3
|
+
require "octothorpe"
|
4
|
+
|
5
|
+
require "pod4"
|
6
|
+
require "pod4/encrypting"
|
7
|
+
require "pod4/null_interface"
|
8
|
+
require "pod4/errors"
|
9
|
+
|
10
|
+
|
11
|
+
describe "(Model with Encryption)" do
|
12
|
+
|
13
|
+
##
|
14
|
+
# Encrypt / decrypt
|
15
|
+
#
|
16
|
+
def encrypt(key, iv=nil, plaintext)
|
17
|
+
cipher = OpenSSL::Cipher.new(iv ? Pod4::Encrypting::CIPHER_IV : Pod4::Encrypting::CIPHER_NO_IV)
|
18
|
+
cipher.encrypt
|
19
|
+
cipher.key = key
|
20
|
+
cipher.iv = iv if iv
|
21
|
+
cipher.update(plaintext) + cipher.final
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:encryption_key) { "dflkasdgklajndgnalkghlgasdgasdghaalsdg" }
|
25
|
+
|
26
|
+
let(:medical_model_class) do # model with an IV column
|
27
|
+
Class.new Pod4::Model do
|
28
|
+
include Pod4::Encrypting
|
29
|
+
attr_columns :id, :nhs_no # note, we don't bother to name encrypted columns
|
30
|
+
encrypted_columns :name, :ailment, :prescription
|
31
|
+
set_key "dflkasdgklajndgnalkghlgasdgasdghaalsdg"
|
32
|
+
set_iv_column :nonce
|
33
|
+
set_interface NullInterface.new(:id, :nhs_no, :name, :ailment, :prescription, :nonce, [])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
let(:medical_model_bad_class) do # model with an IV column and a very very short key
|
38
|
+
Class.new Pod4::Model do
|
39
|
+
include Pod4::Encrypting
|
40
|
+
attr_columns :id, :nhs_no # note, we don't bother to name encrypted columns
|
41
|
+
encrypted_columns :name, :ailment, :prescription
|
42
|
+
set_key "d"
|
43
|
+
set_iv_column :nonce
|
44
|
+
set_interface NullInterface.new(:id, :nhs_no, :name, :ailment, :prescription, :nonce, [])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
let(:diary_model_class) do # model without an IV column
|
49
|
+
Class.new Pod4::Model do
|
50
|
+
include Pod4::Encrypting
|
51
|
+
attr_columns :id, :date, :heading, :text
|
52
|
+
encrypted_columns :heading, :text
|
53
|
+
set_key "dflkasdgklajndgnalkghlgasdgasdghaalsdg"
|
54
|
+
set_interface NullInterface.new(:id, :date, :heading, :text, [])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
let(:m40) do
|
59
|
+
m = medical_model_class.new
|
60
|
+
m.id = 40
|
61
|
+
m.nhs_no = "123456"
|
62
|
+
m.name = "fred"
|
63
|
+
m.ailment = "sore toe"
|
64
|
+
m.prescription = "suck thumb"
|
65
|
+
m
|
66
|
+
end
|
67
|
+
|
68
|
+
let(:d44) do
|
69
|
+
d = diary_model_class.new
|
70
|
+
d.id = 44
|
71
|
+
d.date = Date.new(2018,4,14)
|
72
|
+
d.heading = "fred"
|
73
|
+
d.text = "sore toe"
|
74
|
+
d
|
75
|
+
end
|
76
|
+
|
77
|
+
let(:m40_record) { m40.interface.read(40) }
|
78
|
+
let(:d44_record) { d44.interface.read(44) }
|
79
|
+
|
80
|
+
|
81
|
+
describe "Model::CIPHER_IV" do
|
82
|
+
|
83
|
+
it "should be a valid string to initialize OpenSSL::Cipher" do
|
84
|
+
expect{ OpenSSL::Cipher.new(medical_model_class::CIPHER_IV) }.not_to raise_exception
|
85
|
+
end
|
86
|
+
|
87
|
+
end # of Model::CIPHER_IV
|
88
|
+
|
89
|
+
|
90
|
+
describe "Model::CIPHER_NO_IV" do
|
91
|
+
|
92
|
+
it "should be a valid string to initialize OpenSSL::Cipher" do
|
93
|
+
expect{ OpenSSL::Cipher.new(medical_model_class::CIPHER_NO_IV) }.not_to raise_exception
|
94
|
+
end
|
95
|
+
|
96
|
+
end # of Model::CIPHER_NO_IV
|
97
|
+
|
98
|
+
|
99
|
+
describe "Model.set_key" do
|
100
|
+
|
101
|
+
it "requires a string" do
|
102
|
+
expect( medical_model_class ).to respond_to(:set_key).with(1).argument
|
103
|
+
end
|
104
|
+
|
105
|
+
end # of Model.set_key
|
106
|
+
|
107
|
+
|
108
|
+
describe "Model.encryption_key" do
|
109
|
+
|
110
|
+
it "returns the key passed to set_key" do
|
111
|
+
expect( medical_model_class.encryption_key ).to eq encryption_key
|
112
|
+
end
|
113
|
+
|
114
|
+
end # of Model.encryption_key
|
115
|
+
|
116
|
+
|
117
|
+
describe "Model.set_iv_column" do
|
118
|
+
|
119
|
+
it "requires an IV column" do
|
120
|
+
expect( medical_model_class ).to respond_to(:set_iv_column).with(1).argument
|
121
|
+
end
|
122
|
+
|
123
|
+
it "exposes the encryption_iv attribute, just like attr_accessor" do
|
124
|
+
m20 = medical_model_class.new(20)
|
125
|
+
expect( m20 ).to respond_to(:nonce)
|
126
|
+
expect( m20 ).to respond_to(:nonce=)
|
127
|
+
end
|
128
|
+
|
129
|
+
# See also: Model#encryption_iv
|
130
|
+
|
131
|
+
end # of Model.set_iv_column
|
132
|
+
|
133
|
+
|
134
|
+
describe "Model.encryption_iv_column" do
|
135
|
+
|
136
|
+
it "returns the column passed to set_iv_column" do
|
137
|
+
expect( medical_model_class.encryption_iv_column ).to eq :nonce
|
138
|
+
end
|
139
|
+
|
140
|
+
end # of Model.encryption_iv_column
|
141
|
+
|
142
|
+
|
143
|
+
describe "Model.encrypted_columns" do
|
144
|
+
|
145
|
+
it "requires a list of columns" do
|
146
|
+
expect( medical_model_class ).to respond_to(:encrypted_columns).with(1).argument
|
147
|
+
end
|
148
|
+
|
149
|
+
it "adds the columns to the model's column list, just like attr_columns" do
|
150
|
+
expect( medical_model_class.columns ).
|
151
|
+
to match_array( %i|id nonce nhs_no name ailment prescription| )
|
152
|
+
|
153
|
+
expect( diary_model_class.columns ).to match_array( %i|id date heading text| )
|
154
|
+
end
|
155
|
+
|
156
|
+
it "exposes the columns just like attr_accessor" do
|
157
|
+
m20 = medical_model_class.new(20)
|
158
|
+
|
159
|
+
expect( m20 ).to respond_to(:name)
|
160
|
+
expect( m20 ).to respond_to(:ailment)
|
161
|
+
expect( m20 ).to respond_to(:prescription)
|
162
|
+
expect( m20 ).to respond_to(:name=)
|
163
|
+
expect( m20 ).to respond_to(:ailment=)
|
164
|
+
expect( m20 ).to respond_to(:prescription=)
|
165
|
+
end
|
166
|
+
|
167
|
+
end # of Model.encrypted_columns
|
168
|
+
|
169
|
+
|
170
|
+
describe "Model.encryption_columns" do
|
171
|
+
|
172
|
+
it "exposes the columns given in encrypted_columns" do
|
173
|
+
expect( medical_model_class.encryption_columns ).
|
174
|
+
to match_array( %i|name ailment prescription| )
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end # of Model.encryption_columns
|
179
|
+
|
180
|
+
|
181
|
+
describe "(Creating a record)" do
|
182
|
+
|
183
|
+
context "when we don't have an IV column" do
|
184
|
+
|
185
|
+
it "scrambles the encrypted columns and leaves the others alone" do
|
186
|
+
d44.create
|
187
|
+
expect( d44_record.>>.date ).to eq d44.date
|
188
|
+
expect( d44_record.>>.heading ).not_to eq d44.heading
|
189
|
+
expect( d44_record.>>.text ).not_to eq d44.text
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
context "when we have an IV column" do
|
195
|
+
|
196
|
+
it "writes an IV to the nonce field" do
|
197
|
+
m40.create
|
198
|
+
expect( m40_record.>>.nonce ).not_to be_nil
|
199
|
+
expect( m40_record.>>.nonce ).to eq m40.nonce
|
200
|
+
expect( m40_record.>>.nonce ).to eq m40.encryption_iv
|
201
|
+
end
|
202
|
+
|
203
|
+
it "scrambles the encrypted columns and leaves the others alone" do
|
204
|
+
m40.create
|
205
|
+
expect( m40_record.>>.nhs_no ).to eq m40.nhs_no
|
206
|
+
expect( m40_record.>>.name ).not_to eq m40.name
|
207
|
+
expect( m40_record.>>.ailment ).not_to eq m40.ailment
|
208
|
+
expect( m40_record.>>.prescription ).not_to eq m40.prescription
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
end # of (Creating a record)
|
214
|
+
|
215
|
+
|
216
|
+
describe "(reading a record)" do
|
217
|
+
|
218
|
+
context "when we have no IV column" do
|
219
|
+
before(:each) { d44.create }
|
220
|
+
|
221
|
+
it "decrypts the columns" do
|
222
|
+
d = diary_model_class.new(44).read
|
223
|
+
expect( d.date ).to eq Date.new(2018,4,14)
|
224
|
+
expect( d.heading ).to eq "fred"
|
225
|
+
expect( d.text ).to eq "sore toe"
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
context "when we have an IV column" do
|
231
|
+
before(:each) { m40.create }
|
232
|
+
|
233
|
+
it "returns the IV field as encryption_iv" do
|
234
|
+
m = medical_model_class.new(40).read
|
235
|
+
expect( m.encryption_iv ).not_to be_nil
|
236
|
+
expect( m.encryption_iv ).to eq m.nonce
|
237
|
+
end
|
238
|
+
|
239
|
+
it "decrypts the columns" do
|
240
|
+
m = medical_model_class.new(40).read
|
241
|
+
expect( m.nhs_no ).to eq "123456"
|
242
|
+
expect( m.name ).to eq "fred"
|
243
|
+
expect( m.ailment ).to eq "sore toe"
|
244
|
+
expect( m.prescription ).to eq "suck thumb"
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
end # of (reading a record)
|
250
|
+
|
251
|
+
|
252
|
+
describe "Model#(encryption_iv field)" do
|
253
|
+
|
254
|
+
it "returns nil for a new model object" do
|
255
|
+
m = medical_model_class.new
|
256
|
+
m.id = 50
|
257
|
+
expect( m.nonce ).to be_nil
|
258
|
+
end
|
259
|
+
|
260
|
+
it "returns the nonce for an existing record" do
|
261
|
+
expect( m40.nonce ).to eq m40_record.>>.nonce
|
262
|
+
end
|
263
|
+
|
264
|
+
end # of Model#(encryption_iv field)
|
265
|
+
|
266
|
+
|
267
|
+
describe "Model#encryption_iv" do
|
268
|
+
|
269
|
+
it "returns nil for a new model object" do
|
270
|
+
m = medical_model_class.new
|
271
|
+
m.id = 50
|
272
|
+
|
273
|
+
expect( m.encryption_iv ).to be_nil
|
274
|
+
end
|
275
|
+
|
276
|
+
it "returns nil if we don't have an IV set" do
|
277
|
+
d44.create
|
278
|
+
expect( d44.encryption_iv ).to be_nil
|
279
|
+
end
|
280
|
+
|
281
|
+
it "returns the nonce for an existing record" do
|
282
|
+
expect( m40.encryption_iv ).to eq m40_record.>>.nonce
|
283
|
+
end
|
284
|
+
|
285
|
+
it "returns the same as the actual IV field" do
|
286
|
+
expect( m40.encryption_iv ).to eq m40.nonce
|
287
|
+
end
|
288
|
+
|
289
|
+
end # of Model#encryption_iv
|
290
|
+
|
291
|
+
|
292
|
+
describe "Model#map_to_interface" do
|
293
|
+
|
294
|
+
it "raises Pod4Error if there is an encryption problem, eg, key too short" do
|
295
|
+
bad = medical_model_bad_class.new
|
296
|
+
bad.id = 999
|
297
|
+
bad.nhs_no = "12345"
|
298
|
+
bad.name = "alice"
|
299
|
+
bad.ailment = "tiny key"
|
300
|
+
bad.prescription = "raise an exception"
|
301
|
+
|
302
|
+
expect{ bad.map_to_interface }.to raise_exception Pod4::Pod4Error
|
303
|
+
end
|
304
|
+
|
305
|
+
context "when we don't have an IV column" do
|
306
|
+
|
307
|
+
it "encrypts only the encryptable columns for the interface" do
|
308
|
+
ot = d44.map_to_interface
|
309
|
+
expect( ot.>>.date ).to eq Date.new(2018,4,14)
|
310
|
+
expect( ot.>>.heading ).to eq encrypt(encryption_key, "fred")
|
311
|
+
expect( ot.>>.text ).to eq encrypt(encryption_key, "sore toe")
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
|
316
|
+
context "when we have an IV column" do
|
317
|
+
before(:each) { m40.create }
|
318
|
+
|
319
|
+
it "sets the IV column on create" do
|
320
|
+
expect( m40_record.>>.nonce ).not_to be_nil
|
321
|
+
end
|
322
|
+
|
323
|
+
it "encrypts only the encryptable columns for the interface" do
|
324
|
+
ot = m40.map_to_interface
|
325
|
+
iv = m40.nonce
|
326
|
+
expect( ot.>>.nhs_no ).to eq "123456"
|
327
|
+
expect( ot.>>.name ).to eq encrypt(encryption_key, iv, "fred")
|
328
|
+
expect( ot.>>.ailment ).to eq encrypt(encryption_key, iv, "sore toe")
|
329
|
+
expect( ot.>>.prescription ).to eq encrypt(encryption_key, iv, "suck thumb")
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
|
334
|
+
end # of Model#map_to_interface
|
335
|
+
|
336
|
+
|
337
|
+
describe "Model#map_to_model" do
|
338
|
+
|
339
|
+
context "when we don't have an IV column" do
|
340
|
+
|
341
|
+
it "decrypts only the encryptable columns for the model" do
|
342
|
+
d44.create
|
343
|
+
|
344
|
+
d = diary_model_class.new(44)
|
345
|
+
expect( d44_record.>>.heading ).not_to eq "fred"
|
346
|
+
expect( d44_record.>>.text ).not_to eq "sore toe"
|
347
|
+
|
348
|
+
d.read
|
349
|
+
expect( d.date ).to eq Date.new(2018,4,14)
|
350
|
+
expect( d.heading ).to eq "fred"
|
351
|
+
expect( d.text ).to eq "sore toe"
|
352
|
+
end
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
context "when we have an IV column" do
|
357
|
+
|
358
|
+
it "decrypts only the encryptable columns for the model" do
|
359
|
+
m40.create
|
360
|
+
|
361
|
+
m = medical_model_class.new(40)
|
362
|
+
expect( m40_record.>>.name ).not_to eq "fred"
|
363
|
+
expect( m40_record.>>.ailment ).not_to eq "sore toe"
|
364
|
+
expect( m40_record.>>.prescription ).not_to eq "suck thumb"
|
365
|
+
|
366
|
+
m.read
|
367
|
+
expect( m.nhs_no ).to eq "123456"
|
368
|
+
expect( m.name ).to eq "fred"
|
369
|
+
expect( m.ailment ).to eq "sore toe"
|
370
|
+
expect( m.prescription ).to eq "suck thumb"
|
371
|
+
end
|
372
|
+
|
373
|
+
end
|
374
|
+
|
375
|
+
end # of Model#map_to_model
|
376
|
+
|
377
|
+
|
378
|
+
end
|
379
|
+
|