...
Excerpt | ||||
---|---|---|---|---|
JIRA gives you the choice of storing user records internally, or delegating to an external 'User Directory' like Active Directory, LDAP or Atlassian Crowd. Many smaller orgs start off with internal user records, but later want to migrate users to LDAP for ease of management, or to allow authentication with non-Atlassian LDAP-aware systems. Generating LDAP (LDIF) records from JIRA's cwd_* tables is not hard, but how about password hashes? On this page we'll describe how to convert JIRA credential hashes:
into a format understandable by OpenLDAP (with the
This will let you migrate user records from Jira into LDAP without forcing everyone to reset their password. |
Warning |
---|
Argh! Something is buggy in hash conversion process. It works for some passwords but not for others ('hunter2' in particular). I never ended up using this beyond testing, so don't have inclination to debug. I've left it online for all the incidental information provided. |
Those familiar with JIRA's database will know about the cwd_user
table, where JIRA stores user data:
Code Block |
---|
redradish_jira=> select * from cwd_user where user_name='jturner';
┌─[ RECORD 1 ]────────┬───────────────────────────────────────────────────────────────────────────┐
│ id │ 10000 │
│ directory_id │ 1 │
│ user_name │ jturner │
│ lower_user_name │ jturner │
│ active │ 1 │
│ created_date │ 2013-09-02 18:14:34.078712+10 │
│ updated_date │ 2018-02-23 10:33:48.481+11 │
│ first_name │ Jeff │
│ lower_first_name │ jeff │
│ last_name │ Turner │
│ lower_last_name │ turner │
│ display_name │ Jeff Turner │
│ lower_display_name │ jeff turner │
│ email_address │ jeff@redradishtech.com │
│ lower_email_address │ jeff@redradishtech.com │
│ credential │ {PKCS5S2}U48fu6LonjKCk0VmHPsgLrKf1/i1o/wxLXblOTa6P8eXvvJTU4iRb0fpRlO3xA0J │
│ deleted_externally │ ␀ │
│ external_id │ a330dede-18f8-4745-ac8d-d2ec2bcabedc │
└─────────────────────┴───────────────────────────────────────────────────────────────────────────┘
|
Atlassian's PKCS5S2 format
...
- generate a random 16-byte salt
- feeds the salt plus password into our PBKDF2 function, which applies a hash (HMAC-SHA1) 10,000 times, yielding a a 32-byte hash
- concatenates salt and hash, and base64-encodes them
...
Incidentally you can generate such a hash using Python:
Code Block |
---|
$ sudo pip3 install passlib
$ python3 -c 'from passlib.hash import atlassian_pbkdf2_sha1; print(atlassian_pbkdf2_sha1.hash("hunter2"));'
{PKCS5S2}sFaqFaJUijGG0FqLUQrhPOEXrxB7jrXI7lzkPstbM3bhPq7x8rSS+Q3NtSduIgwt |
If you have a commercial Jira license, you can also download the source at https://my.atlassian.com and take a look (unpack dependencySources/atlassian-password-encoder-*-sources.jar
and look at DefaultPasswordEncoder and PKCS5S2PasswordHashGenerator).
...
OpenLDAP's PBKDF2 Support
OpenLDAP supports PBKDF2 with the help of a module. Here is how to generate a hash from the command-line:
slappasswd -o module-load=pw-pbkdf2.la -h {PBKDF2} -s hunter2
{PBKDF2}10000$wf6MXP0w8pxfQXKqDWCK1g$O3Vb3KDkFcmTqBCZU0w97XlELFc
The format is:
{PBKDF2}<Iteration>$<Adapted Base64 Salt>$<Adapted Base64 DK>
Although Atlassian's {PKCS5S2} and OpenLDAP's {BPKDF2} are really the same thing, the format is a bit different. Our job is to convert from Atlassian's to OpenLDAP's.
This is not hard. Look at OpenLDAP's format again The problem is that its format is different:
{PBKDF2}<Iteration>$<Adapted Base64 Salt>$<Adapted Base64 DKDK>
We know the iteration count (10000). We know the salt, and we . We know the hash (derived key). We just need to reorder the elements.
...
Code Block |
---|
function atlassian_to_pbkdf2() { ab64encode() { python3 -c 'import sys; from passlib.utils.binary import *; print(ab64_encode(sys.stdin.buffer.read()).decode("utf-8"))'; } local credential="$1" credential="${credential#'{PKCS5S2}'}" salt="$(echo -n "$credential" | base64 -d | head -c16 | ab64encode)" hash="$(echo -n "$credential" | base64 -d | tail -c32 | ab64encode)" printf "Salt: %s\n" "$salt" printf "Hash: %s\n" "$hash" printf "{PBKDF2}%d$%s$%s" 10000 "$salt" "$hash" | head -c64 echo } |
or in Python if you prefer:
Code Block | ||||
---|---|---|---|---|
| ||||
#!/usr/bin/env python3 # Converts Atlassian's password format: # # to OpenLDAP's format: # {PBKDF2}<Iteration>$<Adapted Base64 Salt>$<Adapted Base64 DK> import sys from passlib.utils.binary import b64decode from passlib.utils.binary import ab64_encode credential = sys.argv[1] # {PKCS5S2}U48fu6LonjKCk0VmHPsgLrKf1/i1o/wxLXblOTa6P8eXvvJTU4iRb0fpRlO3xA0J #credential="{PKCS5S2}U48fu6LonjKCk0VmHPsgLrKf1/i1o/wxLXblOTa6P8eXvvJTU4iRb0fpRlO3xA0J" credential = credential[9:] # U48fu6LonjKCk0VmHPsgLrKf1/i1o/wxLXblOTa6P8eXvvJTU4iRb0fpRlO3xA0J b64decode(credential) salt = ab64_encode( b64decode(credential)[0:16] ).decode('ascii') hash = ab64_encode( b64decode(credential)[16:48] ).decode('ascii') final=f"{{PBKDF2}}10000${salt}${hash}" print(final[:64]) |
A sample run:
Code Block |
---|
$ atlassian_to_pbkdf2 {PKCS5S2}U48fu6LonjKCk0VmHPsgLrKf1/i1o/wxLXblOTa6P8eXvvJTU4iRb0fpRlO3xA0J {PBKDF2}10000$U48fu6LonjKCk0VmHPsgLg$sp/X.LWj/DEtduU5Nro/x5e.8lN |
...
Code Block |
---|
moduleload pw-pbkdf2.so ... rootdn "cn=admin,dc=redradishtech,dc=com" rootpw {PBKDF2}10000$U48fu6LonjKCk0VmHPsgLg$sp/X.LWj/DEtduU5Nro/x5e.8lN |
What about {SHA} password hashes?
Up till 2013 JIRA (and Crowd) used the 'atlassian-sha1' scheme, which was actually unsalted sha512 (see
Jira | ||||||
---|---|---|---|---|---|---|
|