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.
- checksums.yaml +7 -0
- data/lib/password_protected_file.rb +137 -0
- 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: []
|