Ruby Iconv to Strings#encode
I wrote a script in Perl, years ago, that lets users update their unix and samba passwords at the same time. This keeps them in sync so that people will have the same password whether they’re logging in via linux or at a windows computer. I thought it would be a good idea to rewrite it in ruby, since that’s the language I’m most comfortable with these days. Along with Google, the book that was most helpful was Programming Ruby 1.9 & 2.0 from the Pragmatic Programmers, which fortunately, I had.
We have a server running openldap and samba. It serves as our primary domain controller for the windows cluster and authenticates linux users as well. Each user has an entry that looks like this:
dn: uid=art,ou=people,dc=top,dc=example,dc=com uid: art cn: Art Treatcher objectClass: posixAccount objectClass: top objectClass: person objectClass: inetOrgPerson objectClass: sambaSamAccount loginShell: /bin/bash uidNumber: 1632 gidNumber: 200 homeDirectory: /users/art gecos: Art Treatcher mail: [email protected] sambaAcctFlags: [UX] sambaLogoffTime: 2147483647 sambaKickoffTime: 2146473647 sambaSID: S-1-5-21-3639540563-330460068-1655887120-4264 sambaPrimaryGroupSID: S-1-5-21-3639540563-330460068-1655887120-1401 sn: treacher sambaLMPassword: DE8F9826FE3ACDB8D8F7F5860820ED3F sambaPwdLastSet: 1358449419 sambaNTPassword: 880AAD1DE8956477793C417928DE4C25 userPassword:: e1NTSEF9YnJSQXB4TXlLN3lVUEVuaFBhU3Y0aUhHMDBOano5THcK
The two fields that my script is concerned with are at the bottom of the entry. The field userPassword is the encrypted linux password and sambaNTPassword is the encrypted password that windows looks at to login. Getting the linux password is easy because openldap ships with the slappasswd command. Simply running slappasswd -s password will generate the value to put in userPassword.
The complicated one is sambaNTPassword. After googling around a bit, I found this page. It basically says you should use the following to generate the value for sambaNTPassword.
OpenSSL::Digest::MD4.hexdigest(Iconv.iconv("UCS-2", "UTF-8", pass).join).upcase
That probably worked fine in 2008 when that page was written, but Iconv has been deprecated. A bit more googling told me that I should instead use strings#encode to change the encoding. Now the Programming Ruby book has a whole chapter on character encodings. They also include a little script to generate a table of known encoding names. Running that script gave me a big table, but the interesting part was here:
UTF-16 UTF-16BE (UCS-2BE) UTF-16LE UTF-32 UTF-32BE (UCS-4BE) UTF-32LE (UCS-4LE) UTF-7 (CP65000)
I had tried something like
OpenSSL::Digest::MD4.hexdigest(pass.encode("UCS-2").join).upcase
but found there wasn’t a UCS-2 encoding. However, there was a UCS-2BE. I tried that and found that I didn’t need the join anymore, so I tried this:
OpenSSL::Digest::MD4.hexdigest(pass.encode("UCS-2BE")).upcase
That gave me something, but not the right thing as I couldn’t login to my windows domain. Then I remembered reading this page that’s written in perl, but also says what’s going on. “The NT hash uses the MD4 algorithm, applied to the password in UTF-16 Little Endian encoding”. I figured that UTF-16BE was Big Endian and UTF-16LE was Little Endian. So I changed the code to this:
OpenSSL::Digest::MD4.hexdigest(pass.encode("UTF-16LE")).upcase
And that works perfectly! I was able to login to the windows domain and the linux computers. I still have a little more checking to do. And I’m trying to learn how to write tests for ruby code, so I’d like to do something with that. But right now, I’m really happy with this.
And here’s the script on github.