password_protected_file 0.0.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/password_protected_file.rb +137 -0
  3. metadata +44 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7c8e29e2bfe872a6fc6c04ce9b97c2586280676c
4
+ data.tar.gz: b907e163dc55ec86cd3c363a2c02a32ca3519225
5
+ SHA512:
6
+ metadata.gz: 9562a35d9f717ff823ad65fa65b768eff99154ca2fd99209a0872e4d7c12e38b9ecd3580703ff1a14f35f2334755934f9dcdeae1b123ba32ae615a7b3a3d4b8c
7
+ data.tar.gz: 454ad97ea0115614d4641b060af66b497add655ae51142927b4f95894470954877f819b7c6d2edfdc467cd003b5ccc08c18696eedbe4e8af8b28405d613d5346
@@ -0,0 +1,137 @@
1
+ class PasswordProtectedFile
2
+ DEFAULT_ITERATIONS = 20_000
3
+ ITERATION_BYTES = 8
4
+ KEY_BYTES = 32
5
+ SALT_BYTES = 16
6
+ IV_BYTES = 16
7
+ HASH_FUNCTION = OpenSSL::Digest::SHA256
8
+
9
+ private_class_method :new
10
+
11
+ def self.open(file_name, password)
12
+ assert_file_exists(file_name)
13
+ assert_valid_password(password)
14
+ assert_valid_filename(file_name)
15
+ new(file_name, password).tap { |o| o.__send__(:open_existing) }
16
+ end
17
+
18
+ def self.create(file_name, password, data = '')
19
+ assert_file_does_not_exist(file_name)
20
+ assert_valid_password(password)
21
+ assert_valid_filename(file_name)
22
+ new(file_name, password).tap { |o| o.__send__(:create_new, data) }
23
+ end
24
+
25
+ def data
26
+ @data.clone
27
+ end
28
+
29
+ def data=(new_data)
30
+ fail InvalidDataError unless new_data.instance_of?(String)
31
+ fail InvalidStringEncodingError unless new_data.encoding == Encoding::UTF_8
32
+ @data = new_data.clone
33
+ write_file
34
+ end
35
+
36
+ private
37
+
38
+ def initialize(file_name, password)
39
+ @file_name = file_name
40
+ @password = password
41
+ end
42
+
43
+ def create_new(data)
44
+ @data = data
45
+ @iterations = DEFAULT_ITERATIONS
46
+ write_file
47
+ end
48
+
49
+ def open_existing
50
+ load_file(@file_name, @password)
51
+ end
52
+
53
+ def load_file(file_name, password)
54
+ file = File.binread(file_name).chars
55
+ pw_salt = file.shift(SALT_BYTES).join
56
+ pw_hash = file.shift(KEY_BYTES).join
57
+ aes_pw_salt = file.shift(SALT_BYTES).join
58
+ aes_iv = file.shift(IV_BYTES).join
59
+ @iterations = file.shift(ITERATION_BYTES).join.to_i
60
+ encrypted_data = file.join
61
+
62
+ hashed_pw = pbkdf2(password, pw_salt)
63
+ fail IncorrectPasswordError unless hashed_pw == pw_hash
64
+
65
+ aes_key = pbkdf2(password, aes_pw_salt)
66
+ cipher = new_aes256(:decrypt, aes_key, aes_iv)
67
+ @data = (cipher.update(encrypted_data) + cipher.final)[0..-2]
68
+ end
69
+
70
+ def write_file
71
+ pw_salt = new_salt
72
+ pw_hash = pbkdf2(@password, pw_salt)
73
+ aes_pw_salt = new_salt
74
+ aes_iv = SecureRandom.random_bytes(IV_BYTES)
75
+ aes_key = pbkdf2(@password, aes_pw_salt)
76
+
77
+ cipher = new_aes256(:encrypt, aes_key, aes_iv)
78
+ encrypted_data = cipher.update(@data + "\0") + cipher.final
79
+
80
+ File.open(@file_name, 'w') do |f|
81
+ f.print(pw_salt)
82
+ f.print(pw_hash)
83
+ f.print(aes_pw_salt)
84
+ f.print(aes_iv)
85
+ f.print(@iterations.to_s.rjust(ITERATION_BYTES))
86
+ f.print(encrypted_data)
87
+ end
88
+ true
89
+ end
90
+
91
+ def pbkdf2(password, salt)
92
+ OpenSSL::PKCS5::pbkdf2_hmac(password, salt, @iterations, KEY_BYTES, HASH_FUNCTION.new)
93
+ end
94
+
95
+ def new_aes256(mode, key, iv)
96
+ OpenSSL::Cipher::AES256.new(:CBC).tap do |c|
97
+ c.__send__(mode)
98
+ c.key = key
99
+ c.iv = iv
100
+ end
101
+ end
102
+
103
+ def new_salt
104
+ SecureRandom.random_bytes(SALT_BYTES)
105
+ end
106
+
107
+ def self.assert_file_exists(file_name)
108
+ return file_name if file_name.is_a?(String) && File.exist?(file_name)
109
+ fail FileNotFoundError.new("File #{file_name} not found")
110
+ end
111
+
112
+ def self.assert_file_does_not_exist(file_name)
113
+ return file_name unless file_name.is_a?(String) && File.exist?(file_name)
114
+ fail FilenameNotAvailableError.new("File #{file_name} already exists")
115
+ end
116
+
117
+ def self.assert_valid_password(password)
118
+ return password if password.is_a?(String) && !password.empty?
119
+ fail InvalidPasswordError.new("Invalid password given: #{password.inspect}")
120
+ end
121
+
122
+ def self.assert_valid_filename(file_name)
123
+ return file_name if file_name.is_a?(String) &&
124
+ !file_name.empty? &&
125
+ Dir.exist?(File.dirname(file_name))
126
+ fail InvalidFilenameError.new("Invalid filename given: #{file_name.inspect}")
127
+ end
128
+
129
+ class PasswordProtectedFileError < StandardError; end
130
+ class IncorrectPasswordError < PasswordProtectedFileError; end
131
+ class InvalidPasswordError < PasswordProtectedFileError; end
132
+ class InvalidDataError < PasswordProtectedFileError; end
133
+ class InvalidStringEncodingError < PasswordProtectedFileError; end
134
+ class InvalidFilenameError < PasswordProtectedFileError; end
135
+ class FileNotFoundError < PasswordProtectedFileError; end
136
+ class FilenameNotAvailableError < PasswordProtectedFileError; end
137
+ end
metadata ADDED
@@ -0,0 +1,44 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: password_protected_file
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Isaac Post
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: An easy way to create files protected by a password
14
+ email: post.isaac@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/password_protected_file.rb
20
+ homepage: https://github.com/ipost/password_protected_file
21
+ licenses:
22
+ - MIT
23
+ metadata: {}
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ requirements: []
39
+ rubyforge_project:
40
+ rubygems_version: 2.4.8
41
+ signing_key:
42
+ specification_version: 4
43
+ summary: An easy way to create files protected by a password
44
+ test_files: []