Class: NOMS::Command::Auth::Identity

Inherits:
Base
  • Object
show all
Includes:
Enumerable
Defined in:
lib/noms/command/auth/identity.rb

Constant Summary collapse

@@identity_dir =
File.join(NOMS::Command.home, 'identities')
@@cipher =
'aes-256-cfb'
@@hmac_digest =
'sha256'
@@max_key_idle =
3600

Instance Attribute Summary

Attributes inherited from Base

#logger

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#default_logger

Constructor Details

#initialize(h, attrs = {}) ⇒ Identity

Returns a new instance of Identity.



153
154
155
156
157
# File 'lib/noms/command/auth/identity.rb', line 153

def initialize(h, attrs={})
    @log = attrs[:logger] || default_logger
    @data = h
    refresh_vault_key if h['_decrypted']
end

Class Method Details

.decrypt(blob) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/noms/command/auth/identity.rb', line 85

def self.decrypt(blob)
    mac_key, enc_key = get_vault_key.unpack('a32a32')

    message = Base64.decode64(blob)
    hmac_digest, iv, data = message.unpack('a32a16a*')

    hmac = OpenSSL::HMAC.new(mac_key, OpenSSL::Digest.new(@@hmac_digest))
    hmac.update(iv + data)
    raise NOMS::Command::Error.new("HMAC verification error") unless hmac.digest == hmac_digest

    cipher = OpenSSL::Cipher.new @@cipher
    cipher.decrypt
    cipher.key = enc_key
    cipher.iv = iv
    cipher.update(data) + cipher.final
end

.ensure_dirObject



59
60
61
62
63
64
# File 'lib/noms/command/auth/identity.rb', line 59

def self.ensure_dir
    unless File.directory? @@identity_dir
        FileUtils.mkdir_p @@identity_dir
    end
    File.chmod 0700, @@identity_dir
end

.from(file) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/noms/command/auth/identity.rb', line 130

def self.from(file)
    begin
        raise NOMS::Command::Error.new "Identity file #{file} does not exist" unless File.exist? file
        s = File.stat file
        raise NOMS::Command::Error.new "You don't own identity file #{file}" unless s.owned?
        raise NOMS::Command::Error.new "Permissions on #{file} are too permissive" unless (s.mode & 077 == 0)
        contents = File.read file
        raise NOMS::Command::Error.new "#{file} is empty" unless contents and ! contents.empty?
        case contents[0].chr
        when '{'
            NOMS::Command::Auth::Identity.new(JSON.parse(contents).merge({'_specified' => file }), :logger => @log)
        else
            raise NOMS::Command::Error.new "#{file} contains unsupported or corrupted data"
        end
    rescue StandardError => e
        if e.is_a? NOMS::Command::Error
            raise e
        else
            raise NOMS::Command::Error.new "Couldn't load identity from #{file} (#{e.class}): #{e.message}"
        end
    end
end

.generate_new_keyObject



49
50
51
52
53
# File 'lib/noms/command/auth/identity.rb', line 49

def self.generate_new_key
    cipher = OpenSSL::Cipher.new(@@cipher)
    cipher.encrypt
    cipher.random_key + cipher.random_key
end

.get_vault_keyObject



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/noms/command/auth/identity.rb', line 66

def self.get_vault_key
    key = ''
    ensure_dir
    fh = File.for_fd(IO.sysopen(vault_keyfile, Fcntl::O_RDWR | Fcntl::O_CREAT, 0600))
    fh.flock(File::LOCK_EX)
    mtime = fh.mtime

    key = fh.read if (Time.now - mtime < @@max_key_idle)

    if key.empty?
        key = generate_new_key
        fh.write key
    end
    fh.flock(File::LOCK_EX)
    fh.close

    key
end

.identity_dirObject



41
42
43
# File 'lib/noms/command/auth/identity.rb', line 41

def self.identity_dir
    @@identity_dir
end

.identity_dir=(value) ⇒ Object



45
46
47
# File 'lib/noms/command/auth/identity.rb', line 45

def self.identity_dir=(value)
    @@identity_dir = value
end

.saved(identity_id, opt = {}) ⇒ Object

Returns hash of identity data suitable for passing to .new



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/noms/command/auth/identity.rb', line 104

def self.saved(identity_id, opt={})
    # This can't really log errors, hm.
    id_number = OpenSSL::Digest::SHA1.new(identity_id).hexdigest
    file_base = File.join(@@identity_dir, id_number)

    if File.exist? "#{file_base}.json"
        begin
            JSON.parse(File.read "#{file_base}.json").merge({ '_loaded' => { "#{file_base}.json" => Time.now.to_s } })
        rescue StandardError => e
            File.unlink "#{file_base}.json" if File.exist? "#{file_base}.json"
            return nil
        end
    elsif File.exist? "#{file_base}.enc"
        begin
            hash = JSON.parse(decrypt(File.read("#{file_base}.enc"))).
                merge({ '_decrypted' => true, '_loaded' => { "#{file_base}.enc" => Time.now.to_s } })
        rescue StandardError => e
            File.unlink "#{file_base}.enc" if File.exist? "#{file_base}.enc"
            return nil
        end
    else
        return nil
    end

end

.vault_keyfileObject



55
56
57
# File 'lib/noms/command/auth/identity.rb', line 55

def self.vault_keyfile
    File.join(@@identity_dir, '.noms-vault-key')
end

Instance Method Details

#[](key) ⇒ Object



176
177
178
# File 'lib/noms/command/auth/identity.rb', line 176

def [](key)
    @data[key]
end

#[]=(key, value) ⇒ Object



180
181
182
183
# File 'lib/noms/command/auth/identity.rb', line 180

def []=(key, value)
    $stderr.puts "auth identity set #{key} = #{value}"
    @data[key] = value
end

#auth_verify?(pwd_hash) ⇒ Boolean

Returns:

  • (Boolean)


167
168
169
170
# File 'lib/noms/command/auth/identity.rb', line 167

def auth_verify?(pwd_hash)
    pwd = BCrypt::Password.new(pwd_hash)
    pwd == self['username'] + ':' + self['password'] + '@' + self['id']
end

#clear(opt = {}) ⇒ Object



216
217
218
219
220
221
222
223
# File 'lib/noms/command/auth/identity.rb', line 216

def clear(opt={})
    @log.debug "Clearing #{@data['id']}"
    begin
        basefile = File.join(@@identity_dir, self.id_number)
        File.unlink "#{basefile}.json" if File.exist? "#{basefile}.json"
        File.unlink "#{basefile}.enc" if File.exist? "#{basefile}.enc"
    end
end

#domainObject



254
255
256
# File 'lib/noms/command/auth/identity.rb', line 254

def domain
    @data['domain']
end

#eachObject



185
186
187
# File 'lib/noms/command/auth/identity.rb', line 185

def each
    @data.each
end

#encryptObject



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/noms/command/auth/identity.rb', line 229

def encrypt
    mac_key, enc_key = NOMS::Command::Auth::Identity.get_vault_key.unpack('a32a32')
    cipher = OpenSSL::Cipher.new @@cipher
    cipher.encrypt
    iv = cipher.random_iv
    cipher.key = enc_key
    plaintext = self.to_json
    data = cipher.update(plaintext) + cipher.final

    hmac = OpenSSL::HMAC.new(mac_key, OpenSSL::Digest.new(@@hmac_digest))
    hmac.update(iv + data)

    message = hmac.digest + iv + data

    Base64.encode64(message)
end

#idObject



246
247
248
# File 'lib/noms/command/auth/identity.rb', line 246

def id
    @data['id']
end

#id_numberObject



172
173
174
# File 'lib/noms/command/auth/identity.rb', line 172

def id_number
    OpenSSL::Digest::SHA1.new(self['id']).hexdigest
end

#keysObject



189
190
191
# File 'lib/noms/command/auth/identity.rb', line 189

def keys
    @data.keys
end

#realmObject



250
251
252
# File 'lib/noms/command/auth/identity.rb', line 250

def realm
    @data['realm']
end

#refresh_vault_keyObject



193
194
195
196
197
198
# File 'lib/noms/command/auth/identity.rb', line 193

def refresh_vault_key
    vault_keyfile = NOMS::Command::Auth::Identity.vault_keyfile
    if File.exist? vault_keyfile
        File.utime(Time.now, Time.now, vault_keyfile)
    end
end

#save(opt = {}) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/noms/command/auth/identity.rb', line 200

def save(opt={})
    return self.specified if self.specified
    begin
        opt[:encrypt] = true unless opt.has_key? :encrypt
        file = opt[:file] || File.join(@@identity_dir, self.id_number + '.' + (opt[:encrypt] ? 'enc' : 'json'))
        data = opt[:encrypt] ? self.encrypt : (self.to_json + "\n")

        File.open(file, 'w') { |fh| fh.write data }
        file
    rescue StandardError => e
        @log.warn "Couldn't save identity for #{@data['id']} (#{e.class}): #{e.message}"
        @log.debug { e.backtrace.join("\n") }
        return nil
    end
end

#specifiedObject



159
160
161
# File 'lib/noms/command/auth/identity.rb', line 159

def specified
    @data['_specified']
end

#to_jsonObject



225
226
227
# File 'lib/noms/command/auth/identity.rb', line 225

def to_json
    @data.to_json
end

#to_sObject



258
259
260
# File 'lib/noms/command/auth/identity.rb', line 258

def to_s
    @data['id']
end

#verification_hashObject



163
164
165
# File 'lib/noms/command/auth/identity.rb', line 163

def verification_hash
    BCrypt::Password.create(self['username'] + ':' + self['password'] + '@' + self['id']).to_s
end