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.