can 0.10.13
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 +1 -0
- data/Gemfile +3 -0
- data/README.md +95 -0
- data/Rakefile +6 -0
- data/bin/can +330 -0
- data/can.gemspec +27 -0
- data/lib/can.rb +9 -0
- data/lib/can/crypto.rb +41 -0
- data/lib/can/store.rb +174 -0
- data/lib/can/util.rb +15 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d06281ea5c30184ab441e8e55e192b5f283e592926440f5a0e9c7fd99c7632fe
|
4
|
+
data.tar.gz: a10271e0b9fc97618632b9d733578825e3e156e319cd976149431a97beb83c7e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 166a03130bbb9e6078fc4fb4e87a57cae1e07fdd4fd70b959737a9c42f4b0e9a47b43fe1ff625bd3896ee225638e950e50c4294b9ac41655aa187d338984ffd1
|
7
|
+
data.tar.gz: 564ee2b2a40f920820195f480fbaa238921657f303198307e9d914964278a77f65c46e3c2c66e0bbf95eefa00d9a6754601e234f4252af2f3f76a715c32234c7
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg/
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# Can
|
2
|
+
|
3
|
+
A small cli tool that stores encrypted goods.
|
4
|
+
|
5
|
+
It uses symmetric cryptography with the cipher `AES-256-CBC`. The file database
|
6
|
+
of secrets is ascii based so feel free to commit it.
|
7
|
+
|
8
|
+
|
9
|
+
# Installation
|
10
|
+
|
11
|
+
gem install can
|
12
|
+
|
13
|
+
|
14
|
+
# Usage
|
15
|
+
|
16
|
+
% can set test ok # Stores a secret
|
17
|
+
Key test was stored.
|
18
|
+
% can get test # Copy that secret to the clipboard
|
19
|
+
Password:
|
20
|
+
Key test was copied to the clipboard.
|
21
|
+
|
22
|
+
|
23
|
+
## Commands
|
24
|
+
|
25
|
+
% can
|
26
|
+
Commands:
|
27
|
+
can decrypt DATA # Decrypts data
|
28
|
+
can encrypt DATA # Encrypts data
|
29
|
+
can get KEY # Copies a KEY to the clipboard
|
30
|
+
can help [COMMAND] # Describe available commands or one specific command
|
31
|
+
can ls [TAG] # Lists all keys (filter optionally by TAG)
|
32
|
+
can password # Change the can password
|
33
|
+
can random [LENGTH] # Generates a random password
|
34
|
+
can rename KEY NEW_NAME # Renames a secret
|
35
|
+
can rm KEY # Removes a key
|
36
|
+
can set KEY [VALUE] # Stores a value (empty VALUE show the prompt; use '@rando...
|
37
|
+
can tag KEY TAG # Tags a key
|
38
|
+
can tags [KEY] # Show all tags (filter for a key)
|
39
|
+
can untag KEY TAG # Untags a tag from a key
|
40
|
+
can version # Show the current version
|
41
|
+
|
42
|
+
Options:
|
43
|
+
-p, [--password=PASSWORD]
|
44
|
+
-v, [--verbose], [--no-verbose]
|
45
|
+
-f, [--file=FILE]
|
46
|
+
|
47
|
+
|
48
|
+
## Using another can file
|
49
|
+
|
50
|
+
Use the `CAN_FILE` environment variable or pass the `--file FILE` or `-f FILE`
|
51
|
+
param option to use another can file:
|
52
|
+
|
53
|
+
% export CAN_FILE="$HOME/secrets/main.can"
|
54
|
+
% can ls --file $HOME/secrets/main.can
|
55
|
+
% can ls -f $HOME/secrets/main.can
|
56
|
+
aws-root
|
57
|
+
azure-aad
|
58
|
+
vpn-demo
|
59
|
+
|
60
|
+
|
61
|
+
## Default lookup files
|
62
|
+
|
63
|
+
If you don't pass an explicit `--file` or `-f` the default ones are checked in
|
64
|
+
order. The first one to exist is used:
|
65
|
+
|
66
|
+
~/.config/can/main.can
|
67
|
+
~/.can
|
68
|
+
|
69
|
+
|
70
|
+
## Default lookup directories
|
71
|
+
|
72
|
+
If you pass an explicit file (via the `--file` or `-f` flags) *without* a `.can`
|
73
|
+
file extension the following directories are checked. The first one to have a
|
74
|
+
file that exists with a `<dir>/<file>.can` match is used:
|
75
|
+
|
76
|
+
~/.config/can
|
77
|
+
/etc/can
|
78
|
+
|
79
|
+
|
80
|
+
## Avoid the password prompt
|
81
|
+
|
82
|
+
Use the `CAN_PASSWORD` environment variable to avoid the password prompt:
|
83
|
+
|
84
|
+
% export CAN_PASSWORD="secret"
|
85
|
+
% can ls
|
86
|
+
aws-root
|
87
|
+
azure-aad
|
88
|
+
vpn-demo
|
89
|
+
|
90
|
+
Or pass it as a arg option (`-p` or `--password`):
|
91
|
+
|
92
|
+
% can ls -p secret
|
93
|
+
aws-root
|
94
|
+
azure-aad
|
95
|
+
vpn-demo
|
data/Rakefile
ADDED
data/bin/can
ADDED
@@ -0,0 +1,330 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + "/../lib"
|
3
|
+
|
4
|
+
require "thor"
|
5
|
+
require "can"
|
6
|
+
require "io/console"
|
7
|
+
require "json"
|
8
|
+
require "tablelize"
|
9
|
+
|
10
|
+
DIRS = [
|
11
|
+
"#{ENV['HOME']}/.config/can",
|
12
|
+
"/etc/can",
|
13
|
+
]
|
14
|
+
|
15
|
+
FILES = [
|
16
|
+
"#{ENV['HOME']}/.config/can/main.can",
|
17
|
+
"#{ENV['HOME']}/.can",
|
18
|
+
]
|
19
|
+
|
20
|
+
module Can
|
21
|
+
class Cli < Thor
|
22
|
+
class_option :password, aliases: "-p"
|
23
|
+
class_option :verbose, aliases: "-v", :type => :boolean
|
24
|
+
class_option :file, aliases: "-f"
|
25
|
+
|
26
|
+
desc "version", "Show the current version"
|
27
|
+
def version
|
28
|
+
puts VERSION
|
29
|
+
latest = Util.latest()
|
30
|
+
debug "Remote version: #{latest}"
|
31
|
+
if Gem::Version.new(latest) > Gem::Version.new(VERSION)
|
32
|
+
STDERR.puts "New version #{latest} available."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# desc "dump", "Show all the contents"
|
37
|
+
# def dump
|
38
|
+
# init
|
39
|
+
# p @can.all()
|
40
|
+
# end
|
41
|
+
|
42
|
+
# desc "info", "Show the current state of affairs"
|
43
|
+
# def info
|
44
|
+
# init
|
45
|
+
# # options[:verbose] = true
|
46
|
+
# ENV["CAN_DEBUG"] = "1"
|
47
|
+
# _info()
|
48
|
+
# end
|
49
|
+
|
50
|
+
desc "ls [TAG]", "Lists all keys (filter optionally by TAG)"
|
51
|
+
option :short, :type => :boolean, aliases: "-s"
|
52
|
+
def ls(tag=nil)
|
53
|
+
init
|
54
|
+
rows = [["KEY", "LEN", "CREATED", "TAGS"]]
|
55
|
+
empty = true
|
56
|
+
|
57
|
+
@can.all().each do |key, entry|
|
58
|
+
empty = false
|
59
|
+
next if tag and (not entry["tags"] or not entry["tags"].include?(tag))
|
60
|
+
if options[:short]
|
61
|
+
puts key
|
62
|
+
next
|
63
|
+
end
|
64
|
+
|
65
|
+
if entry.class == Hash
|
66
|
+
value = entry["value"]
|
67
|
+
created = entry["created"]
|
68
|
+
entry["tags"] = entry["tags"] || []
|
69
|
+
tags = entry["tags"].join(" ")
|
70
|
+
else
|
71
|
+
value = entry
|
72
|
+
created = ""
|
73
|
+
tags = ""
|
74
|
+
end
|
75
|
+
|
76
|
+
rows << [key, value.size, created, tags]
|
77
|
+
# if options[:values]
|
78
|
+
# rows << [key, value, created, tags]
|
79
|
+
# else
|
80
|
+
# rows << [key, value.size, created, tags]
|
81
|
+
# end
|
82
|
+
end
|
83
|
+
|
84
|
+
if empty
|
85
|
+
puts "The can file #{@file} is empty."
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
Tablelize::table(rows) if not options[:short]
|
90
|
+
end
|
91
|
+
|
92
|
+
desc "get KEY", "Copies a KEY to the clipboard"
|
93
|
+
def get(key)
|
94
|
+
init
|
95
|
+
entry = @can.get(key) or abort "Key #{key} does not exist."
|
96
|
+
copy(val(entry))
|
97
|
+
puts "Key #{key} was copied to the clipboard."
|
98
|
+
end
|
99
|
+
|
100
|
+
desc "rename KEY NEW_NAME", "Renames a secret"
|
101
|
+
def rename(key, new_name)
|
102
|
+
init
|
103
|
+
entry = @can.get(key) or abort "Key #{key} does not exist."
|
104
|
+
@can.rename key, new_name
|
105
|
+
puts "Key #{key} was renamed to #{new_name}."
|
106
|
+
end
|
107
|
+
|
108
|
+
desc "password", "Change the can password"
|
109
|
+
def password
|
110
|
+
init
|
111
|
+
pass1 = ask("New password: ")
|
112
|
+
pass2 = ask("Confirm password: ")
|
113
|
+
if pass1 != pass2
|
114
|
+
puts "Passwords don't match."
|
115
|
+
abort
|
116
|
+
end
|
117
|
+
@can.password pass1
|
118
|
+
puts "Can password was changed."
|
119
|
+
end
|
120
|
+
|
121
|
+
desc "set KEY [VALUE]", "Stores a value (empty VALUE show the prompt; use '@random' for a random value)"
|
122
|
+
def set(key, value=nil)
|
123
|
+
init
|
124
|
+
clipboard = false
|
125
|
+
if value == "@random"
|
126
|
+
clipboard = true
|
127
|
+
value = random(60, true)
|
128
|
+
end
|
129
|
+
value = value || ask("Value: ")
|
130
|
+
@can.set key, value
|
131
|
+
if clipboard
|
132
|
+
copy(value)
|
133
|
+
puts "Key #{key} was stored and copied to the clipboard."
|
134
|
+
else
|
135
|
+
puts "Key #{key} was stored."
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
desc "rm KEY", "Removes a key"
|
140
|
+
def rm(key)
|
141
|
+
init
|
142
|
+
@can.exists(key) or abort "Key #{key} does not exist."
|
143
|
+
@can.remove key
|
144
|
+
puts "Key #{key} was deleted."
|
145
|
+
end
|
146
|
+
|
147
|
+
desc "tag KEY TAG", "Tags a key"
|
148
|
+
def tag(key, tag)
|
149
|
+
init
|
150
|
+
@can.exists(key) or abort "Key #{key} does not exist."
|
151
|
+
ok = @can.tag(key, tag)
|
152
|
+
puts "Tag #{tag} was added to #{key}." if ok
|
153
|
+
puts "Tag #{tag} already exists on #{key}." unless ok
|
154
|
+
end
|
155
|
+
|
156
|
+
desc "tags [KEY]", "Show all tags (filter for a key)"
|
157
|
+
def tags(key=nil)
|
158
|
+
init
|
159
|
+
if key
|
160
|
+
@can.exists(key) or abort "Key #{key} does not exist."
|
161
|
+
entry = @can.get(key)
|
162
|
+
tags = entry["tags"] || []
|
163
|
+
else
|
164
|
+
tags = []
|
165
|
+
@can.all().each do |key, entry|
|
166
|
+
entry["tags"] = entry["tags"] || []
|
167
|
+
tags = tags | entry["tags"]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
puts tags.uniq.join("\n")
|
172
|
+
end
|
173
|
+
|
174
|
+
desc "untag KEY TAG", "Untags a tag from a key"
|
175
|
+
def untag(key, tag)
|
176
|
+
init
|
177
|
+
@can.exists(key) or abort "Key #{key} does not exist."
|
178
|
+
ok = @can.untag(key, tag)
|
179
|
+
puts "Tag #{tag} was removed from #{key}." if ok
|
180
|
+
puts "Tag #{tag} doesn't exist on #{key}." unless ok
|
181
|
+
end
|
182
|
+
|
183
|
+
desc "migrate", "Migrates to new format"
|
184
|
+
def migrate()
|
185
|
+
init
|
186
|
+
count = @can.migrate()
|
187
|
+
puts "Keys migrated: #{count}."
|
188
|
+
end
|
189
|
+
|
190
|
+
desc "encrypt DATA", "Encrypts data"
|
191
|
+
def encrypt(data)
|
192
|
+
init
|
193
|
+
puts @can.encrypt(data)
|
194
|
+
end
|
195
|
+
|
196
|
+
desc "decrypt DATA", "Decrypts data"
|
197
|
+
def decrypt(data)
|
198
|
+
init
|
199
|
+
puts @can.decrypt(data)
|
200
|
+
end
|
201
|
+
|
202
|
+
desc "random [LENGTH]", "Generates a random password"
|
203
|
+
option :symbols, :type => :boolean, aliases: "-s"
|
204
|
+
def random(length=60, capture=false)
|
205
|
+
info
|
206
|
+
length = length.to_i
|
207
|
+
chars = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a
|
208
|
+
# chars += ("!".."?").to_a if options[:symbols]
|
209
|
+
chars += %w(! ? # $ @ % & ~ _ - + = : . ; , æ ø å\\ / \( \) { } < >).to_a \
|
210
|
+
if options[:symbols]
|
211
|
+
chars.delete(["'", '"'])
|
212
|
+
# pass = chars.sort_by { rand }.join[0...length]
|
213
|
+
pass = []
|
214
|
+
(1 .. length).each do
|
215
|
+
pass << chars[rand(chars.size)]
|
216
|
+
end
|
217
|
+
pass = pass.join()
|
218
|
+
return pass if capture
|
219
|
+
puts pass
|
220
|
+
end
|
221
|
+
|
222
|
+
# def method_missing name, *args
|
223
|
+
# name = name.to_s
|
224
|
+
# value = args[0]
|
225
|
+
# if value
|
226
|
+
# set name, value
|
227
|
+
# else
|
228
|
+
# get name
|
229
|
+
# end
|
230
|
+
# end
|
231
|
+
|
232
|
+
private
|
233
|
+
def info()
|
234
|
+
# latest = Can::Util::latest()
|
235
|
+
debug "Can version: #{VERSION}"
|
236
|
+
# debug "Latest version: #{latest}"
|
237
|
+
debug "Check files #{FILES}"
|
238
|
+
debug "Check dirs #{DIRS}"
|
239
|
+
debug "Using file #{@file}"
|
240
|
+
debug "Using options #{options}"
|
241
|
+
debug "Using verbose mode (goes to stderr)"
|
242
|
+
end
|
243
|
+
|
244
|
+
def init()
|
245
|
+
@file = options[:file] || ENV.fetch("CAN_FILE", nil)
|
246
|
+
|
247
|
+
if @file == nil or @file.length == 0
|
248
|
+
FILES.each do |file|
|
249
|
+
debug "Checking file #{file}"
|
250
|
+
if File.file?(file)
|
251
|
+
@file = file
|
252
|
+
debug "Using file #{file}"
|
253
|
+
break
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
elsif File.extname(@file) != ".can"
|
258
|
+
DIRS.each do |dir|
|
259
|
+
file = "#{dir}/#{@file}.can"
|
260
|
+
debug "Checking file #{file}"
|
261
|
+
if File.file?(file)
|
262
|
+
@file = file
|
263
|
+
debug "Using file #{file}"
|
264
|
+
break
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|
269
|
+
|
270
|
+
raise "Cound't find a valid can file (#{@file})." if not File.file?(@file)
|
271
|
+
|
272
|
+
info()
|
273
|
+
|
274
|
+
return if check(options[:password], "options")
|
275
|
+
return if check(ENV.fetch("CAN_PASSWORD", nil), "environment")
|
276
|
+
return if check(ask(), "prompt")
|
277
|
+
abort
|
278
|
+
end
|
279
|
+
|
280
|
+
def check(password, source)
|
281
|
+
return if not password or password.length < 1
|
282
|
+
|
283
|
+
@can = Can::Store.new(@file, password)
|
284
|
+
begin
|
285
|
+
@can.all()
|
286
|
+
debug "Opened with password from #{source}"
|
287
|
+
debug "Can format: v#{@can.format()}"
|
288
|
+
return true
|
289
|
+
rescue
|
290
|
+
debug "Failed to open with password from #{source}"
|
291
|
+
raise "Wrong password"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def val(value)
|
296
|
+
return value["value"] if value.class == Hash
|
297
|
+
value
|
298
|
+
end
|
299
|
+
|
300
|
+
def ask(prompt = "Password: ")
|
301
|
+
print prompt
|
302
|
+
answer = STDIN.noecho(&:gets).chomp
|
303
|
+
raise Interrupt if answer.length < 1
|
304
|
+
puts
|
305
|
+
answer
|
306
|
+
end
|
307
|
+
|
308
|
+
def debug(message)
|
309
|
+
show = options[:verbose] || ENV.fetch("CAN_DEBUG", "0") == "1"
|
310
|
+
return if not show
|
311
|
+
STDERR.puts "\033[38;5;240m#{message}\033[0m"
|
312
|
+
end
|
313
|
+
|
314
|
+
def copy(value)
|
315
|
+
IO.popen("pbcopy", "w") { |cc| cc.write(value) }
|
316
|
+
value
|
317
|
+
end
|
318
|
+
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
begin
|
323
|
+
Can::Cli.start ARGV
|
324
|
+
rescue Interrupt
|
325
|
+
puts
|
326
|
+
exit 2
|
327
|
+
rescue Exception => e
|
328
|
+
puts "Error: #{e}"
|
329
|
+
exit 1
|
330
|
+
end
|
data/can.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require "can"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "can"
|
8
|
+
spec.version = Can::VERSION
|
9
|
+
spec.summary = "Can stores encrypted goods using symmetric cryptography."
|
10
|
+
# spec.description = File.read("readme.md")
|
11
|
+
spec.description = "Can stores encrypted goods using symmetric cryptography (AES-256-CBC)"
|
12
|
+
spec.homepage = "https://github.com/ptdorf/can"
|
13
|
+
spec.authors = ["ptdorf"]
|
14
|
+
spec.email = ["ptdorf@gmail.com"]
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split $/
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename f }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "thor"
|
22
|
+
spec.add_dependency "tablelize"
|
23
|
+
spec.add_dependency "base62-rb"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
|
26
|
+
# spec.required_ruby_version = "~> 2.4"
|
27
|
+
end
|
data/lib/can.rb
ADDED
data/lib/can/crypto.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "digest/sha1"
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module Can
|
6
|
+
class Crypto
|
7
|
+
|
8
|
+
CIPHER = "AES-256-CBC"
|
9
|
+
|
10
|
+
def self.encrypt(content, password)
|
11
|
+
digest = Digest::SHA1.hexdigest(password)
|
12
|
+
cipher = OpenSSL::Cipher.new(CIPHER)
|
13
|
+
cipher.encrypt
|
14
|
+
cipher.key = digest[0..31]
|
15
|
+
cipher.iv = iv = cipher.random_iv
|
16
|
+
encrypted = cipher.update(content) + cipher.final
|
17
|
+
|
18
|
+
binit = Base64.strict_encode64(iv)
|
19
|
+
ilen = binit.length.to_s
|
20
|
+
blen = Base64.strict_encode64(ilen)
|
21
|
+
bdata = Base64.strict_encode64(encrypted)
|
22
|
+
# blen + "--" + binit + "--" + bdata
|
23
|
+
binit + "--" + bdata
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.decrypt(content, password)
|
27
|
+
digest = Digest::SHA1.hexdigest(password)
|
28
|
+
init, encrypted = content.split("--").map do |v|
|
29
|
+
Base64.strict_decode64(v)
|
30
|
+
end
|
31
|
+
|
32
|
+
cipher = OpenSSL::Cipher.new(CIPHER)
|
33
|
+
cipher.decrypt
|
34
|
+
cipher.key = digest[0..31]
|
35
|
+
cipher.iv = init
|
36
|
+
|
37
|
+
cipher.update(encrypted) + cipher.final
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
data/lib/can/store.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
require "json"
|
2
|
+
require "time"
|
3
|
+
# require "zlib"
|
4
|
+
|
5
|
+
module Can
|
6
|
+
class Store
|
7
|
+
|
8
|
+
HEADER = "Can"
|
9
|
+
SEPARATOR = "\n\n"
|
10
|
+
|
11
|
+
def initialize(file, password)
|
12
|
+
@password = password
|
13
|
+
@file = file
|
14
|
+
@format = "1"
|
15
|
+
end
|
16
|
+
|
17
|
+
def all()
|
18
|
+
read()
|
19
|
+
end
|
20
|
+
|
21
|
+
def format()
|
22
|
+
@format
|
23
|
+
end
|
24
|
+
|
25
|
+
def exists(key)
|
26
|
+
data = read()
|
27
|
+
data[key] ? true : false
|
28
|
+
end
|
29
|
+
|
30
|
+
def get(key)
|
31
|
+
data = read()
|
32
|
+
data[key] || nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def set(key, value)
|
36
|
+
data = read()
|
37
|
+
data[key] ||= {}
|
38
|
+
data[key]["value"] = value
|
39
|
+
data[key]["created"] = Time.new
|
40
|
+
data[key]["tags"] ||= []
|
41
|
+
write(data)
|
42
|
+
end
|
43
|
+
|
44
|
+
def rename(key, new_key)
|
45
|
+
data = read()
|
46
|
+
data[new_key] = data[key]
|
47
|
+
data.delete(key)
|
48
|
+
write(data)
|
49
|
+
end
|
50
|
+
|
51
|
+
def tag(key, tag)
|
52
|
+
data = read()
|
53
|
+
data[key]["tags"] = data[key]["tags"] || []
|
54
|
+
if not data[key]["tags"].include?(tag)
|
55
|
+
data[key]["tags"] << tag
|
56
|
+
return write(data)
|
57
|
+
end
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def untag(key, tag)
|
62
|
+
data = read()
|
63
|
+
if data[key]["tags"] and data[key]["tags"].include?(tag)
|
64
|
+
data[key]["tags"].delete(tag)
|
65
|
+
return write(data)
|
66
|
+
end
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove(key)
|
71
|
+
data = read()
|
72
|
+
data.delete(key)
|
73
|
+
write(data)
|
74
|
+
end
|
75
|
+
|
76
|
+
def encrypt(payload)
|
77
|
+
encrypted = Crypto.encrypt(payload, @password)
|
78
|
+
encode(encrypted)
|
79
|
+
end
|
80
|
+
|
81
|
+
def decrypt(payload)
|
82
|
+
decoded = decode(payload)
|
83
|
+
Crypto.decrypt(decoded, @password)
|
84
|
+
end
|
85
|
+
|
86
|
+
def password(new_password)
|
87
|
+
data = read()
|
88
|
+
@password = new_password
|
89
|
+
write(data)
|
90
|
+
end
|
91
|
+
|
92
|
+
def migrate()
|
93
|
+
count = 0
|
94
|
+
data = read()
|
95
|
+
data.each do |key, value|
|
96
|
+
# puts "Checking key #{key}..."
|
97
|
+
if value.class != Hash
|
98
|
+
data[key] = {}
|
99
|
+
data[key]["value"] = value
|
100
|
+
data[key]["created"] = Time.new
|
101
|
+
data[key]["tags"] = []
|
102
|
+
count += 1
|
103
|
+
puts "Key #{key} migrated to new format"
|
104
|
+
else
|
105
|
+
puts "Key #{key} already exists in new format"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
write(data)
|
110
|
+
count
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
def read()
|
115
|
+
return {} unless File.exist?(@file)
|
116
|
+
|
117
|
+
content = File.read(@file)
|
118
|
+
headless = rm_header(content)
|
119
|
+
cleaned = clean(headless)
|
120
|
+
decoded = decode(cleaned)
|
121
|
+
decrypted = Crypto.decrypt(decoded, @password)
|
122
|
+
|
123
|
+
JSON.parse(decrypted)
|
124
|
+
end
|
125
|
+
|
126
|
+
def write(data)
|
127
|
+
payload = JSON.dump(data)
|
128
|
+
encrypted = Crypto.encrypt(payload, @password)
|
129
|
+
encoded = encode(encrypted)
|
130
|
+
aligned = align(encoded)
|
131
|
+
content = add_header(aligned)
|
132
|
+
|
133
|
+
File.write(@file, content)
|
134
|
+
end
|
135
|
+
|
136
|
+
def encode(data)
|
137
|
+
data.unpack("H*").first
|
138
|
+
end
|
139
|
+
|
140
|
+
def decode(data)
|
141
|
+
data.scan(/../).map { |x| x.hex }.pack("c*")
|
142
|
+
end
|
143
|
+
|
144
|
+
# def compress(data)
|
145
|
+
# Zlib::Deflate.deflate(data)
|
146
|
+
# end
|
147
|
+
|
148
|
+
# def uncompress(data)
|
149
|
+
# Zlib::Inflate.inflate(data)
|
150
|
+
# end
|
151
|
+
|
152
|
+
def align(data)
|
153
|
+
data.scan(/.{1,64}/).join("\n")
|
154
|
+
end
|
155
|
+
|
156
|
+
def clean(data)
|
157
|
+
data.split("\n").join("")
|
158
|
+
end
|
159
|
+
|
160
|
+
def add_header(payload)
|
161
|
+
"#{HEADER}:v#{@format}#{SEPARATOR}#{payload}"
|
162
|
+
end
|
163
|
+
|
164
|
+
def rm_header(payload)
|
165
|
+
parts = payload.split(SEPARATOR)
|
166
|
+
header = parts[0]
|
167
|
+
body = parts[1]
|
168
|
+
m = header.match(/Can\:v(\d+)/)
|
169
|
+
@format = m[1]
|
170
|
+
body
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
data/lib/can/util.rb
ADDED
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: can
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.10.13
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ptdorf
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-09-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: tablelize
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: base62-rb
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Can stores encrypted goods using symmetric cryptography (AES-256-CBC)
|
70
|
+
email:
|
71
|
+
- ptdorf@gmail.com
|
72
|
+
executables:
|
73
|
+
- can
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- Gemfile
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- bin/can
|
82
|
+
- can.gemspec
|
83
|
+
- lib/can.rb
|
84
|
+
- lib/can/crypto.rb
|
85
|
+
- lib/can/store.rb
|
86
|
+
- lib/can/util.rb
|
87
|
+
homepage: https://github.com/ptdorf/can
|
88
|
+
licenses:
|
89
|
+
- MIT
|
90
|
+
metadata: {}
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubygems_version: 3.0.3
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Can stores encrypted goods using symmetric cryptography.
|
110
|
+
test_files: []
|