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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3d709112b62b9ed79d5d650602042faf2a3dfc2f
4
- data.tar.gz: deda5ba7ba3a00ea77b3f914fd23c8d14cd7c04a
3
+ metadata.gz: def326c20e1ca30c9928c4832d30685545243fc0
4
+ data.tar.gz: 21b5882854d42a868ae1dd13238f230f4d2894b1
5
5
  SHA512:
6
- metadata.gz: ede7a998d84a0504c55bcf1f203c832a8c800e647f43ff4c17a7edfb1801ee2e37292f5a771e04d97798985af4c2e0c32d9507dc7c5428d73acc6ed6ea38a45d
7
- data.tar.gz: 19fef434f6cdd5a71599f9774a4d56e284ae82d51d819b2a001de0626da9d237ada8eaef8e89e4569d3fb91877d732169c9cefe0429eea11ae3524794bdacb70
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 globally known keychains."
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
- puts "Available keychains:"
62
- Mellon::Keychain.list.each do |keychain|
63
- puts " #{keychain.name}"
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
@@ -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
- Keychain.new(keychain_path, ensure_exists: false)
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
- Keychain.new(keychain_path, ensure_exists: false)
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 Keychain::TYPES
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Mellon
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -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 "fetch" do
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.0.0
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-09 00:00:00.000000000 Z
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