Please review pull request #554: (#12833) Handle PBKDF2 passwords in OS X opened by (glarizza)

Description:

Handling PBKDF2 password introduces a unique problem with Puppet.
Previously, Puppet utilized a password hash for the 'password' property
with the User resource on OS X. The hashing algorithm changed between
versions of OS X and so we had to chase down this moving target. PBKDF2
is another change to the hashing algoritm, but it also changes the way
that passwords must be set in a Puppet manifest for OS X clients.

PBKDF2 passwords are generated when a plaintext password is salted
(given a separate 32-byte salt string) and hashed a number of times. The
number of times this password hashing algorithm is run is based on a
parameter known as 'iterations', and is usually recommended to be
between 10,000 and 15,000. The resultant value is the password hash,
HOWEVER, that password hash is useless unless you know the value of the
salt that was used and the value for the iterations parameter. Since we
can no longer support a single 'password' property in Puppet, we must
support two additional properties called 'salt' and 'iterations'.

This commit introduces the 'salt' and 'iterations' properties and also
refactors the way salted-sha512 and salted-sha512-pbkdf2 passwords are
retrieved and set in OS X.

I ran spec tests against 10.7, a future version of OS X, centos5, and ubuntu lucid. All were passing at the time.

  • Opened: Fri Mar 02 22:50:18 UTC 2012
  • Based on: puppetlabs:master (372c3982888d9048be0a08921e39c81dab3a5dec)
  • Requested merge: glarizza:bug/master/12833_OS_X_PBKDF2 (79add6699f17a4201dceb1434910ebbf14fec2ff)

Diff follows:

diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb
index 2bdff3b..268c4c8 100644
--- a/lib/puppet/provider/nameservice/directoryservice.rb
+++ b/lib/puppet/provider/nameservice/directoryservice.rb
@@ -150,12 +150,16 @@ def self.generate_attribute_hash(input_hash, *type_properties)
       end
       attribute_hash[@@ds_to_ns_attribute_map[ds_attribute]] = ds_value
     end
-
+    converted_hash_plist = get_shadowhashdata(attribute_hash[:name])
     # NBK: need to read the existing password here as it's not actually
     # stored in the user record. It is stored at a path that involves the
     # UUID of the user record for non-Mobile local acccounts.
     # Mobile Accounts are out of scope for this provider for now
-    attribute_hash[:password] = self.get_password(attribute_hash[:guid], attribute_hash[:name]) if @resource_type.validproperties.include?(:password) and Puppet.features.root?
+    attribute_hash[:password] = self.get_password(attribute_hash[:guid], attribute_hash[:name], converted_hash_plist) if @resource_type.validproperties.include?(:password) and Puppet.features.root?
+    # GDL: The salt and iterations properties are only available in versions of
+    # OS X greater than 10.7.
+    attribute_hash[:salt] = self.get_salt(attribute_hash[:name], converted_hash_plist)
+    attribute_hash[:iterations] = self.get_iterations(attribute_hash[:name], converted_hash_plist)
     attribute_hash
   end
 
@@ -254,45 +258,26 @@ def self.set_password(resource_name, guid, password_hash)
       # 10.7 uses salted SHA512 password hashes which are 128 characters plus
       # an 8 character salt. Previous versions used a SHA1 hash padded with
       # zeroes. If someone attempts to use a password hash that worked with
-      # a previous version of OX X, we will fail early and warn them.
-      if password_hash.length != 136
-        fail("OS X 10.7 requires a Salted SHA512 hash password of 136 characters. \
-             Please check your password and try again.")
-      end
-
-      if File.exists?("#{@@users_plist_dir}/#{resource_name}.plist")
-        # If a plist already exists in /var/db/dslocal/nodes/Default/users, then
-        # we will need to extract the binary plist from the 'ShadowHashData'
-        # key, log the new password into the resultant plist's 'SALTED-SHA512'
-        # key, and then save the entire structure back.
-        users_plist = Plist::parse_xml(plutil( '-convert', 'xml1', '-o', '/dev/stdout', \
-                                       "#{@@users_plist_dir}/#{resource_name}.plist"))
-
-        # users_plist['ShadowHashData'][0].string is actually a binary plist
-        # that's nested INSIDE the user's plist (which itself is a binary
-        # plist).
-        password_hash_plist = users_plist['ShadowHashData'][0].string
-        converted_hash_plist = convert_binary_to_xml(password_hash_plist)
-
-        # converted_hash_plist['SALTED-SHA512'].string expects a Base64 encoded
-        # string. The password_hash provided as a resource attribute is a
-        # hex value. We need to convert the provided hex value to a Base64
-        # encoded string to nest it in the converted hash plist.
-        converted_hash_plist['SALTED-SHA512'].string = \
-          password_hash.unpack('a2'*(password_hash.size/2)).collect { |i| i.hex.chr }.join
-
-        # Finally, we can convert the nested plist back to binary, embed it
-        # into the user's plist, and convert the resultant plist back to
-        # a binary plist.
-        changed_plist = convert_xml_to_binary(converted_hash_plist)
-        users_plist['ShadowHashData'][0].string = changed_plist
-        Plist::Emit.save_plist(users_plist, "#{@@users_plist_dir}/#{resource_name}.plist")
-        plutil('-convert', 'binary1', "#{@@users_plist_dir}/#{resource_name}.plist")
+      # a previous version of OX X, we will fail early and warn them. If
+      # the version of OS X is greater than 10.7, a salted-sha512 PBKDF2
+      # password will be utilized (and Puppet will fail if the password
+      # hash isn't 256 characters).
+      if get_macosx_version_major == '10.7' and password_hash.length != 136
+        fail("OS X 10.7 requires a Salted SHA512 password hash of 136 characters. Please check your password and try again.")
+      elsif get_macosx_version_major != '10.7' and password_hash.length != 256
+        fail("OS X versions > 10.7 require a Salted SHA512 PBKDF2 password hash of 256 characters. Please check your password hash and try again.")
+      else
+        converted_hash_plist = get_shadowhashdata(resource_name)
+        if get_macosx_version_major == '10.7'
+          set_salted_sha512(resource_name, password_hash, converted_hash_plist)
+        else
+          set_salted_sha512_pbkdf2(resource_name, 'entropy', password_hash, converted_hash_plist)
+        end
       end
     end
   end
 
-  def self.get_password(guid, username)
+  def self.get_password(guid, username, converted_hash_plist)
     # Use Puppet::Util::Package.versioncmp() to catch the scenario where a
     # version '10.10' would be < '10.7' with simple string comparison. This
     # if-statement only executes if the current version is less-than 10.7 
@@ -307,32 +292,126 @@ def self.get_password(guid, username)
       end
       password_hash
     else
-      if File.exists?("#{@@users_plist_dir}/#{username}.plist")
-        # If a plist exists in /var/db/dslocal/nodes/Default/users, we will
-        # extract the binary plist from the 'ShadowHashData' key, decode the
-        # salted-SHA512 password hash, and then return it.
-        users_plist = Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{@@users_plist_dir}/#{username}.plist"))
-        if users_plist['ShadowHashData']
-          # users_plist['ShadowHashData'][0].string is actually a binary plist
-          # that's nested INSIDE the user's plist (which itself is a binary
-          # plist).
-          password_hash_plist = users_plist['ShadowHashData'][0].string
-          converted_hash_plist = convert_binary_to_xml(password_hash_plist)
-
-          # converted_hash_plist['SALTED-SHA512'].string is a Base64 encoded
-          # string. The password_hash provided as a resource attribute is a
-          # hex value. We need to convert the Base64 encoded string to a
-          # hex value and provide it back to Puppet.
-          password_hash = converted_hash_plist['SALTED-SHA512'].string.unpack("H*")[0]
-          password_hash
-        end
+      return nil if not converted_hash_plist
+
+      if get_macosx_version_major == '10.7'
+        get_salted_sha512(converted_hash_plist)
+      else
+        get_salted_sha512_pbkdf2(converted_hash_plist, 'entropy')
+      end
+    end
+  end
+
+  def self.get_shadowhashdata(resource_name)
+  #  This method will convert the user's plist located in
+  #  /var/db/dslocal/nodes/Default/users to XML and return the
+  #  value of the ShadowHashData key.  This value is a binary
+  #  encoded plist that is converted and returned as a Hash.
+    if (not File.exists?("#{@@users_plist_dir}/#{resource_name}.plist")) \
+    or (not File.readable?("#{@@users_plist_dir}/#{resource_name}.plist"))
+      fail("#{@@users_plist_dir}/#{resource_name}.plist is not readable, please check that permissions are correct and that the file is not corrupt.")
+    else
+      converted_users_plist = plutil('-convert',    \
+                                     'xml1',        \
+                                     '-o',          \
+                                     '/dev/stdout', \
+                                     "#{@@users_plist_dir}/#{resource_name}.plist")
+      users_plist = Plist::parse_xml(converted_users_plist)
+      if users_plist['ShadowHashData']
+        password_hash_plist = users_plist['ShadowHashData'][0].string
+        convert_binary_to_xml(password_hash_plist)
+      else
+        false
       end
     end
   end
 
+  def self.set_shadowhashdata(resource_name, converted_hash_plist, users_plist)
+  # This method converts the nested plist back to binary, embeds it
+  # into the user's plist, and convert the resultant plist back to
+  # a binary plist that can be read by the system. Arguments passed
+  # are the username, the nested plist, and the user's plist (as a hash)
+    changed_plist = convert_xml_to_binary(converted_hash_plist)
+    users_plist['ShadowHashData'][0].string = changed_plist
+    Plist::Emit.save_plist(users_plist, "#{@@users_plist_dir}/#{resource_name}.plist")
+    plutil('-convert', 'binary1', "#{@@users_plist_dir}/#{resource_name}.plist")
+  end
+
+  def self.get_salted_sha512(converted_hash_plist)
+  # This method retrieves the password hash from the embedded-plist
+  # retrieved from the 'ShadowHashData' key in the user's plist.
+  # Converted_hash_plist['SALTED-SHA512'].string is a Base64 encoded
+  # string. The password_hash provided as a resource attribute is a
+  # hex value. We need to convert the Base64 encoded string to a
+  # hex value and provide it back to Puppet.
+    converted_hash_plist['SALTED-SHA512'].string.unpack("H*").first
+  end
+
+  def self.set_salted_sha512(resource_name, password_hash, converted_hash_plist)
+    # This method takes passed arguments of the username, the password hash
+    # to be set, and the current converted_hash_plist retrieved from the
+    # system and sets the salted-sha512 hash according to how OS X 10.7 prefers
+    # to use it. Finally, set_shadowhashdata() is called to save the changes
+    # back to the local system.
+    converted_users_plist = plutil('-convert',    \
+                                   'xml1',        \
+                                   '-o',          \
+                                   '/dev/stdout', \
+                                   "#{@@users_plist_dir}/#{resource_name}.plist")
+    users_plist = Plist::parse_xml(converted_users_plist)
+    converted_hash_plist['SALTED-SHA512'].string = \
+      password_hash.unpack('a2'*(password_hash.size/2)).collect { |i| i.hex.chr }.join
+    set_shadowhashdata(resource_name, converted_hash_plist, users_plist)
+  end
+
+  def self.get_salted_sha512_pbkdf2(converted_hash_plist, field)
+  # This method reads the passed converted_hash_plist hash and returns values
+  # according to which field is passed.  Arguments passed are the hash
+  # containing the value read from the 'ShadowHashData' key in the User's
+  # plist, and the field to be read (one of 'entropy', 'salt', or 'iterations')
+    case field
+    when 'entropy', 'salt'
+      converted_hash_plist['SALTED-SHA512-PBKDF2'][field].string.unpack('H*').first
+    when 'iterations'
+      Integer(converted_hash_plist['SALTED-SHA512-PBKDF2'][field])
+    else
+      fail("Puppet has tried to read an incorrect value from the \
+            'SALTED-SHA512-PBKDF2' hash. Acceptable fields are 'salt', \
+            'entropy', or 'iterations'.")
+    end
+  end
+
+  def self.set_salted_sha512_pbkdf2(resource_name, field, value, converted_hash_plist)
+  # This method accepts a passed value and one of three fields: 'salt',
+  # 'entropy', or 'iterations'.  These fields correspond with the fields
+  # utilized in a PBKDF2 password hashing system.
+  # (see http://en.wikipedia.org/wiki/PBKDF2 for more information).
+  # The arguments passed are the username, the field to be changed (whether
+  # 'salt, 'entropy, or 'iterations'), and a hash containing the value
+  # to be set for the 'ShadowHashData' key in the User's plist.
+    case field
+    when 'salt', 'entropy'
+      converted_hash_plist['SALTED-SHA512-PBKDF2'][field].string =  \
+        value.unpack('a2'*(value.size/2)).collect { |i| i.hex.chr }.join
+    when 'iterations'
+      converted_hash_plist['SALTED-SHA512-PBKDF2'][field] = Integer(value)
+    else
+      fail("Puppet has tried to set an incorrect field for the \
+            'SALTED-SHA512-PBKDF2' hash. Acceptable fields are 'salt', \
+            'entropy', or 'iterations'.")
+    end
+    converted_users_plist = plutil('-convert',    \
+                                   'xml1',        \
+                                   '-o',          \
+                                   '/dev/stdout', \
+                                   "#{@@users_plist_dir}/#{resource_name}.plist")
+    users_plist = Plist::parse_xml(converted_users_plist)
+    set_shadowhashdata(resource_name, converted_hash_plist, users_plist)
+  end
+
+  def self.convert_xml_to_binary(plist_data)
   # This method will accept a hash that has been returned from Plist::parse_xml
   # and convert it to a binary plist (string value).
-  def self.convert_xml_to_binary(plist_data)
     Puppet.debug('Converting XML plist to binary')
     Puppet.debug('Executing: \'plutil -convert binary1 -o - -\'')
     IO.popen('plutil -convert binary1 -o - -', mode='r+') do |io|
@@ -343,9 +422,9 @@ def self.convert_xml_to_binary(plist_data)
     @converted_plist
   end
 
+  def self.convert_binary_to_xml(plist_data)
   # This method will accept a binary plist (as a string) and convert it to a
   # hash via Plist::parse_xml.
-  def self.convert_binary_to_xml(plist_data)
     Puppet.debug('Converting binary plist to XML')
     Puppet.debug('Executing: \'plutil -convert xml1 -o - -\'')
     IO.popen('plutil -convert xml1 -o - -', mode='r+') do |io|
@@ -422,6 +501,44 @@ def password=(passphrase)
     end
   end
 
+  def salt=(salt)
+  # This is the setter method for the 'salt' property that is only used when
+  # PBKDF2 passwords are necessary. This method uses
+  # self.set_salted_sha512_pbkdf2() to set the value in the User's plist.
+    if (Puppet::Util::Package.versioncmp(self.class.get_macosx_version_major, '10.7') == 1)
+      converted_hash_plist = self.class.get_shadowhashdata(@resource[:name])
+      self.class.set_salted_sha512_pbkdf2(@resource[:name], 'salt', salt, converted_hash_plist)
+    end
+  end
+
+  def iterations=(iterations)
+  # This is the setter method for the 'iterations' property that is only used
+  # when PBKDF2 passwords are necessary. This method uses
+  # self.set_salted_sha512_pbkdf2() to set the value in the User's plist.
+    if (Puppet::Util::Package.versioncmp(self.class.get_macosx_version_major, '10.7') == 1)
+      converted_hash_plist = self.class.get_shadowhashdata(@resource[:name])
+      self.class.set_salted_sha512_pbkdf2(@resource[:name], 'iterations', iterations, converted_hash_plist)
+    end
+  end
+
+  def self.get_iterations(username, converted_hash_plist)
+  # This is the getter method for the 'iterations' property that is only used
+  # when PBKDF2 passwords are necessary. This method uses
+  # self.get_salted_sha512_pbkdf2() to get the value from the User's plist.
+    if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == 1)
+      get_salted_sha512_pbkdf2(converted_hash_plist, 'iterations') if converted_hash_plist
+    end
+  end
+
+  def self.get_salt(username, converted_hash_plist)
+  # This is the getter method for the 'salt' property that is only used
+  # when PBKDF2 passwords are necessary. This method uses
+  # self.get_salted_sha512_pbkdf2() to get the value from the User's plist.
+    if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == 1)
+      get_salted_sha512_pbkdf2(converted_hash_plist, 'salt') if converted_hash_plist
+    end
+  end
+
   # NBK: we override @parent.set as we need to execute a series of commands
   # to deal with array values, rather than the single command nameservice.rb
   # expects to be returned by modifycmd. Thus we don't bother defining modifycmd.
diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb
index 6556a24..ffaa4be 100755
--- a/lib/puppet/type/user.rb
+++ b/lib/puppet/type/user.rb
@@ -518,6 +518,15 @@ def delimiter
       defaultto :minimum
     end
 
+    newproperty(:salt) do
+      desc "This is the 32 byte salt used to generate the PBKDF2 password used in
+            OS X"
+    end
 
+    newproperty(:iterations) do
+      desc "This is the number of iterations of a chained computation of the
+            password hash (http://en.wikipedia.org/wiki/PBKDF2).  This parameter
+            is used in OS X"
+    end
   end
 end
diff --git a/spec/unit/provider/nameservice/directoryservice_spec.rb b/spec/unit/provider/nameservice/directoryservice_spec.rb
index e3d32d7..5660aa0 100755
--- a/spec/unit/provider/nameservice/directoryservice_spec.rb
+++ b/spec/unit/provider/nameservice/directoryservice_spec.rb
@@ -80,13 +80,31 @@
   # The below is a binary plist containing a ShadowHashData key which CONTAINS
   # another binary plist. The nested binary plist contains a 'SALTED-SHA512'
   # key that contains a base64 encoded salted-SHA512 password hash...
-  let (:binary_plist) { "bplist00\324\001\002\003\004\005\006\a\bXCRAM-MD5RNT]SALTED-SHA512[RECOVERABLEO\020 \231k2\3360\200GI\201\355J\216\202\215y\243\001\206J\300\363\032\031\022\006\2359\024\257\217<\361O\020\020F\353\at\377\277\226\276c\306\254\031\037J(\235O\020D\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245O\021\002\000k\024\221\270x\353\001\237\346D}\377?\265]\356+\243\v[\350\316a\340h\376<\322\266\327\016\306n\272r\t\212A\253L\216\214\205\016\241 [\360/\335\002#\\A\372\241a\261\346\346\\\251\330\312\365\016\n\341\017\016\225&;\322\\\004*\ru\316\372\a \362?8\031\247\231\030\030\267\315\023\v\343{@\227\301s\372h\212\000a\244&\231\366\nt\277\2036,\027bZ+\223W\212g\333`\264\331N\306\307\362\257(^~ b\262\247&\231\261t\341\231%\244\247\203eOt\365\271\201\273\330\350\363C^A\327F\214!\217hgf\e\320k\260n\31
 5u~\336\371M\t\235k\230S\375\311\303\240\351\037d\273\321y\335=K\016`_\317\230\2612_\023K\036\350\v\232\323Y\310\317_\035\227%\237\v\340\023\016\243\233\025\306:\227\351\370\364x\234\231\266\367\016w\275\333-\351\210}\375x\034\262\272kRuHa\362T/F!\347B\231O`K\304\037'k$$\245h)e\363\365mT\b\317\\2\361\026\351\254\375Jl1~\r\371\267\352\2322I\341\272\376\243^Un\266E7\230[VocUJ\220N\2116D/\025f=\213\314\325\vG}\311\360\377DT\307m\261&\263\340\272\243_\020\271rG^BW\210\030l\344\0324\335\233\300\023\272\225Im\330\n\227*Yv[\006\315\330y'\a\321\373\273A\240\305F{S\246I#/\355\2425\031\031GGF\270y\n\331\004\023G@\331\000\361\343\350\264$\032\355_\210y\000\205\342\375\212q\024\004\026W:\205 \363v?\035\270L-\270=\022\323\2003\v\336\277\t\237\356\374\n\267n\003\367\342\330;\371S\326\016`B6@Njm>\240\021%\336\345\002(P\204Yn\3279l\0228\264\254\304\2528t\372h\217\347sA\314\345\245\337)]\000\b\000\021\000\032\000\035\000+\0007\000Z\000m\000\264\000\000\000\000\000\000\002\001\000\000\
 000\000\000\000\000\t\000\000\000\000\000\000\000\000\000\000\000\000\000\000\002\270" }
+  let (:salted_sha512_binary_plist) { "bplist00\324\001\002\003\004\005\006\a\bXCRAM-MD5RNT]SALTED-SHA512[RECOVERABLEO\020 \231k2\3360\200GI\201\355J\216\202\215y\243\001\206J\300\363\032\031\022\006\2359\024\257\217<\361O\020\020F\353\at\377\277\226\276c\306\254\031\037J(\235O\020D\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245O\021\002\000k\024\221\270x\353\001\237\346D}\377?\265]\356+\243\v[\350\316a\340h\376<\322\266\327\016\306n\272r\t\212A\253L\216\214\205\016\241 [\360/\335\002#\\A\372\241a\261\346\346\\\251\330\312\365\016\n\341\017\016\225&;\322\\\004*\ru\316\372\a \362?8\031\247\231\030\030\267\315\023\v\343{@\227\301s\372h\212\000a\244&\231\366\nt\277\2036,\027bZ+\223W\212g\333`\264\331N\306\307\362\257(^~ b\262\247&\231\261t\341\231%\244\247\203eOt\365\271\201\273\330\350\363C^A\327F\214!\217hgf\
 e\320k\260n\315u~\336\371M\t\235k\230S\375\311\303\240\351\037d\273\321y\335=K\016`_\317\230\2612_\023K\036\350\v\232\323Y\310\317_\035\227%\237\v\340\023\016\243\233\025\306:\227\351\370\364x\234\231\266\367\016w\275\333-\351\210}\375x\034\262\272kRuHa\362T/F!\347B\231O`K\304\037'k$$\245h)e\363\365mT\b\317\\2\361\026\351\254\375Jl1~\r\371\267\352\2322I\341\272\376\243^Un\266E7\230[VocUJ\220N\2116D/\025f=\213\314\325\vG}\311\360\377DT\307m\261&\263\340\272\243_\020\271rG^BW\210\030l\344\0324\335\233\300\023\272\225Im\330\n\227*Yv[\006\315\330y'\a\321\373\273A\240\305F{S\246I#/\355\2425\031\031GGF\270y\n\331\004\023G@\331\000\361\343\350\264$\032\355_\210y\000\205\342\375\212q\024\004\026W:\205 \363v?\035\270L-\270=\022\323\2003\v\336\277\t\237\356\374\n\267n\003\367\342\330;\371S\326\016`B6@Njm>\240\021%\336\345\002(P\204Yn\3279l\0228\264\254\304\2528t\372h\217\347sA\314\345\245\337)]\000\b\000\021\000\032\000\035\000+\0007\000Z\000m\000\264\000\000\000\000\000\000\00
 2\001\000\000\000\000\000\000\000\t\000\000\000\000\000\000\000\000\000\000\000\000\000\000\002\270" }
+
+  # The below is a binary plist containing a ShadowHashData key which CONTAINS
+  # another binary plist. The nested binary plist contains a
+  # 'SALTED-SHA512-PBKDF2' key that contains a base64 encoded salted-SHA512
+  # password hash...
+  let (:salted_sha512_pbkdf2_binary_plist) {"bplist00\321\001\002_\020\024SALTED-SHA512-PBKDF2\323\003\004\005\006\a\bWentropyTsaltZiterationsO\020\200x\352\320\334?$C\244\234?C\tt\222\233i\366\004\036\021\371&\315\313H&,\205q\366\271+\335}kL\005K\324t\004c \235\217\030\202\3361\353=\3715\322hEh\320H\017\312\0304n\"p'F\375\020r\300\005\235!2#7\237\351\030\036\202\246\224\362\e\215\236o8n\3268\334L\355\231\275[\372\223b\017\020O\314,\025\354T\302;\370\nB\316\274\2207\3163\214I\251\235p\aO\020 G|\323\303\032\3033\260L\206\025\222\372\345\221\263Q\375\200\f~j\255\224\034\227fW\206\266\323\035\021\017Z\b\v\")16A\304\347\000\000\000\000\000\000\001\001\000\000\000\000\000\000\000\t\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\352"}
 
   # The below is a base64 encoded salted-SHA512 password hash.
-  let (:pw_string) { "\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245" }
+  let (:salted_sha512_pw_string) { "\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245" }
+
+  # And this is a base64 encoded salted-SHA512-PBKDF2 password hash.
+  let (:salted_sha512_pbkdf2_pw_string) {"x\352\320\334?$C\244\234?C\tt\222\233i\366\004\036\021\371&\315\313H&,\205q\366\271+\335}kL\005K\324t\004c \235\217\030\202\3361\353=\3715\322hEh\320H\017\312\0304n\"p'F\375\020r\300\005\235!2#7\237\351\030\036\202\246\224\362\e\215\236o8n\3268\334L\355\231\275[\372\223b\017\020O\314,\025\354T\302;\370\nB\316\274\2207\3163\214I\251\235p\a"}
 
   # The below is a salted-SHA512 password hash in hex.
-  let (:sha512_hash) { 'dd067bfc346740ff7a84d20dda7411d80a03a64b93ee1c2150b1c5741de6ea7086036ea74d4d41c8c15a3cf6a6130e315733e0ef00cf5409c1c92b84a64c37bef8d02aa5' }
+  let (:salted_sha512_hash) { 'dd067bfc346740ff7a84d20dda7411d80a03a64b93ee1c2150b1c5741de6ea7086036ea74d4d41c8c15a3cf6a6130e315733e0ef00cf5409c1c92b84a64c37bef8d02aa5' }
+
+  # The below is a salted-SHA512-PBKDF2 password hash in hex.
+  let (:salted_sha512_pbkdf2_hash) {'78ead0dc3f2443a49c3f430974929b69f6041e11f926cdcb48262c8571f6b92bdd7d6b4c054bd4740463209d8f1882de31eb3df935d2684568d0480fca18346e22702746fd1072c0059d213223379fe9181e82a694f21b8d9e6f386ed638dc4ced99bd5bfa93620f104fcc2c15ec54c23bf80a42cebc9037ce338c49a99d7007'}
+
+  let (:salted_sha512_pbkdf2_salt_hex) {'477cd3c31ac333b04c861592fae591b351fd800c7e6aad941c97665786b6d31d'}
+
+  let (:salted_sha512_pbkdf2_salt_binary) {"G|\323\303\032\3033\260L\206\025\222\372\345\221\263Q\375\200\f~j\255\224\034\227fW\206\266\323\035"}
+
+  let (:salted_sha512_pbkdf2_iterations) {'3930'}
 
   let :plist_path do
     '/var/db/dslocal/nodes/Default/users/jeff.plist'
@@ -96,45 +114,221 @@
     Puppet::Provider::NameService::DirectoryService
   end
 
-  let :shadow_hash_data do
-    {'ShadowHashData' => [StringIO.new(binary_plist)]}
+  let :salted_sha512_converted_hash_plist do
+    { 'SALTED-SHA512' => StringIO.new(salted_sha512_pw_string)
+    }
+  end
+
+  let :salted_sha512_pbkdf2_converted_hash_plist do
+    { 'SALTED-SHA512-PBKDF2' =>
+      { 'salt'       => StringIO.new(salted_sha512_pbkdf2_salt_binary),
+        'entropy'    => StringIO.new(salted_sha512_pbkdf2_pw_string),
+        'iterations' => salted_sha512_pbkdf2_iterations
+      }
+    }
+  end
+
+  let :salted_sha512_shadow_hash_data do
+    {'ShadowHashData' => [StringIO.new(salted_sha512_binary_plist)]}
+  end
+
+  let :salted_sha512_pbkdf2_shadow_hash_data do
+    {'ShadowHashData' => [StringIO.new(salted_sha512_pbkdf2_binary_plist)]}
   end
 
   subject do
     Puppet::Provider::NameService::DirectoryService
   end
 
-  before :each do
-    subject.expects(:get_macosx_version_major).returns("10.7")
+  it 'should return the correct password when it is set on 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.7').times(6)
+    subject.expects(:get_shadowhashdata \
+                   ).with('jeff').returns(salted_sha512_converted_hash_plist)
+    subject.expects(:set_salted_sha512 \
+                   ).with('jeff',                            \
+                          salted_sha512_hash,                \
+                          salted_sha512_converted_hash_plist \
+                         ).returns(true)
+    subject.set_password('jeff', 'uid', salted_sha512_hash)
+    subject.get_password('uid', \
+                         'jeff', \
+                         salted_sha512_converted_hash_plist \
+                        ).should == salted_sha512_hash
+  end
+
+  it 'should return the correct salt when it is set on > 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.8').times(5)
+    subject.expects(:get_shadowhashdata \
+                   ).with('jeff').returns(salted_sha512_pbkdf2_converted_hash_plist)
+    subject.expects(:set_salted_sha512_pbkdf2 \
+                   ).with('jeff',                                   \
+                          'entropy',                                \
+                          salted_sha512_pbkdf2_hash,                \
+                          salted_sha512_pbkdf2_converted_hash_plist \
+                         ).returns(true)
+    subject.set_password('jeff', 'uid', salted_sha512_pbkdf2_hash)
+    subject.get_salt('jeff',                                   \
+                     salted_sha512_pbkdf2_converted_hash_plist \
+                    ).should == salted_sha512_pbkdf2_salt_hex
+  end
+
+  it 'should return the correct iterations when it is set on > 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.8').times(5)
+    subject.expects(:get_shadowhashdata \
+                   ).with('jeff').returns(salted_sha512_pbkdf2_converted_hash_plist)
+    subject.expects(:set_salted_sha512_pbkdf2 \
+                   ).with('jeff',                                   \
+                          'entropy',                                \
+                          salted_sha512_pbkdf2_hash,                \
+                          salted_sha512_pbkdf2_converted_hash_plist \
+                         ).returns(true)
+    subject.set_password('jeff', 'uid', salted_sha512_pbkdf2_hash)
+    subject.get_iterations('jeff',                             \
+                     salted_sha512_pbkdf2_converted_hash_plist \
+                    ).should == Integer(salted_sha512_pbkdf2_iterations)
   end
 
-  it 'should execute convert_binary_to_xml once when getting the password on >= 10.7' do
-    subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)})
-    File.expects(:exists?).with(plist_path).once.returns(true)
-    Plist.expects(:parse_xml).returns(shadow_hash_data)
-    # On Mac OS X 10.7 we first need to convert to xml when reading the password
-    subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path)
-    subject.get_password('uid', 'jeff')
+  it 'should return the correct password when it is set on > 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.8').times(6)
+    subject.expects(:get_shadowhashdata \
+                   ).with('jeff').returns(salted_sha512_pbkdf2_converted_hash_plist)
+    subject.expects(:set_salted_sha512_pbkdf2 \
+                   ).with('jeff',                                   \
+                          'entropy',                                \
+                          salted_sha512_pbkdf2_hash,                \
+                          salted_sha512_pbkdf2_converted_hash_plist \
+                         ).returns(true)
+    subject.set_password('jeff', 'uid', salted_sha512_pbkdf2_hash)
+    subject.get_password('uid',                                    \
+                         'jeff',                                   \
+                         salted_sha512_pbkdf2_converted_hash_plist \
+                        ).should == salted_sha512_pbkdf2_hash
   end
 
-  it 'should fail if a salted-SHA512 password hash is not passed in >= 10.7' do
+  it 'should execute get_salted_sha512 when getting the password on 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.7').twice
+    subject.expects(:get_salted_sha512).with(salted_sha512_converted_hash_plist).returns(salted_sha512_hash)
+    subject.get_password('uid', 'jeff', salted_sha512_converted_hash_plist)
+  end
+
+  it 'should execute get_salted_sha512_pbkdf2 when getting the password on \
+      > 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.8').twice
+    subject.expects(:get_salted_sha512_pbkdf2 \
+                   ).with(salted_sha512_pbkdf2_converted_hash_plist, \
+                          'entropy'                                  \
+                         ).returns(salted_sha512_pbkdf2_hash)
+    subject.get_password('uid', 'jeff', salted_sha512_pbkdf2_converted_hash_plist)
+  end
+
+  it 'should fail if a salted-SHA512 password hash is not passed in 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.7').twice
     expect {
       subject.set_password('jeff', 'uid', 'badpassword')
-    }.should raise_error(RuntimeError, /OS X 10.7 requires a Salted SHA512 hash password of 136 characters./)
+    }.should raise_error(RuntimeError, /OS X 10.7 requires a Salted SHA512 password hash of 136 characters./)
+  end
+
+  it 'should fail if a salted-SHA512-PBKDF2 password hash is not passed \
+      in > 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.8').times(3)
+    expect {
+      subject.set_password('jeff', 'uid', 'wrongpassword')
+    }.should raise_error(RuntimeError, \
+                         /OS X versions > 10.7 require a Salted SHA512 PBKDF2 password hash of 256 characters/)
+  end
+
+  it 'should not call get_salted_sha512_pbkdf2 with \'iterations\' on <= 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.7')
+    subject.expects(:get_salted_sha512_pbkdf2).never
+    subject.get_iterations('jeff', salted_sha512_converted_hash_plist \
+                    ).should == nil
+
+  end
+
+  it 'should call get_salted_sha512_pbkdf2 with \'iterations\' on > 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.8')
+    subject.expects(:get_salted_sha512_pbkdf2 \
+                   ).with(salted_sha512_pbkdf2_converted_hash_plist, \
+                          'iterations').returns(true)
+    subject.get_iterations('jeff', salted_sha512_pbkdf2_converted_hash_plist)
+  end
+
+  it 'should not call get_salted_sha512_pbkdf2 with \'salt\' on <= 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.7')
+    subject.expects(:get_salted_sha512_pbkdf2).never
+    subject.get_salt('jeff', salted_sha512_converted_hash_plist \
+                    ).should == nil
+
+  end
+
+  it 'should call get_salted_sha512_pbkdf2 with \'salt\' on > 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.8')
+    subject.expects(:get_salted_sha512_pbkdf2 \
+                   ).with(salted_sha512_pbkdf2_converted_hash_plist, \
+                          'salt').returns(true)
+    subject.get_salt('jeff', salted_sha512_pbkdf2_converted_hash_plist)
+  end
+
+  it 'should call set_salted_sha512 on 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.7').times(4)
+    subject.expects(:get_shadowhashdata \
+                   ).with('jeff').returns(salted_sha512_converted_hash_plist)
+    subject.expects(:set_salted_sha512 \
+                   ).with('jeff',                            \
+                          salted_sha512_hash,                \
+                          salted_sha512_converted_hash_plist \
+                         ).returns(true)
+    subject.set_password('jeff', 'uid', salted_sha512_hash)
+  end
+
+  it 'should call set_salted_sha512_pbkdf2 on > 10.7' do
+    subject.expects(:get_macosx_version_major).returns('10.8').times(4)
+    subject.expects(:get_shadowhashdata \
+                   ).with('jeff').returns(salted_sha512_pbkdf2_converted_hash_plist)
+    subject.expects(:set_salted_sha512_pbkdf2 \
+                   ).with('jeff',                                   \
+                          'entropy',                                \
+                          salted_sha512_pbkdf2_hash,                \
+                          salted_sha512_pbkdf2_converted_hash_plist \
+                         ).returns(true)
+    subject.set_password('jeff', 'uid', salted_sha512_pbkdf2_hash)
+  end
+
+  it 'should fail if the OS X Users plist does not exist' do
+    File.expects(:exists?).with(plist_path).returns false
+    expect {
+      subject.get_shadowhashdata('jeff')
+    }.should raise_error(RuntimeError, /jeff.plist is not readable/)
   end
 
-  it 'should convert xml-to-binary and binary-to-xml when setting the pw on >= 10.7' do
-    subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)})
-    subject.expects(:convert_xml_to_binary).returns(binary_plist)
-    File.expects(:exists?).with(plist_path).once.returns(true)
-    Plist.expects(:parse_xml).returns(shadow_hash_data)
-    # On Mac OS X 10.7 we first need to convert to xml
-    subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path)
-    # And again back to a binary plist or DirectoryService will complain
-    subject.expects(:plutil).with('-convert', 'binary1', plist_path)
-    Plist::Emit.expects(:save_plist).with(shadow_hash_data, plist_path)
-    subject.set_password('jeff', 'uid', sha512_hash)
+  it 'should fail if the OS X Users plist is not readable' do
+    File.expects(:exists?).with(plist_path).returns true
+    File.expects(:readable?).with(plist_path).returns false
+    expect {
+      subject.get_shadowhashdata('jeff')
+    }.should raise_error(RuntimeError, /jeff.plist is not readable/)
+  end
+
+  it 'should call convert_binary_to_xml if a correct Users plist is passed' do
+    File.expects(:exists?).with(plist_path).returns true
+    File.expects(:readable?).with(plist_path).returns true
+    Plist.expects(:parse_xml \
+                 ).returns(salted_sha512_pbkdf2_shadow_hash_data)
+    subject.expects(:convert_binary_to_xml \
+                   ).with(salted_sha512_pbkdf2_binary_plist).returns(true)
+    subject.expects(:plutil).returns true
+    subject.get_shadowhashdata('jeff')
   end
+
+  it 'should return false if the Users plist lacks a ShadowHashData field' do
+    File.expects(:exists?).with(plist_path).returns true
+    File.expects(:readable?).with(plist_path).returns true
+    Plist.expects(:parse_xml \
+                 ).returns({'nothing' => 'set'})
+    subject.expects(:plutil).returns true
+    subject.get_shadowhashdata('jeff').should == false
+  end
+
 end
 
 describe '(#4855) directoryservice group resource failure' do

    

--
You received this message because you are subscribed to the Google Groups "Puppet Developers" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to [email protected].
For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en.

Reply via email to