mellon 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/bin/mellon +25 -4
- data/lib/mellon.rb +9 -0
- data/lib/mellon/keychain.rb +36 -76
- data/lib/mellon/utils.rb +92 -0
- data/lib/mellon/version.rb +1 -1
- data/spec/mellon/keychain_spec.rb +23 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: def326c20e1ca30c9928c4832d30685545243fc0
|
4
|
+
data.tar.gz: 21b5882854d42a868ae1dd13238f230f4d2894b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 148f9e5dab12df775f0635d07f7f5da20b0d6f1c9361e8b8b757aa9a130f8a813634f48a2c5b9d4bbe5b2ef330c894fedc5d4fad7edf416e3fb355f124c108bd
|
7
|
+
data.tar.gz: 75c894dcf7e31e103c29d0c049d9adb4820cc1984b9a2c4d5a76a3fa5588d5cebfb0df892f668cd589ed168e4d0581a997797281ee8556b9f70e110a59b03dd7
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
[HEAD][]
|
2
|
+
--------
|
3
|
+
|
4
|
+
[v1.1.0][]
|
5
|
+
----------
|
6
|
+
|
7
|
+
- `mellon list -k keychain` now lists all keys in given keychain (d34052c0)
|
8
|
+
- `mellon list` now lists all keys in all keychains (e9d67f10)
|
9
|
+
- Implemented Mellon::Keychain#keys (7ee9c3fe)
|
10
|
+
- Implemented equality checking for Mellon::Keychain (4368c73c)
|
11
|
+
|
12
|
+
[v1.0.0][]
|
13
|
+
----------
|
14
|
+
|
15
|
+
Initial release!
|
16
|
+
|
17
|
+
[HEAD]: https://github.com/elabs/mellon/compare/v1.1.0...HEAD
|
18
|
+
[v1.1.0]: https://github.com/elabs/mellon/compare/v1.0.0...v1.1.0
|
19
|
+
[v1.0.0]: https://github.com/elabs/mellon/compare/24b83977d...v1.0.0
|
data/bin/mellon
CHANGED
@@ -55,12 +55,33 @@ Slop.parse(strict: true, help: true) do
|
|
55
55
|
exit
|
56
56
|
end
|
57
57
|
|
58
|
-
description "list
|
58
|
+
description "list keychain entries."
|
59
59
|
command "list" do
|
60
|
+
banner "Usage: mellon list [options]"
|
61
|
+
define_common[self]
|
62
|
+
|
60
63
|
run do
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
+
if $keychain.nil?
|
65
|
+
entries = {}
|
66
|
+
Mellon::Keychain.list.map do |keychain|
|
67
|
+
keys = keychain.keys
|
68
|
+
entries[keychain] = keys if keys.length > 0
|
69
|
+
end
|
70
|
+
|
71
|
+
if entries.empty?
|
72
|
+
puts "There are no keychains with entries."
|
73
|
+
else
|
74
|
+
puts entries.map { |keychain, entries|
|
75
|
+
joiner = "\n "
|
76
|
+
"#{keychain.path}:#{joiner}" << entries.join(joiner)
|
77
|
+
}.join("\n\n")
|
78
|
+
end
|
79
|
+
else
|
80
|
+
joiner = "\n "
|
81
|
+
keychain = $keychain
|
82
|
+
entries = keychain.keys
|
83
|
+
|
84
|
+
puts "#{keychain.path}:#{joiner}" << entries.join(joiner)
|
64
85
|
end
|
65
86
|
end
|
66
87
|
end
|
data/lib/mellon.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "mellon/version"
|
2
|
+
require "mellon/utils"
|
2
3
|
require "mellon/shell_utils"
|
3
4
|
require "mellon/keychain"
|
4
5
|
require "mellon/store"
|
@@ -6,6 +7,14 @@ require "mellon/store"
|
|
6
7
|
module Mellon
|
7
8
|
KEYCHAIN_REGEXP = /"(.+)"/
|
8
9
|
|
10
|
+
DEFAULT_OPTIONS = { type: :note }
|
11
|
+
TYPES = {
|
12
|
+
"note" => {
|
13
|
+
kind: "secure note",
|
14
|
+
type: "note"
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
9
18
|
class Error < StandardError; end
|
10
19
|
class CommandError < Error; end
|
11
20
|
end
|
data/lib/mellon/keychain.rb
CHANGED
@@ -3,14 +3,6 @@ require "plist"
|
|
3
3
|
module Mellon
|
4
4
|
# Keychain provides simple methods for reading and storing keychain entries.
|
5
5
|
class Keychain
|
6
|
-
DEFAULT_OPTIONS = { type: :note }
|
7
|
-
TYPES = {
|
8
|
-
"note" => {
|
9
|
-
kind: "secure note",
|
10
|
-
type: "note"
|
11
|
-
}
|
12
|
-
}
|
13
|
-
|
14
6
|
class << self
|
15
7
|
# Find the first keychain that contains the key.
|
16
8
|
#
|
@@ -46,13 +38,13 @@ module Mellon
|
|
46
38
|
# @return [Keychain] default keychain
|
47
39
|
def default
|
48
40
|
keychain_path = ShellUtils.security("default-keychain")[KEYCHAIN_REGEXP, 1]
|
49
|
-
|
41
|
+
new(keychain_path, ensure_exists: false)
|
50
42
|
end
|
51
43
|
|
52
44
|
# @return [Array<Keychain>] all available keychains
|
53
45
|
def list
|
54
46
|
ShellUtils.security("list-keychains").scan(KEYCHAIN_REGEXP).map do |(keychain_path)|
|
55
|
-
|
47
|
+
new(keychain_path, ensure_exists: false)
|
56
48
|
end
|
57
49
|
end
|
58
50
|
end
|
@@ -107,6 +99,36 @@ module Mellon
|
|
107
99
|
end
|
108
100
|
end
|
109
101
|
|
102
|
+
# Retrieve all available keys.
|
103
|
+
#
|
104
|
+
# @return [Array<String>]
|
105
|
+
def keys
|
106
|
+
Utils.parse_dump(command "dump-keychain").map do |keychain, info|
|
107
|
+
info[:label]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# @return a hash unique to keychains of the same path
|
112
|
+
def hash
|
113
|
+
path.hash
|
114
|
+
end
|
115
|
+
|
116
|
+
# @param other
|
117
|
+
# @return [Boolean] true if the keychains have the same path
|
118
|
+
def eql?(other)
|
119
|
+
self == other or super
|
120
|
+
end
|
121
|
+
|
122
|
+
# @param other
|
123
|
+
# @return [Boolean] true if the keychains have the same path
|
124
|
+
def ==(other)
|
125
|
+
if other.is_a?(Keychain)
|
126
|
+
path == other.path
|
127
|
+
else
|
128
|
+
super
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
110
132
|
private
|
111
133
|
|
112
134
|
# Read a key from the keychain.
|
@@ -115,7 +137,7 @@ module Mellon
|
|
115
137
|
# @return [Array<Hash, String>, nil] tuple of entry info, and text contents, or nil if key does not exist
|
116
138
|
def read(key)
|
117
139
|
command "find-generic-password", "-g", "-l", key do |info, password_info|
|
118
|
-
[parse_info(info), parse_contents(password_info)]
|
140
|
+
[Utils.parse_info(info), Utils.parse_contents(password_info)]
|
119
141
|
end
|
120
142
|
rescue CommandError => e
|
121
143
|
nil
|
@@ -131,13 +153,13 @@ module Mellon
|
|
131
153
|
# @param [String] key
|
132
154
|
# @param [String] data
|
133
155
|
# @param [Hash] options
|
134
|
-
# @option options [#to_s] :type (:note) one of
|
156
|
+
# @option options [#to_s] :type (:note) one of Mellon::TYPES
|
135
157
|
# @option options [String] :account_name ("")
|
136
158
|
# @option options [String] :service_name (key)
|
137
159
|
# @option options [String] :label (service_name)
|
138
160
|
# @raise [CommandError] if writing fails
|
139
161
|
def write(key, data, options = {})
|
140
|
-
info = build_info(key, options)
|
162
|
+
info = Utils.build_info(key, options)
|
141
163
|
|
142
164
|
command "add-generic-password",
|
143
165
|
"-a", info[:account_name],
|
@@ -156,7 +178,7 @@ module Mellon
|
|
156
178
|
# @param [Hash] options
|
157
179
|
# @option (see #write)
|
158
180
|
def delete(key, options = {})
|
159
|
-
info = build_info(key, options)
|
181
|
+
info = Utils.build_info(key, options)
|
160
182
|
|
161
183
|
command "delete-generic-password",
|
162
184
|
"-a", info[:account_name],
|
@@ -173,67 +195,5 @@ module Mellon
|
|
173
195
|
command += [path]
|
174
196
|
ShellUtils.security *command, &block
|
175
197
|
end
|
176
|
-
|
177
|
-
private
|
178
|
-
|
179
|
-
# Build an info hash used for #write and #delete.
|
180
|
-
#
|
181
|
-
# @param [String] key
|
182
|
-
# @param [Hash] options
|
183
|
-
# @return [Hash]
|
184
|
-
def build_info(key, options = {})
|
185
|
-
options = DEFAULT_OPTIONS.merge(options)
|
186
|
-
|
187
|
-
note_type = TYPES.fetch(options.fetch(:type, :note).to_s)
|
188
|
-
account_name = options.fetch(:account_name, "")
|
189
|
-
service_name = options.fetch(:service_name, key)
|
190
|
-
label = options.fetch(:label, service_name)
|
191
|
-
|
192
|
-
{
|
193
|
-
account_name: account_name,
|
194
|
-
service_name: service_name,
|
195
|
-
label: label,
|
196
|
-
kind: note_type.fetch(:kind),
|
197
|
-
type: note_type.fetch(:type),
|
198
|
-
}
|
199
|
-
end
|
200
|
-
|
201
|
-
# Parse entry information.
|
202
|
-
#
|
203
|
-
# @param [String] info
|
204
|
-
# @return [Hash]
|
205
|
-
def parse_info(info)
|
206
|
-
extract = lambda { |key| info[/#{key}.+=(?:<NULL>|[^"]*"(.+)")/, 1].to_s }
|
207
|
-
{
|
208
|
-
account_name: extract["acct"],
|
209
|
-
kind: extract["desc"],
|
210
|
-
type: extract["type"],
|
211
|
-
label: extract["0x00000007"],
|
212
|
-
service_name: extract["svce"],
|
213
|
-
}
|
214
|
-
end
|
215
|
-
|
216
|
-
# Parse entry contents.
|
217
|
-
#
|
218
|
-
# @param [String]
|
219
|
-
# @return [String]
|
220
|
-
def parse_contents(password_info)
|
221
|
-
unpacked = password_info[/password: 0x([a-f0-9]+)/i, 1]
|
222
|
-
|
223
|
-
password = if unpacked
|
224
|
-
[unpacked].pack("H*")
|
225
|
-
else
|
226
|
-
password_info[/password: "(.+)"/m, 1]
|
227
|
-
end
|
228
|
-
|
229
|
-
password ||= ""
|
230
|
-
|
231
|
-
parsed = Plist.parse_xml(password.force_encoding("".encoding))
|
232
|
-
if parsed and parsed["NOTE"]
|
233
|
-
parsed["NOTE"]
|
234
|
-
else
|
235
|
-
password
|
236
|
-
end
|
237
|
-
end
|
238
198
|
end
|
239
199
|
end
|
data/lib/mellon/utils.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module Mellon
|
2
|
+
module Utils
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# Build an entry info hash.
|
6
|
+
#
|
7
|
+
# @param [String] key
|
8
|
+
# @param [Hash] options
|
9
|
+
# @return [Hash]
|
10
|
+
def build_info(key, options = {})
|
11
|
+
options = DEFAULT_OPTIONS.merge(options)
|
12
|
+
|
13
|
+
note_type = TYPES.fetch(options.fetch(:type, :note).to_s)
|
14
|
+
account_name = options.fetch(:account_name, "")
|
15
|
+
service_name = options.fetch(:service_name, key)
|
16
|
+
label = options.fetch(:label, service_name)
|
17
|
+
|
18
|
+
{
|
19
|
+
account_name: account_name,
|
20
|
+
service_name: service_name,
|
21
|
+
label: label,
|
22
|
+
kind: note_type.fetch(:kind),
|
23
|
+
type: note_type.fetch(:type),
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [String]
|
28
|
+
# @return [Array<[keychain_path, info]>]
|
29
|
+
def parse_dump(keychain_dump)
|
30
|
+
attributes_start = /attributes:/
|
31
|
+
keychain_start = /keychain: #{KEYCHAIN_REGEXP}/
|
32
|
+
|
33
|
+
keychain_path = nil
|
34
|
+
state = :ignoring
|
35
|
+
info_chunks = keychain_dump.each_line.chunk do |line|
|
36
|
+
if line =~ attributes_start
|
37
|
+
state = :attributes
|
38
|
+
nil
|
39
|
+
elsif line =~ keychain_start
|
40
|
+
state = :ignoring
|
41
|
+
keychain_path = $1
|
42
|
+
nil
|
43
|
+
elsif state == :attributes
|
44
|
+
keychain_path
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
info_chunks.each_with_object([]) do |(keychain_path, chunk), keys|
|
49
|
+
info = parse_info(chunk.join)
|
50
|
+
keys << [keychain_path, info] if TYPES.has_key?(info[:type])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Parse entry information.
|
55
|
+
#
|
56
|
+
# @param [String] info
|
57
|
+
# @return [Hash]
|
58
|
+
def parse_info(info)
|
59
|
+
extract = lambda { |key| info[/#{key}.+=(?:<NULL>|[^"]*"(.+)")/, 1].to_s }
|
60
|
+
{
|
61
|
+
account_name: extract["acct"],
|
62
|
+
kind: extract["desc"],
|
63
|
+
type: extract["type"],
|
64
|
+
label: extract["0x00000007"],
|
65
|
+
service_name: extract["svce"],
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Parse entry contents.
|
70
|
+
#
|
71
|
+
# @param [String]
|
72
|
+
# @return [String]
|
73
|
+
def parse_contents(password_string)
|
74
|
+
unpacked = password_string[/password: 0x([a-f0-9]+)/i, 1]
|
75
|
+
|
76
|
+
password = if unpacked
|
77
|
+
[unpacked].pack("H*")
|
78
|
+
else
|
79
|
+
password_string[/password: "(.+)"/m, 1]
|
80
|
+
end
|
81
|
+
|
82
|
+
password ||= ""
|
83
|
+
|
84
|
+
parsed = Plist.parse_xml(password.force_encoding("".encoding))
|
85
|
+
if parsed and parsed["NOTE"]
|
86
|
+
parsed["NOTE"]
|
87
|
+
else
|
88
|
+
password
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/mellon/version.rb
CHANGED
@@ -11,13 +11,35 @@ describe Mellon::Keychain do
|
|
11
11
|
keychain.path.should eq keychain_path
|
12
12
|
end
|
13
13
|
|
14
|
+
specify "keychain can be stored in hash" do
|
15
|
+
hash = {}
|
16
|
+
hash[keychain] = "some value"
|
17
|
+
hash[Mellon::Keychain.new(keychain.path)].should eq "some value"
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#==" do
|
21
|
+
it "is equal to another keychain with same path" do
|
22
|
+
keychain.should eq Mellon::Keychain.new(keychain.path)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "is not equal to any other object" do
|
26
|
+
keychain.should_not eq({})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
14
30
|
describe "#initialize" do
|
15
31
|
it "raises an error if keychain does not exist" do
|
16
32
|
expect { Mellon::Keychain.new("missing.keychain") }.to raise_error(Mellon::Error, /missing.keychain/)
|
17
33
|
end
|
18
34
|
end
|
19
35
|
|
20
|
-
describe "
|
36
|
+
describe "#keys" do
|
37
|
+
it "lists all keys available in the keychain" do
|
38
|
+
keychain.keys.should =~ ["simple", "existing", "encoded", "plist", "empty", "doomed", "json store", "yaml store"]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#fetch" do
|
21
43
|
it "delegates (and as such, behaves equally) to #[]" do
|
22
44
|
keychain.should_receive(:[]).with("simple").and_call_original
|
23
45
|
keychain.fetch("simple").should eq "Simple note"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mellon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kim Burgestrand
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-04-
|
11
|
+
date: 2014-04-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: plist
|
@@ -104,6 +104,7 @@ extra_rdoc_files: []
|
|
104
104
|
files:
|
105
105
|
- .gitignore
|
106
106
|
- .rspec
|
107
|
+
- CHANGELOG.md
|
107
108
|
- Gemfile
|
108
109
|
- LICENSE
|
109
110
|
- README.md
|
@@ -114,6 +115,7 @@ files:
|
|
114
115
|
- lib/mellon/keychain.rb
|
115
116
|
- lib/mellon/shell_utils.rb
|
116
117
|
- lib/mellon/store.rb
|
118
|
+
- lib/mellon/utils.rb
|
117
119
|
- lib/mellon/version.rb
|
118
120
|
- mellon.gemspec
|
119
121
|
- spec/keychain.keychain
|