noms-command 0.5.0 → 2.1.1

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.
@@ -0,0 +1,72 @@
1
+ var nomsargs = { };
2
+
3
+ (function (self) {
4
+
5
+ function NomsArgs(args, opt) {
6
+ var comparisons = [ ];
7
+ var assignment = { };
8
+ var warnings = [ ];
9
+ var errors = [ ];
10
+ var extra = [ ];
11
+
12
+ args.map(function (item) {
13
+ var match = /^([^!=~><]+)(==|=|!=|>=|<=|~|!~)(.*)$/.exec(item)
14
+ if (match == null) {
15
+ extra.push(item);
16
+ } else {
17
+ var m_field = match[1];
18
+ var m_op = match[2];
19
+ var m_rvalue = match[3];
20
+
21
+ comparisons.push({
22
+ field: m_field,
23
+ op: m_op,
24
+ rvalue: m_rvalue
25
+ });
26
+
27
+ if (m_op == '=') {
28
+ assignment[m_field] = m_rvalue
29
+ }
30
+ }
31
+ });
32
+
33
+ this.comparisons = comparisons;
34
+ this.assignment = assignment;
35
+ this.warnings = warnings;
36
+ this.errors = errors;
37
+ this.extra = extra;
38
+ };
39
+
40
+ NomsArgs.prototype = {
41
+ keys: function () {
42
+ this.comparisons.map(function (comp) { return comp.field; });
43
+ },
44
+ assignmentKeys: function () {
45
+ var keys = [ ];
46
+
47
+ for (var key in this.assignment) {
48
+ if (this.assignment.hasOwnProperty(key)) {
49
+ keys.push(key);
50
+ }
51
+ }
52
+
53
+ return keys;
54
+ },
55
+ query: function () {
56
+ return encodeURI(this.comparisons.map(function (comp) {
57
+ return comp.field + comp.op + comp.rvalue;
58
+ }).join('&'));
59
+ },
60
+ assignmentQuery: function () {
61
+ return encodeURI(this.assignmentKeys().map(function (key) {
62
+ return key + '=' + this.assignment[key];
63
+ }).join('&'))
64
+ },
65
+ keys: function () {
66
+ return this.comparisons.map(function (comp) { comp.field });
67
+ }
68
+ };
69
+
70
+ self.NomsArgs = NomsArgs;
71
+
72
+ })(nomsargs);
data/lib/noms/command.rb CHANGED
@@ -1,13 +1,22 @@
1
1
  #!ruby
2
2
 
3
3
  require 'noms/command/version'
4
+ require 'noms/command/home'
4
5
 
5
6
  require 'trollop'
6
7
  require 'logger'
7
8
 
9
+ require 'noms/command/base'
10
+ require 'noms/command/useragent'
11
+
8
12
  require 'noms/command/window'
13
+ require 'noms/command/xmlhttprequest'
14
+ require 'noms/command/document'
15
+
9
16
  require 'noms/command/application'
10
17
  require 'noms/command/formatter'
18
+ require 'noms/command/auth'
19
+ require 'noms/command/auth/identity'
11
20
 
12
21
  class NOMS
13
22
 
@@ -23,7 +32,7 @@ class NOMS::Command
23
32
  def initialize(argv)
24
33
  @argv = argv
25
34
  @log = Logger.new($stderr)
26
- @log.formatter = lambda { |sev, timestamp, prog, msg| msg[-1].chr == "\n" ? msg : msg + "\n" }
35
+ @log.formatter = lambda { |sev, timestamp, prog, msg| (msg.empty? or msg[-1].chr != "\n") ? msg + "\n" : msg }
27
36
  end
28
37
 
29
38
  def run
@@ -36,16 +45,26 @@ class NOMS::Command
36
45
  noms [noms-options] { bookmark | url } [options] [arguments]
37
46
  noms-options:
38
47
  USAGE
39
- opt :identity, "Identity file", :type => :string, :multi => true
40
- opt :verbose, "Enable verbose output"
41
- opt :list, "List bookmarks"
48
+ opt :identity, "Identity file", :short => '-i',
49
+ :type => :string,
50
+ :multi => true
51
+ opt :logout, "Log out of authentication sessions", :short => '-L'
52
+ opt :verbose, "Enable verbose output", :short => '-v'
53
+ opt :list, "List bookmarks", :short => '-l'
42
54
  opt :bookmarks, "Bookmark file location (can be specified multiple times)",
55
+ :short => '-b',
43
56
  :type => :string,
44
57
  :multi => true
58
+ opt :home, "Use directory as NOMS_HOME instead of #{NOMS::Command.home}",
59
+ :short => '-H',
60
+ :type => :string
61
+ opt :nocache, "Don't cache files",
62
+ :short => '-C'
45
63
  opt :nodefault_bookmarks, "Don't consult default bookmarks files",
46
64
  :short => '-X',
47
65
  :long => '--nodefault-bookmarks'
48
- opt :debug, "Enable debug output"
66
+ opt :debug, "Enable debug output", :short => '-d'
67
+ opt :'plaintext-identity', "Save identity credentials in plaintext", :short => '-P'
49
68
  stop_on_unknown
50
69
  end
51
70
 
@@ -53,14 +72,16 @@ class NOMS::Command
53
72
  parser.parse(@argv)
54
73
  end
55
74
 
75
+ NOMS::Command.home = @opt[:home] if @opt[:home]
76
+
56
77
  Trollop::with_standard_exception_handling parser do
57
- raise Trollop::HelpNeeded if @argv.empty? and ! @opt[:list]
78
+ raise Trollop::HelpNeeded if @argv.empty? and ! @opt[:list] and ! @opt[:logout]
58
79
  end
59
80
 
60
81
  @opt[:debug] = true if ENV['NOMS_DEBUG'] and ! ENV['NOMS_DEBUG'].empty?
61
82
 
62
83
  default_bookmarks =
63
- [ File.join(ENV['HOME'], '.noms/bookmarks.json'),
84
+ [ File.join(NOMS::Command.home, 'bookmarks.json'),
64
85
  '/usr/local/etc/noms/bookmarks.json',
65
86
  '/etc/noms/bookmarks.json'].select { |f| File.exist? f }
66
87
 
@@ -89,10 +110,18 @@ class NOMS::Command
89
110
  return 0
90
111
  end
91
112
 
113
+ if @opt[:logout]
114
+ File.unlink NOMS::Command::Auth::Identity.vault_keyfile if
115
+ File.exist? NOMS::Command::Auth::Identity.vault_keyfile
116
+ return 0
117
+ end
118
+
92
119
  begin
93
120
  origin = @bookmark[@argv[0].split('/').first] || @argv[0]
94
121
  app = NOMS::Command::Application.new(origin, @argv, :logger => @log,
95
- :specified_identities => @opt[:identity])
122
+ :specified_identities => @opt[:identity],
123
+ :cache => ! @opt[:nocache],
124
+ :plaintext_identity => @opt[:'plaintext-identity'])
96
125
  app.fetch! # Retrieve page
97
126
  app.render! # Run scripts
98
127
  out = app.display
@@ -5,14 +5,7 @@ require 'noms/command/version'
5
5
  require 'mime-types'
6
6
  require 'v8'
7
7
 
8
- require 'noms/command/window'
9
- require 'noms/command/useragent'
10
- require 'noms/command/error'
11
- require 'noms/command/urinion'
12
- require 'noms/command/formatter'
13
- require 'noms/command/document'
14
- require 'noms/command/xmlhttprequest'
15
- require 'noms/command/base'
8
+ require 'noms/command'
16
9
 
17
10
  class NOMS
18
11
 
@@ -45,7 +38,10 @@ class NOMS::Command::Application < NOMS::Command::Base
45
38
 
46
39
  @log.debug "Application #{argv[0]} has origin: #{origin}"
47
40
  @useragent = NOMS::Command::UserAgent.new(@origin, :logger => @log,
48
- :specified_identities => (attrs[:specified_identities] || []))
41
+ :specified_identities => (attrs[:specified_identities] || []),
42
+ :cache => (attrs.has_key?(:cache) ? attrs[:cache] : true),
43
+ :plaintext_identity => (attrs.has_key?(:plaintext_identity) ?
44
+ attrs[:plaintext_identity] : false))
49
45
  end
50
46
 
51
47
  def fetch!
@@ -64,15 +60,14 @@ class NOMS::Command::Application < NOMS::Command::Base
64
60
  @origin = new_url
65
61
  @useragent.origin = new_url
66
62
  @window.origin = new_url
67
- @log.debug "Setting origin to: #{@origin}"
68
- if response.ok?
63
+ if response.success?
69
64
  # Unlike typical ReST data sources, this
70
65
  # should very rarely fail unless there is
71
66
  # a legitimate communication issue.
72
- @type = response.contenttype || 'text/plain'
73
- @body = response.content
67
+ @type = response.content_type || 'text/plain'
68
+ @body = response.body
74
69
  else
75
- raise NOMS::Command::Error.new("Failed to request #{@origin}: #{response.status} #{response.reason}")
70
+ raise NOMS::Command::Error.new("Failed to request #{@origin}: #{response.statusText}")
76
71
  end
77
72
  else
78
73
  raise NOMS::Command::Error.new("noms command #{@argv[0].inspect} not found: not a URL or bookmark")
@@ -87,12 +82,10 @@ class NOMS::Command::Application < NOMS::Command::Base
87
82
  end
88
83
  if @body.respond_to? :has_key? and @body.has_key? '$doctype'
89
84
  @type = @body['$doctype']
90
- @log.debug "Treating as #{@type} document"
91
85
  @document = NOMS::Command::Document.new @body
92
86
  @document.argv = @argv
93
87
  @document.exitcode = 0
94
88
  else
95
- @log.debug "Treating as raw object (no '$doctype')"
96
89
  @type = 'noms-raw'
97
90
  end
98
91
  end
@@ -122,26 +115,38 @@ class NOMS::Command::Application < NOMS::Command::Base
122
115
  @document.script.each do |script|
123
116
  if script.respond_to? :has_key? and script.has_key? '$source'
124
117
  # Parse relative URL and load
125
- response, landing_url = @useragent.get(script['$source'])
118
+ request_error = nil
119
+ begin
120
+ response, landing_url = @useragent.get(script['$source'])
121
+ rescue StandardError => e
122
+ @log.debug "Setting request_error (#{e.class}) to #{e.message})"
123
+ request_error = e
124
+ end
126
125
  # Don't need landing_url
127
126
  script_name = File.basename(@useragent.absolute_url(script['$source']).path)
128
127
  script_ref = "#{script_index},#{script_name}"
129
- if response.ok?
130
- case response.contenttype
128
+ if request_error.nil? and response.success?
129
+ case response.content_type
131
130
  when /^(application|text)\/(x-|)javascript/
132
131
  begin
133
- @v8.eval response.content
132
+ @v8.eval response.body
134
133
  rescue StandardError => e
135
134
  @log.warn "Javascript[#{script_ref}] error: #{e.message}"
136
- @log.debug e.backtrace.join("\n")
135
+ @log.debug { e.backtrace.join("\n") }
137
136
  end
138
137
  else
139
- @log.warn "Unsupported script type '#{response.contenttype.inspect}' " +
138
+ @log.warn "Unsupported script type '#{response.content_type.inspect}' " +
140
139
  "for script from #{script['$source'].inspect}"
141
140
  end
142
141
  else
143
- @log.warn "Couldn't load script from #{script['$source'].inspect}: #{response.status} #{response.reason}"
144
- @log.debug "Body of unsuccessful request: #{response.body}"
142
+ if request_error
143
+ @log.warn "Couldn't load script from #{script['$source'].inspect} " +
144
+ "(#{request_error.class}): #{request_error.message})"
145
+ @log.debug { request_error.backtrace.join("\n") }
146
+ else
147
+ @log.warn "Couldn't load script from #{script['$source'].inspect}: " +
148
+ "#{response.statusText}"
149
+ end
145
150
  end
146
151
  else
147
152
  # It's javascript text
@@ -150,7 +155,7 @@ class NOMS::Command::Application < NOMS::Command::Base
150
155
  @v8.eval script
151
156
  rescue StandardError => e
152
157
  @log.warn "Javascript[#{script_ref}] error: #{e.message}"
153
- @log.debug e.backtrace.join("\n")
158
+ @log.debug { e.backtrace.join("\n") }
154
159
  end
155
160
  end
156
161
  script_index += 1
@@ -169,7 +174,8 @@ class NOMS::Command::Application < NOMS::Command::Base
169
174
  def display
170
175
  case @type
171
176
  when 'noms-v2'
172
- NOMS::Command::Formatter.new(_sanitize(@document.body)).render
177
+ body = _sanitize(@document.body)
178
+ NOMS::Command::Formatter.new(body, :logger => @log).render
173
179
  when 'noms-raw'
174
180
  @body.to_yaml
175
181
  when /^text(\/|$)/
@@ -4,7 +4,7 @@ require 'noms/command/version'
4
4
 
5
5
  require 'httpclient'
6
6
  require 'etc'
7
- require 'highline/import'
7
+ require 'highline'
8
8
  require 'json'
9
9
  require 'cgi'
10
10
 
@@ -23,48 +23,22 @@ class NOMS::Command::Auth < NOMS::Command::Base
23
23
 
24
24
  def initialize(opts={})
25
25
  @log = opts[:logger] || default_logger
26
- @loaded = { }
26
+ @specified = { }
27
+ @input = opts[:prompt_input] || $stdin
28
+ @output = opts[:prompt_output] || $stdout
29
+ @force_prompt = opts.has_key?(:force_prompt) ? opts[:force_prompt] : false
30
+
27
31
  (opts[:specified_identities] || []).each do |file|
28
- maybe_id = read_identity_from file
32
+ maybe_id = NOMS::Command::Auth::Identity.from file
29
33
  raise NOMS::Command::Error.now "#{file} contains invalid identity (no 'id')" unless
30
34
  maybe_id['id']
31
- @loaded[maybe_id['id']] = maybe_id
35
+ @specified[maybe_id['id']] = maybe_id
32
36
  end
33
37
  end
34
38
 
35
- def read_identity_from(file)
36
- @log.debug "Reading identity file #{file}"
37
- begin
38
- # TODO: Encryption and passphrases
39
- raise NOMS::Command::Error.new "Identity file #{file} does not exist" unless File.exist? file
40
- s = File.stat file
41
- raise NOMS::Command::Error.new "You don't own identity file #{file}" unless s.owned?
42
- raise NOMS::Command::Error.new "Permissions on #{file} are too permissive" unless (s.mode & 077 == 0)
43
- contents = File.read file
44
- case contents[0].chr
45
- when '{'
46
- NOMS::Command::Auth::Identity.new(self, JSON.parse(contents))
47
- else
48
- raise NOMS::Command::Error.new "#{file} contains unsupported or corrupted data"
49
- end
50
- rescue StandardError => e
51
- if e.is_a? NOMS::Command::Error
52
- raise e
53
- else
54
- raise NOMS::Command::Error.new "Couldn't load identity from #{file} (#{e.class}): #{e.message}"
55
- end
56
- end
57
- end
58
-
59
- # TODO: Persistent auth creds
60
- # Store like a client certificate: encrypted. Then use an
61
- # agent to store by using <agent>-add and typing passphrase
62
- # just like a client cert. <agent> expires credentials.
63
- # also you can explicitly unencrypt identity file
64
-
65
39
  def load(url, response)
66
40
  # Prompt
67
- auth_header = response.header['www-authenticate']
41
+ auth_header = response.header('WWW-Authenticate')
68
42
  auth_header = (auth_header.respond_to?(:first) ? auth_header.first : auth_header)
69
43
  case auth_header
70
44
  when /Basic/
@@ -75,30 +49,42 @@ class NOMS::Command::Auth < NOMS::Command::Base
75
49
  end
76
50
  domain = [url.scheme, '://', url.host, ':', url.port, '/'].join('')
77
51
  identity_id = CGI.escape(realm) + '=' + domain
78
- if saved(identity_id)
79
- retrieve(identity_id)
52
+ unless @specified.empty?
53
+ if @specified[identity_id]
54
+ @specified[identity_id]
55
+ else
56
+ @log.warn "No identity specified for #{domain} (#{realm})"
57
+ nil
58
+ end
80
59
  else
81
- if $stdin.tty?
82
- default_user = Etc.getlogin
83
- prompt = "#{domain} (#{realm}) username: "
84
- user = ask(prompt) { |u| u.default = Etc.getlogin }
85
- pass = ask('Password: ') { |p| p.echo = false }
86
- NOMS::Command::Auth::Identity.new(self, {
87
- 'id' => identity_id,
88
- 'realm' => realm,
89
- 'domain' => domain,
90
- 'username' => user,
91
- 'password' => pass
92
- })
60
+ if id_info = saved(identity_id)
61
+ NOMS::Command::Auth::Identity.new(id_info, :logger => @log)
93
62
  else
94
- @log.warn "Can't prompt for #{domain} (#{realm}) authentication (not a terminal)"
95
- NOMS::Command::Auth::Identity.new({
96
- 'id' => identity_id,
97
- 'realm' => realm,
98
- 'domain' => domain,
99
- 'username' => '',
100
- 'password' => ''
101
- })
63
+ # It might be nice to synchronize around this to
64
+ # make pipelines work.
65
+ if $stdin.tty? or @force_prompt
66
+ highline = HighLine.new(@input, @output)
67
+ default_user = Etc.getlogin
68
+ prompt = "#{domain} (#{realm}) username: "
69
+ user = highline.ask(prompt) { |u| u.default = Etc.getlogin }
70
+ pass = highline.ask('Password: ') { |p| p.echo = false }
71
+ NOMS::Command::Auth::Identity.new({
72
+ 'id' => identity_id,
73
+ 'realm' => realm,
74
+ 'domain' => domain,
75
+ 'username' => user,
76
+ 'password' => pass
77
+ }, :logger => @log)
78
+ else
79
+ @log.warn "Can't prompt for #{domain} (#{realm}) authentication (not a terminal)"
80
+ NOMS::Command::Auth::Identity.new({
81
+ 'id' => identity_id,
82
+ 'realm' => realm,
83
+ 'domain' => domain,
84
+ 'username' => '',
85
+ 'password' => ''
86
+ }, :logger => @log)
87
+ end
102
88
  end
103
89
  end
104
90
  else
@@ -107,11 +93,7 @@ class NOMS::Command::Auth < NOMS::Command::Base
107
93
  end
108
94
 
109
95
  def saved(identity_id)
110
- @loaded.has_key? identity_id
111
- end
112
-
113
- def retrieve(identity_id)
114
- @loaded[identity_id]
96
+ NOMS::Command::Auth::Identity.saved identity_id
115
97
  end
116
98
 
117
99
  end
@@ -1,6 +1,22 @@
1
1
  #!ruby
2
2
 
3
+ require 'noms/command/version'
4
+ require 'noms/command/home'
5
+
6
+ require 'fileutils'
3
7
  require 'logger'
8
+ require 'openssl'
9
+ require 'fcntl'
10
+ require 'base64'
11
+ require 'bcrypt'
12
+
13
+ require 'noms/command'
14
+
15
+ class String
16
+ def to_hex
17
+ self.unpack('C*').map { |n| '%02x' % n }.join('')
18
+ end
19
+ end
4
20
 
5
21
  class NOMS
6
22
 
@@ -17,10 +33,144 @@ end
17
33
  class NOMS::Command::Auth::Identity < NOMS::Command::Base
18
34
  include Enumerable
19
35
 
20
- def initialize(auth, h, attrs={})
36
+ @@identity_dir = File.join(NOMS::Command.home, 'identities')
37
+ @@cipher = 'aes-256-cfb'
38
+ @@hmac_digest = 'sha256'
39
+ @@max_key_idle = 3600
40
+
41
+ def self.identity_dir
42
+ @@identity_dir
43
+ end
44
+
45
+ def self.identity_dir=(value)
46
+ @@identity_dir = value
47
+ end
48
+
49
+ def self.generate_new_key
50
+ cipher = OpenSSL::Cipher.new(@@cipher)
51
+ cipher.encrypt
52
+ cipher.random_key + cipher.random_key
53
+ end
54
+
55
+ def self.vault_keyfile
56
+ File.join(@@identity_dir, '.noms-vault-key')
57
+ end
58
+
59
+ def self.ensure_dir
60
+ unless File.directory? @@identity_dir
61
+ FileUtils.mkdir_p @@identity_dir
62
+ end
63
+ File.chmod 0700, @@identity_dir
64
+ end
65
+
66
+ def self.get_vault_key
67
+ key = ''
68
+ ensure_dir
69
+ fh = File.for_fd(IO.sysopen(vault_keyfile, Fcntl::O_RDWR | Fcntl::O_CREAT, 0600))
70
+ fh.flock(File::LOCK_EX)
71
+ mtime = fh.mtime
72
+
73
+ key = fh.read if (Time.now - mtime < @@max_key_idle)
74
+
75
+ if key.empty?
76
+ key = generate_new_key
77
+ fh.write key
78
+ end
79
+ fh.flock(File::LOCK_EX)
80
+ fh.close
81
+
82
+ key
83
+ end
84
+
85
+ def self.decrypt(blob)
86
+ mac_key, enc_key = get_vault_key.unpack('a32a32')
87
+
88
+ message = Base64.decode64(blob)
89
+ hmac_digest, iv, data = message.unpack('a32a16a*')
90
+
91
+ hmac = OpenSSL::HMAC.new(mac_key, OpenSSL::Digest.new(@@hmac_digest))
92
+ hmac.update(iv + data)
93
+ raise NOMS::Command::Error.new("HMAC verification error") unless hmac.digest == hmac_digest
94
+
95
+ cipher = OpenSSL::Cipher.new @@cipher
96
+ cipher.decrypt
97
+ cipher.key = enc_key
98
+ cipher.iv = iv
99
+ cipher.update(data) + cipher.final
100
+ end
101
+
102
+
103
+ # Returns hash of identity data suitable for passing to .new
104
+ def self.saved(identity_id, opt={})
105
+ # This can't really log errors, hm.
106
+ id_number = OpenSSL::Digest::SHA1.new(identity_id).hexdigest
107
+ file_base = File.join(@@identity_dir, id_number)
108
+
109
+ if File.exist? "#{file_base}.json"
110
+ begin
111
+ JSON.parse(File.read "#{file_base}.json").merge({ '_loaded' => { "#{file_base}.json" => Time.now.to_s } })
112
+ rescue StandardError => e
113
+ File.unlink "#{file_base}.json" if File.exist? "#{file_base}.json"
114
+ return nil
115
+ end
116
+ elsif File.exist? "#{file_base}.enc"
117
+ begin
118
+ hash = JSON.parse(decrypt(File.read("#{file_base}.enc"))).
119
+ merge({ '_decrypted' => true, '_loaded' => { "#{file_base}.enc" => Time.now.to_s } })
120
+ rescue StandardError => e
121
+ File.unlink "#{file_base}.enc" if File.exist? "#{file_base}.enc"
122
+ return nil
123
+ end
124
+ else
125
+ return nil
126
+ end
127
+
128
+ end
129
+
130
+ def self.from(file)
131
+ begin
132
+ raise NOMS::Command::Error.new "Identity file #{file} does not exist" unless File.exist? file
133
+ s = File.stat file
134
+ raise NOMS::Command::Error.new "You don't own identity file #{file}" unless s.owned?
135
+ raise NOMS::Command::Error.new "Permissions on #{file} are too permissive" unless (s.mode & 077 == 0)
136
+ contents = File.read file
137
+ raise NOMS::Command::Error.new "#{file} is empty" unless contents and ! contents.empty?
138
+ case contents[0].chr
139
+ when '{'
140
+ NOMS::Command::Auth::Identity.new(JSON.parse(contents).merge({'_specified' => file }), :logger => @log)
141
+ else
142
+ raise NOMS::Command::Error.new "#{file} contains unsupported or corrupted data"
143
+ end
144
+ rescue StandardError => e
145
+ if e.is_a? NOMS::Command::Error
146
+ raise e
147
+ else
148
+ raise NOMS::Command::Error.new "Couldn't load identity from #{file} (#{e.class}): #{e.message}"
149
+ end
150
+ end
151
+ end
152
+
153
+ def initialize(h, attrs={})
21
154
  @log = attrs[:logger] || default_logger
22
- @auth = auth
23
155
  @data = h
156
+ refresh_vault_key if h['_decrypted']
157
+ end
158
+
159
+ def specified
160
+ @data['_specified']
161
+ end
162
+
163
+ def verification_hash
164
+ BCrypt::Password.create(self['username'] + ':' + self['password'] + '@' + self['id']).to_s
165
+ end
166
+
167
+ def auth_verify?(pwd_hash)
168
+ pwd = BCrypt::Password.new(pwd_hash)
169
+ pwd == self['username'] + ':' + self['password'] + '@' + self['id']
170
+ end
171
+
172
+ def id_number
173
+ OpenSSL::Digest::SHA1.new(self['id']).hexdigest
24
174
  end
25
175
 
26
176
  def [](key)
@@ -28,6 +178,7 @@ class NOMS::Command::Auth::Identity < NOMS::Command::Base
28
178
  end
29
179
 
30
180
  def []=(key, value)
181
+ $stderr.puts "auth identity set #{key} = #{value}"
31
182
  @data[key] = value
32
183
  end
33
184
 
@@ -39,8 +190,57 @@ class NOMS::Command::Auth::Identity < NOMS::Command::Base
39
190
  @data.keys
40
191
  end
41
192
 
42
- def save
43
- @log.debug "Saving #{@data['id']}"
193
+ def refresh_vault_key
194
+ vault_keyfile = NOMS::Command::Auth::Identity.vault_keyfile
195
+ if File.exist? vault_keyfile
196
+ File.utime(Time.now, Time.now, vault_keyfile)
197
+ end
198
+ end
199
+
200
+ def save(opt={})
201
+ return self.specified if self.specified
202
+ begin
203
+ opt[:encrypt] = true unless opt.has_key? :encrypt
204
+ file = opt[:file] || File.join(@@identity_dir, self.id_number + '.' + (opt[:encrypt] ? 'enc' : 'json'))
205
+ data = opt[:encrypt] ? self.encrypt : (self.to_json + "\n")
206
+
207
+ File.open(file, 'w') { |fh| fh.write data }
208
+ file
209
+ rescue StandardError => e
210
+ @log.warn "Couldn't save identity for #{@data['id']} (#{e.class}): #{e.message}"
211
+ @log.debug { e.backtrace.join("\n") }
212
+ return nil
213
+ end
214
+ end
215
+
216
+ def clear(opt={})
217
+ @log.debug "Clearing #{@data['id']}"
218
+ begin
219
+ basefile = File.join(@@identity_dir, self.id_number)
220
+ File.unlink "#{basefile}.json" if File.exist? "#{basefile}.json"
221
+ File.unlink "#{basefile}.enc" if File.exist? "#{basefile}.enc"
222
+ end
223
+ end
224
+
225
+ def to_json
226
+ @data.to_json
227
+ end
228
+
229
+ def encrypt
230
+ mac_key, enc_key = NOMS::Command::Auth::Identity.get_vault_key.unpack('a32a32')
231
+ cipher = OpenSSL::Cipher.new @@cipher
232
+ cipher.encrypt
233
+ iv = cipher.random_iv
234
+ cipher.key = enc_key
235
+ plaintext = self.to_json
236
+ data = cipher.update(plaintext) + cipher.final
237
+
238
+ hmac = OpenSSL::HMAC.new(mac_key, OpenSSL::Digest.new(@@hmac_digest))
239
+ hmac.update(iv + data)
240
+
241
+ message = hmac.digest + iv + data
242
+
243
+ Base64.encode64(message)
44
244
  end
45
245
 
46
246
  def id
@@ -56,7 +256,7 @@ class NOMS::Command::Auth::Identity < NOMS::Command::Base
56
256
  end
57
257
 
58
258
  def to_s
59
- "#{@data['id']}"
259
+ @data['id']
60
260
  end
61
261
 
62
262
  end