mellon 1.0.0 → 1.1.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 +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
|