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
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. |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
#!/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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
#!/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 <password> # <scramble> # % ./nxdecode.py <scramble> # <password> # ================================================================ 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#!/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" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# ================================================ # 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!