NX password scrambling and unscrambling algorithms in python 2.7

The NX web page entitled “The password scrambling algorithm in NX client” describes their password scrambling algorithm in C++ and perl but it does not describe it in python. Nor does it describe how to unscramble the data. This blog describes how I implemented both algorithms in python. I was surprised that NX didn’t use a standard symmetrical key algorithm like blowfish or DES for storing the keys but they must have some good reason.

DOWNLOADS

SCRAMBLING ALGORITHM

The scrambling algorithm is more for visual obfuscation than security as is clearly stated on their web page.
#!/usr/bin/env python
# ================================================================
# Author : Joe Linoff
# Date : 2012-02-04
# Version: 1.0
#
# Implements the password scrambling algorithm used by NX in python as
# defined here:
#
# http://www.nomachine.com/ar/view.php?ar_id=AR01C00125
# https://gist.github.com/902387
#
# Usage:
#     ./nxpasswd.py import os
# ================================================================
import os
import sys
import string
import time

validCharList = [
         "!", "#", "$", "%", "&", "(", ")", "*", "+", "-",
         ".", "0", "1", "2", "3", "4", "5", "6", "7", "8",
         "9", ":", ";", "<", ">", "?", "@", "A", "B", "C",
         "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
         "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W",
         "X", "Y", "Z", "[", "]", "_", "a", "b", "c", "d",
         "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
         "o", "p", "q", "r", "s", "t", "u", "v", "w", "x",
         "y", "z", "{", "|", "}"
         ]

def encodePassword(p):
    sPass = ":"
    sTmp = ""

    if p in ['', None]:
        return ''

    for i in range(len(p)):
        sPass += '%d:' % (ord(p[i])+i+1)

    return sPass

def findCharInList(c):
    global validCharList
    for i in range(len(validCharList)):
        if validCharList[i] == c:
            return i
    return -1

def getRandomValidCharFromList():
    global validCharList
    lt = time.localtime()
    s = lt.tm_sec
    if os.getenv('NXPASSWD_NONRANDOM'):
        s = int(os.getenv('NXPASSWD_NONRANDOM'))
    return validCharList[s]

def URLEncode(url):
    url = url.replace('&','&')
    url = url.replace('"','"')
    url = url.replace("'",''')
    url = url.replace('<','<')
    url = url.replace('>','>')
    return url

def scrambleString(s):
    global validCharList
    dummyString = "{{{{"
    sRet = ''

    if s in ['', None]:
        return ''

    s1 = encodePassword(s)

    if len(s1) < 32:
        sRet += dummyString

    sRet += s1[::-1]  # reverse string

    if len(sRet) < 32:
        sRet += dummyString

    ch = getRandomValidCharFromList()
    k = ord(ch) + len(sRet) - 2
    sRet = ch + sRet

    for i in range(1,len(sRet)):
        j = findCharInList(sRet[i])
        if j == -1:
            return sRet
        n = (j + k * (i+1)) % len(validCharList)
        sRet = sRet[:i] + validCharList[n] + sRet[i+1:]

    sRet += chr((ord(getRandomValidCharFromList())) + 2)
    sRet = URLEncode(sRet)
    return sRet

for i in range(1,len(sys.argv)):
    p=sys.argv[i]
    print scrambleString(p)

 UNSCRAMBLING ALGORITHM

This algorithm shows how to unscramble the output from the previous script to regenerate the original password.
#!/usr/bin/env python
# ================================================================
# Author : Joe Linoff
# Date : 2012-02-04
# Version: 1.0
#
# Unscrambles passwords creating by the NX scrambling algorithm.
#
# http://www.nomachine.com/ar/view.php?ar_id=AR01C00125
#
# Usage:
#    % ./nxpasswd.py 
#    <scramble>
#    % ./nxdecode.py <scramble>
#    
# ================================================================
import sys
import re

# We know that the first character is always a ':'
# We can use that to figure out the correct k value.
validCharList = [
         "!", "#", "$", "%", "&", "(", ")", "*", "+", "-",
         ".", "0", "1", "2", "3", "4", "5", "6", "7", "8",
         "9", ":", ";", "<", ">", "?", "@", "A", "B", "C",
         "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
         "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W",
         "X", "Y", "Z", "[", "]", "_", "a", "b", "c", "d",
         "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
         "o", "p", "q", "r", "s", "t", "u", "v", "w", "x",
         "y", "z", "{", "|", "}"
         ]

def findCharInList(c):
    global validCharList
    for i in range(len(validCharList)):
        if validCharList[i] == c:
            return i
    return -1

def findK(ch0,i=5,chf=':'):
    global validCharList
    j = findCharInList(chf)
    for k in range(0,200):
        n = (j + k * (i+1)) % len(validCharList)
        m = validCharList[n]
        if m == ch0:
            return k
    return -1

def unScramble(scramble):
    scramble=URLDecode(scramble)

    # Were dummy strings appended/prepended?
    # Need 3 tests to check.
    tests = []
    scramble=scramble[1:]
    scramble=scramble[:-1]
    tests.append([scramble,0])

    scramble=scramble[4:]
    tests.append([scramble,4])

    scramble=scramble[:-4]
    tests.append([scramble,4])

    for data in tests:
        scramble=data[0]
        i=data[1]
        k=findK(scramble[0],i+1)

        charset=''
        for ch in scramble:
            i += 1
            n = findCharInList(ch)
            for j in range(len(validCharList)):
                zn = (j + k * (i+1)) % len(validCharList)
                if n == zn:
                    ich = validCharList[j]
                    charset += ich
                    break

        charset = charset[::-1]
        ordvals = charset.split(':')
        passwd = ''
        i=0
        ok = True
        for ordval in ordvals:
            if len(ordval)>0:
                m = re.search('^[0-9]+$',ordval)
                if m:
                    j = int(ordval)-i-1
                    passwd += chr(j)
                    i += 1
                else:
                    ok = False
                    break

        if ok:
            return passwd

    return None

def URLDecode(url):
    url = url.replace('&','&')
    url = url.replace('"','"')
    url = url.replace(''',"'")
    url = url.replace('<','<')
    url = url.replace('>','>')
    return url

for i in range(1,len(sys.argv)):
    p=sys.argv[i]
    print unScramble(p)

EXAMPLE RUNS

Below I have run both programs to show they work using this test script.
#!/bin/bash
ps=(
    'a'
    '1'
    '12'
    '123'
    '1234'
    '12345'
    '123456'
    '1234567'
    '12345678'
    '123456789'
    '1234567890'
    '1234567890a'
    '11111111111'
    '1234567890ab' )
passed=0
failed=0
total=0
for p in ${ps[@]} ; do
    (( total++ ))
    echo
    echo "# ================================================"
    echo "# test       : $total"
    echo "# password   : $p"
    s=$(eval ./nxpasswd.py "'$p'" | tail -1)
    echo "# scrambled  : $s"
    px=$(eval ./nxdecode.py "'$s'" | tail -1)
    echo "# unscrambled: $px"
    if [[ "$px" != "$p" ]] ; then
        echo "# status     : FAILED"
        (( failed++ ))
    else
        echo "# status     : OK"
        (( passed++ ))
    fi
done
echo
echo "# SUMMARY"
echo "#   Passed : $passed"
echo "#   Failed : $failed"
echo "#   Total  : $total"
# ================================================
# test       : 1
# password   : a
# scrambled  : _Kbv1as.EAUl$a
# unscrambled: a
# status     : OK

# ================================================
# test       : 2
# password   : 1
# scrambled  : _Kbv1ak)EAUl$a
# unscrambled: 1
# status     : OK

# ================================================
# test       : 3
# password   : 12
# scrambled  : aUq1K#7Tu)Jkl*Fbc
# unscrambled: 12
# status     : OK

# ================================================
# test       : 4
# password   : 123
# scrambled  : b_}Bb?VwAVyCV{EJj-Ld
# unscrambled: 123
# status     : OK

# ================================================
# test       : 5
# password   : 1234
# scrambled  : bf+NqQp6]yBj&Mu2X$.Ps:d
# unscrambled: 1234
# status     : OK

# ================================================
# test       : 6
# password   : 12345
# scrambled  : cn9a*l5W(KqBc.YxGs4a1>e0Ue
# unscrambled: 12345
# status     : OK

# ================================================
# test       : 7
# password   : 123456
# scrambled  : ctCm;!CsFn9i3Z0R}QsEt8h;KuDne
# unscrambled: 123456
# status     : OK

# ================================================
# test       : 8
# password   : 1234567
# scrambled  : d|O}P>d:m5jAm<p?lDl@s>oGkCv0]1_f
# unscrambled: 1234567
# status     : OK

# ================================================
# test       : 9
# password   : 12345678
# scrambled  : d&X0bP{S.V2h3kEtG}O#Z*]9c;r>tOh>oEf
# unscrambled: 12345678
# status     : OK

# ================================================
# test       : 10
# password   : 123456789
# scrambled  : !L5xbdG0wXD1lWE}mX@#nQ9%eM;vcO-ve#
# unscrambled: 123456789
# status     : OK

# ================================================
# test       : 11
# password   : 1234567890
# scrambled  : !R?)qvaH9{hV<)uWH8tgU@&tZF7weT8%sSE6#
# unscrambled: 1234567890
# status     : OK

# ================================================
# test       : 12
# password   : 1234567890a
# scrambled  : #q[B1&oXL6!qWH:zm_B6&oXL6}qWG:zl_B5&%
# unscrambled: 1234567890a
# status     : OK

# ================================================
# test       : 13
# password   : 11111111111
# scrambled  : #oPD5{eUA)wbK>$m_F3#gSF*uhK;-l[N1}p%
# unscrambled: 11111111111
# status     : OK

# ================================================
# test       : 14
# password   : 1234567890ab
# scrambled  : ${eWKH9!spbPI8*}kaVB91tnfUF?-zsaSL7-$jcY&
# unscrambled: 1234567890ab
# status     : OK

# SUMMARY
#   Passed : 14
#   Failed : 0
#   Total  : 14
Enjoy!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Script Description
nxpasswd.py Scrambling algorithm.
nxpasswd.cc Scrambling algorithm in vanilla C++. The NoMachine example assumes that you are using the Qt library.
nxdecode.py Unscrambling algorithm.
test.sh Test script.