dayglojesus/macadmin

View on GitHub
lib/macadmin/shadowhash.rb

Summary

Maintainability
A
1 hr
Test Coverage
module MacAdmin
  
  # Custom error
  class ShadowHashError < StandardError
    UNSUPPORTED_OBJECT_ERR = 'Unsupported object: cannot store ShadowHashData'
  end
  
  # ShadowHash (super class)
  # - common methods for password sub-classes
  class ShadowHash
    
    include MacAdmin::Password
    
    attr_reader :label
    
    class << self
      
      # Reads the password data from the user record
      # - returns an appropriate ShadowHash object
      def create_from_user_record(user)
        if user['ShadowHashData']
          password = read_shadowhashdata(user['ShadowHashData'])
          if password[SaltedSHA512::LABEL]
            return SaltedSHA512.create_from_shadowhashdata(password)
          else
            return SaltedSHA512PBKDF2.create_from_shadowhashdata(password)
          end
        else
          if guid = user['generateduid']
            return SaltedSHA1.create_from_shadowhash_file(guid)
          end
        end
        nil
      end
            
      # Returns Hash
      # - key: label, value: password data
      def read_shadowhashdata(data)
        plist = CFPropertyList::List.new(:data => data[0].to_s)
        CFPropertyList.native_types(plist.value)
      end
            
    end # end self
    
  end
  
  # Legacy ShadowHashs
  # - password management for Mac OS X 10.6 and below
  # - passwords are managed in separate files in /var/db/shadow/hash
  class SaltedSHA1 < ShadowHash
    
    SHADOWHASH_STORE = '/private/var/db/shadow/hash'
    
    attr_accessor :hash
    
    class << self
      
      def create_from_shadowhash_file(guid)
        file = "#{SHADOWHASH_STORE}/#{guid}"
        if File.exists? file
          hash = read_from_shadowhash_file(file)
          return nil unless hash
          self.new(hash)
        end
      end
      
      def read_from_shadowhash_file(file)
        content = File.readlines(file).first
        content[168,48]
      end
      
    end
    
    # Initializes a SaltedSHA512 ShadowHash object from string
    # - string param should be a hex string, 24 bytes
    def initialize(string)
      @hash = validate(string)
    end
    
    # Validates the string param
    # - ensure the string param is hex string 24 bytes long
    def validate(string)
      error = "Invalid: arg must be hexadecimal string (24 bytes)"
      raise ArgumentError.new(error) unless string =~ /([A-F0-9]{2}){24}/
      string
    end
    
    # Return a String representation of the ShadowHash data
    def password
      @hash.to_s
    end
    
    # Return the ShadowHash as a Salted SHA1 String
    def data
      @data ||= @hash.to_s
    end
    
    private
    
    # Pseudo callback
    # - this method does not modify User objects
    # - SHA1 passwords are not stored as part of the User object
    # - They're stored in separate files on disk
    def store(sender)
      raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
      @data = @hash.to_s
    end
    
    # Write password to file
    # - success based on the length of the file, must be 1240 bytes
    # - returns boolean
    def write_to_shadowhash_file(sender)
      raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
      path = "#{SHADOWHASH_STORE}/#{sender[:generateduid].first}"
      file = File.open(path, mode='w+')
      content = file.read
      content = "0" * 1240 if content.length < 1240
      file.rewind
      content[168...(168 + 48)] = @hash.to_s
      file.write content
      file.close
      File.size(path) == 1240
    end
    alias :to_file :write_to_shadowhash_file
    
    # Remove the ShadowHash file associated with the sender
    # - returns boolean
    def remove_shadowhash_file(sender)
      raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
      path = "#{SHADOWHASH_STORE}/#{sender[:generateduid].first}"
      FileUtils.rm path if File.exists? path
      !File.exists? path
    end
    alias :rm_file :remove_shadowhash_file
    
  end
  
  # Lion ShadowHashs
  # - Mac OS X 10.7 store passwords as Salted SHA512 hashes
  # - hash is stored directly in the user's plist
  class SaltedSHA512 < ShadowHash
    
    LABEL = 'SALTED-SHA512'
    
    attr_accessor :hash
    
    class << self
      
      # Constructs a SaltedSHA512 ShadowHash object from ShadowHashData
      # - param is raw ShadowHashData object
      def create_from_shadowhashdata(data)
        value = data[SaltedSHA512::LABEL].to_s
        hex = MacAdmin::Password.convert_to_hex(value)
        self.new(hex)
      end
      
    end
    
    # Initializes a SaltedSHA512 ShadowHash object from string
    # - string param should be a hex string, 68 bytes
    def initialize(string)
      @label = LABEL
      @hash = validate(string)
    end
    
    # Validates the string param
    # - ensure the string param is hex string 68 bytes long
    def validate(string)
      error = "Invalid: arg must be hexadecimal string (68 bytes)"
      raise ArgumentError.new(error) unless string =~ /([a-f0-9]{2}){68}/
      string
    end
    
    # Return the ShadowHash as a ShadowHashData object
    # - Binary Plist
    def data
      @data ||= { @label => convert_to_blob(@hash) }.to_plist
    end
    
    # Return a Hash representation of the ShadowHash data
    def password
      { @label => @hash }
    end
    
    private
    
    # Pseudo callback for inserting a ShadowHashData object into the User object
    def store(sender)
      raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
      @data = { @label => convert_to_blob(@hash) }.to_plist
      sender['ShadowHashData'] = [@data]
    end
    
  end
  
  # Current ShadowHash Scheme
  # - Mac OS X 10.8 and up store passwords as Salted SHA512-PBKDF2 hashes
  # - hash is stored directly in the user's plist
  class SaltedSHA512PBKDF2 < ShadowHash
    
    LABEL = 'SALTED-SHA512-PBKDF2'
    
    class << self
      
      # Constructs a SaltedSHA512PBKDF2 ShadowHash object from ShadowHashData
      # - param is raw ShadowHashData object
      def create_from_shadowhashdata(data)
        hash = data[SaltedSHA512PBKDF2::LABEL]
        hash = hash.inject({}) do |memo, (key, value)|
          if key.eql? 'iterations'
            value = value.to_i
          else
            value = MacAdmin::Password.convert_to_hex(value)
          end
          memo[key.to_sym] = value
          memo
        end
        self.new(hash)
      end
      
    end
    
    # Initializes a SaltedSHA512PBKDF2 ShadowHash object from Hash or Array
    # - if passing an array, you must order the elements: entropy, salt, iterations
    # - pass a Hash with keys: entropy, salt, iterations
    def initialize(args)
      @label = LABEL
      @hash = validate(args)
    end
    
    # Validates the params
    # - ensure that we have the required params
    # - an Array that maps to required_keys structure
    # - a Hash that contains the required keys
    # - all values are qualified according to requirements
    def validate(args)
      error = nil
      hash = nil
      required_keys = [:entropy, :salt, :iterations]
      if args.is_a? Array
        hash = Hash[required_keys.zip(args)]
      elsif args.is_a? Hash
        hash = args
      end
      # validate hash
      unless (hash.keys - required_keys).empty?
        error = "Invalid: args must contain, #{required_keys.join(', ')}"
      end
      unless hash[:entropy] =~ /([a-f0-9]{2}){128}/
        error = "Invalid: entropy must be hexadecimal string (128 bytes)"
      end
      unless hash[:salt] =~ /([a-f0-9]{2}){32}/
        error = "Invalid: salt must be hexadecimal string (32 bytes)"
      end
      unless hash[:iterations] >= 0
        error = "Invalid: entropy must positive integer"
      end
      raise ArgumentError.new(error) if error
      hash
    end
    
    # Return the ShadowHash as a ShadowHashData object
    # - Binary Plist
    def data
      @data ||= { @label => format(@hash) }.to_plist
    end
    
    # Return a Hash representation of the ShadowHash data
    def password
      { @label => @hash }
    end
    
    private

    # Pseudo callback for inserting a ShadowHashData object into the User object
    def store(sender)
      raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
      @data = { @label => format(@hash) }.to_plist
      sender['ShadowHashData'] = [@data]
    end
    
    # Format the password data for ShadowHashData object compatibility
    def format(hash)
      hash.inject({}) do |memo, (key, value)|
        if key.to_s.eql? 'iterations'
          value = value.to_i
        else
          value = convert_to_blob value
        end
        memo[key.to_s] = value
        memo
      end
    end
    
  end
  
end