tumf/keystorage

View on GitHub
lib/keystorage/manager.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# coding: utf-8
require 'keystorage'
module Keystorage
  # ks = keystorage::Manager.new(:file =>"",:secret=> "P@ssword")
  # ks.get("mygroup","mykey") # => "mysecret"
  class Manager
    include Keystorage
    attr_reader :options
    def initialize options = {}
      @options = options
    end

    def groups
      file.keys.delete_if {|i| i == "@" }
    end

    def keys(group)
      file[group].keys
    end

    def get(group,key)
      raise SecretMissMatch unless valid?
      decode(file[group][key])
    end

    def set(group,key,value)
      raise RejectGroupName.new("Cannot use '@' for group name.") if group == "@"
      raise SecretMissMatch unless valid?

      data = file
      data[group] = {} unless data.has_key?(group)
      data[group][key] = {} unless data[group].has_key?(key)
      data[group][key] = encode(value)
      write(data)

      data[group][key]
    rescue Errno::ENOENT
      write({})
      retry
    end

    def password new_secret
      raise SecretMissMatch unless valid?

      # update passwords
      data = file.each { |name,keys|
        next if name == "@"
        keys.each { |key,value|
          keys[key] = encode(decode(value),new_secret)
        }
      }
      # update root group and write to file
      write root!(new_secret,data)
    rescue Errno::ENOENT
      write({})
      retry
    end

    def exec *cmd
      raise SecretMissMatch unless valid?
      system(envs.collect{ |k,v| "#{k}='#{v}'" }.join(' ') + " " + cmd.join(' '))
    end

    private

    def envs
      result = {}
      groups.each { |g|
        keys(g).each { |k|
          result["#{g}_#{k}"] = get(g,k)
        }
      }
      result
    end

    def sign message,_secret=secret
      raise NoSecret.new("set env KEYSTORAGE_SECRET") unless _secret
      OpenSSL::HMAC.hexdigest('sha512',_secret, message)
    end

    def token
      SecureRandom.urlsafe_base64(nil, false)
    end

    def root
      raise NoRootGroup unless file.has_key?("@")
      file["@"] || {}
    end

    def root! _secret=secret,data=file
      data["@"] = {}
      data["@"]["token"] = token
      data["@"]["sig"] = sign(data["@"]["token"],_secret)
      data
    end

    # file validation
    def valid?
      sign(root["token"]) == root["sig"]
    rescue NoRootGroup
      write root! and true
    end

    def encode(str,_secret=secret)
      enc = OpenSSL::Cipher::Cipher.new('aes256')
      enc.encrypt.pkcs5_keyivgen(_secret)
      ((enc.update(str) + enc.final).unpack("H*")).first.to_s
    end

    def decode(str,_secret=secret)
      dec = OpenSSL::Cipher::Cipher.new('aes256')
      dec.decrypt.pkcs5_keyivgen(_secret)
      (dec.update(Array.new([str]).pack("H*")) + dec.final)
    end

    def path
      options[:file] || ENV['KEYSTORAGE_FILE'] || DEFAULT_FILE
    end

    def file
      YAML.load(File.new(path)) || {}
    end

    def write data
      FileUtils.mkdir_p(File.dirname(path))
      File.open(path,'w',0600) { |f| YAML.dump(data,f) }
    end

    def secret
      options[:secret] || ENV['KEYSTORAGE_SECRET'] || DEFAULT_SECRET
    end

  end
end