Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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:

Code Block
{PKCS5S2}U48fu6LonjKCk0VmHPsgLrKf1/i1o/wxLXblOTa6P8eXvvJTU4iRb0fpRlO3xA0J

into a format understandable by OpenLDAP (with the  pw-pbkdf2  module loaded):

Code Block
{PBKDF2}10000$cjsPF6FcSW9CDwmpREtZog$qWi06T.6SSapuTtDsFn/2DPacsc

This will let you migrate user records from Jira into LDAP without forcing everyone to reset their password.

Those familiar with JIRA's database will know about the cwd_user  table, where JIRA stores user data:


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                                      │
└─────────────────────┴───────────────────────────────────────────────────────────────────────────┘

...

Code Block
$ credential='{PKCS5S2}U48fu6LonjKCk0VmHPsgLrKf1/i1o/wxLXblOTa6P8eXvvJTU4iRb0fpRlO3xA0J'
$ credential="${credential#'{PKCS5S2}'}"							# Chop off the identifier
$ echo $credential
U48fu6LonjKCk0VmHPsgLrKf1/i1o/wxLXblOTa6P8eXvvJTU4iRb0fpRlO3xA0J
$ echo -n "$credential" | base64 -d | xxd
00000000: 538f 1fbb a2e8 9e32 8293 4566 1cfb 202e  S......2..Ef.. .
00000010: b29f d7f8 b5a3 fc31 2d76 e539 36ba 3fc7  .......1-v.96.?.
00000020: 97be f253 5388 916f 47e9 4653 b7c4 0d09  ...SS..oG.FS....

The first 16 bytes is our salt, and the remainder is our hash:

Code Block
$ salt="$(echo -n "$credential" | base64 -d | head -c16)"
$ hash="$(echo -n "$credential" | base64 -d | tail -c32)"

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:

{PBKDF2}<Iteration>$<Adapted Base64 Salt>$<Adapted Base64 DK>

We know the iteration count (10000). We know the salt. We know the hash (derived key). We just need to reorder the elements.

Also, what is "adapted Base64"? Per the passlib docs it is just a shortened base64 format which trims the padding (appearing as '=' at the end of base64-encoded strings), and uses '.' characters instead of '+'.We can define this as a bash function:

Code Block
$ ab64encode() { python3 -c 'import sys; from passlib.utils.binary import *; print(ab64_encode(sys.stdin.buffer.read()).decode("utf-8"))'; }
$ echo foo | base64        # regular base64
Zm9vCg==
$ echo foo | ab64encode    # adapted base64
Zm9vCg

Now we have everything we need to write a conversion function:

.......1-v.96.?.
00000020: 97be f253 5388 916f 47e9 4653 b7c4 0d09  ...SS..oG.FS....

The first 16 bytes is our salt, and the remainder is our hash:

Code Block
$ salt="$(echo -n "$credential" | base64 -d | head -c16)"
$ hash="$(echo -n "$credential" | base64 -d | tail -c32)"

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:

{PBKDF2}<Iteration>$<Adapted Base64 Salt>$<Adapted Base64 DK>

We know the iteration count (10000). We know the salt. We know the hash (derived key). We just need to reorder the elements.

Also, what is "adapted Base64"? Per the passlib docs it is just a shortened base64 format which trims the padding (appearing as '=' at the end of base64-encoded strings), and uses '.' characters instead of '+'.We can define this as a bash function:

Code Block
$ ab64encode() { python3 -c 'import sys; from passlib.utils.binary import *; print(ab64_encode(sys.stdin.buffer.read()).decode("utf-8"))'; }
$ echo foo | base64        # regular base64
Zm9vCg==
$ echo foo | ab64encode    # adapted base64
Zm9vCg

Now we have everything we need to write a conversion function:

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
languagepy
titleatlassian_to_pbkdf2.py
#!/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
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
}

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

...