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 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