Sphinx Algorithm Explained

Explaining how the SPHINX password scheme works and why its magic

jupyter   security   sphinx   somethingAwesome

import ecdsa.ellipticcurve as ecc
import ecdsa.numbertheory as nt

Preliminary: Diffie Hellman Shared Secret

Elliptic Cryptography is based on the Discrete Logarithm Problem. In essense, we can simplify the idea to

Adding is easy on elliptic curves, but undoing addition seems hard

More formally, we can give the following definition:Let $G$ be an additive group, and let $x,y$ be elements of $G$ so that $x=ny$ for some integer $n$. The Discrete Logarithm Problem asks one to find $n$ when given $x$ and $y$. In integers, this problem is quite easy. If I have:

  • $x=12$
  • $y=4185072$

Then, I can compute that $y=41805072=348756*12=348756x \rightarrow n=348756$.

Division for integers is efficient, but for elliptic curves this is not the case.

Here is a toy problem testing my understanding of DH's shared secret protocol.

def sendDH(privateKey, generator, sendFunction):
   return sendFunction(privateKey * generator)

def receiveDH(privateKey, receiveFunction):
   return privateKey * receiveFunction()

prime = 3851
a = 324
b = 1287
myCurve = ecc.CurveFp(prime, a, b, 1)
basePoint = ecc.Point(myCurve, 920, 303)
aliceSecretKey = 233    # generateSecretKey(8)
bobSecretKey = 25       # generateSecretKey(8)

alicePublicKey = sendDH(aliceSecretKey, basePoint, lambda x:x)
bobPublicKey = sendDH(bobSecretKey, basePoint, lambda x:x)

sharedSecret1 = receiveDH(bobSecretKey, lambda: alicePublicKey)
sharedSecret2 = receiveDH(aliceSecretKey, lambda: bobPublicKey)
print(myCurve)
print(basePoint)
print('Shared secret is %s == %s' % (sharedSecret1, sharedSecret2))
CurveFp(p=3851, a=324, b=1287, h=1)
(920,303)
Shared secret is (1001,3826) == (1001,3826)

Algorithm Overview

Steps to implement

  1. Hashing password into elliptic curve

    • PWD entered by user
    • SHA-256 computation, hashing into the NIST P-256 Curve
    • this is computed on input + iteration_counter $\rightarrow Z_q$.
    • computed value is considered $x$ coord of a point on curve if $y$-value is associated with it is a quadratic residue (i.e. $x,y$ satisfy the curve equation).
    • this is repeated until a curve element is obtained. (which is the output)
    • Password is concatenated with domain name and input into H'. (ADD RESISTANCE AGAINST PHISHING)
  2. FK-PTR OPRF protocol
    1. EXTENSION:
      • Blind the password with OPRF
        • OPRF: $F_k(x)=H(x,(H'(x))^k)$
          • input $x$ from client
          • $k$ is from device
          • H maps from arbitrary length string $\rightarrow e \in \{0,1\}^\tau$, $\tau$ is a security parameter
            • Looking at the formula for OPRF, we assume that H(xbytearray, point in G) = $H(x || P_x || P_y)$
          • H' maps from arbitrary length string $\rightarrow g \in G$
            • H' is the "Hash into Elliptic Curve" function, which maps the password into a point on NIST P-256 curve
        • works over a group $G$ of prime order $p$ (e.g. NIST P-256 group)
      • extension picks a random number $\rho \in Z_q$ and raises the hash value of the input to the power $\rho$.
        • this blinding factor $\rho$ hides the password with information-theoretic security) SEND THIS AS $\alpha$
    2. DEVICE
      • check if $\alpha$ is $\in G$
      • compute and SEND BACK $\beta = \alpha^k$
    3. BACK TO EXTENSION
      • check if $\beta$ is $\in G$
      • raise the recieved value to the power of $\rho^{-1} \in Z_q$
      • then compute the SHA-256 hash of calculated value.
    4. BACK TO RWD PASSWORD
      • (same as PwdHash implementation)
      • encoded to a random combination of letters, numbers and symbols matching the passwrod requirement of the visited website and entered into pwd field of login page.

N.B.

  • taking the exponential in a group is just repetition of the operation
  • how to take the inverse power in a group
  • Instantiation assumes a cyclic group $G$ of prime order $q$, $|q|=\tau$, with generator $g$.
    • At init, User chooses master password pwd, while Device chooses and stores $k \leftarrow Z_q$.
  • H which looks like it accepts two arguments when it's called in $H(x, (H'(x))^k)$ really just means hash it all at once, by appending it.
    • in the paper, it describes this step as "Client hashes this value $(H'(pwd|domain))^k$ with the pwd to obtain rwd"
    • in the implementation, they use crypto_generichash_update() from source.
    • furthermore, in the documentation for <sodium.h> they use this function to compute the hash on a multi-part example
    • there is a reddit thread which also explains how the function used can be used to hash variable length things such as a stream
  • the notation of $\{0,1\}^\tau$ means a bit string of length $\tau$
  • octet means base-256, and not to be confused with octal which is base-8

Curve definition

Using the primitives from ecdsa package in python, we will create the following curve based on the parameters for P-256.

# NIST Curve P-256:
# ORDER = 115792089210356248762697446949407573529996955224135760342422259061068512044369
PRIME = 115792089210356248762697446949407573530086143415290314195533631308867097853951
R = 115792089210356248762697446949407573529996955224135760342422259061068512044369
# s = 0xc49d360886e704936a6678e1139d26b7819f7e90L
# c = 0x7efba1662985be9403cb055c75d4f7e0ce8d84a9c5114abcaf3177680104fa0dL
A = -3
B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
Gx = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
Gy = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5

curve_256 = ecc.CurveFp(PRIME, A, B, 1)
curve_256_generator = ecc.PointJacobi(curve_256, Gx, Gy, 1, R, generator=True)

1. Hashing password into elliptic curve

1.1 HashToBase Implementation

Here is a definition of the function: HashToBase(x): $H(x)[0:log_2(p) + 1]$, i.e., hash-truncate-reduce, where H is a cryptographic hash function, such as SHA256, and $p$ is the prime order of base field $F_p$. Here is some psuedo code:

HashToBase(x, i)

Parameters:

 H - cryptographic hash function to use
 hbits - number of bits output by H
 p - order of the base field Fp
 label - context label for domain separation

Preconditions:

 floor(log2(p)) + 1 >= hbits

Input:

 x - value to be hashed, an octet string
 i - hash call index, a non-negative integer

Output:

 y - a value in the field Fp

Steps:

 1. t1 = H("h2c" || label || I2OSP(i, 4) || x)
 2. t2 = OS2IP(t1)
 3. y = t2 (mod p)
 4. Output y

where I2OSP, OS2IP [RFC8017] are used to convert an octet string to
and from a non-negative integer, and a || b denotes concatenation of
a and b.
from binascii import hexlify, unhexlify
from hashlib import sha1, sha256, sha384, sha512
import hashlib
from ecdsa import NIST256p

# http://www.secg.org/sec2-v2.pdf
# print(NIST256p.oid)
ORDER = NIST256p.order

# https://github.com/bdauvergne/python-pkcs1/blob/master/pkcs1/primitives.py
def OS2IP(x: str) -> int:
    '''Converts the byte string x representing an integer reprented using the
       big-endian convient to an integer.
    '''
    h = hexlify(x) #.binascii
    return int(h, 16)

# https://github.com/bdauvergne/python-pkcs1/blob/master/pkcs1/primitives.py
def I2OSP(x: int, x_len: int = 4) -> str:
    '''Converts the integer x to its big-endian representation of length
       x_len.
    '''
    if x > 256**x_len:
        raise ValueError("Integer too large.")
    h = hex(x)[2:]
    if h[-1] == 'L':
        h = h[:-1]
    if len(h) & 1 == 1:
        h = '0%s' % h
    x = unhexlify(h) #.binascii
    return b'\x00' * int(x_len-len(x)) + x

print("OCT TEST: I2OSP(23) = {}, and then OS2IP(I2OSP(23)) = {}".format(I2OSP(23, 4), OS2IP(I2OSP(23, 4))))



# https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-02#appendix-C.5
def HashToBase(x: bytearray, i: int, label: str="label", p: int=ORDER) -> int:
    '''Hashes the bytearray x with a label string, the hash call index i, and
       returns y, a value in the field F_p
    '''
    H = sha256()
    toHash = ["h2c", label, I2OSP(i, 4), x]
    H.update(b"hc2")
    H.update(label.encode())
#     H.update(I2OSP(i,4))
    H.update(str(i).encode())
    H.update(x)
    t1 = H.digest()
    t2 = OS2IP(t1)
    return (t2 % p) # = y
    
valueToBeHashed = 23
hashCallIndex = 11
print(PRIME)
print(ORDER)
print("HashToBase(I2OSP({})={}, {}) = {}".format(valueToBeHashed, 
                                                 I2OSP(valueToBeHashed), 
                                                 hashCallIndex, 
                                                 HashToBase(I2OSP(valueToBeHashed), hashCallIndex)
                                                ))
OCT TEST: I2OSP(23) = b'\x00\x00\x00\x17', and then OS2IP(I2OSP(23)) = 23
115792089210356248762697446949407573530086143415290314195533631308867097853951
115792089210356248762697446949407573529996955224135760342422259061068512044369
HashToBase(I2OSP(23)=b'\x00\x00\x00\x17', 11) = 52666019840208479355183598407159392888009956878866650321053742936543132317912

1.2 Simplified SWU Method (5.2.3.)

As per the hashing into elliptic curves paper, for P-256 curve, we should use Simple SWU.

The following map2curve_simple_swu(alpha) implements the simplified
Shallue-Woestijne-Ulas algorithm from [SimpleSWU].  This algorithm
works for any curve over F_{p^n}, where p = 3 mod 4, including:

o  P256

o  ...

Given curve equation $g(x) = x^3 + Ax + B$, this algorithm works as follows:

  1. t = HashToBase(\alpha)
  2. $\alpha = \frac{-b}{a} * (1+\frac{1}{t^4 + t^2})$
  3. $\beta = -t^2 * \alpha$
  4. If $g(\alpha)$ is square, output $(\alpha, \sqrt{g(\alpha)})$
  5. Output $(\beta, \sqrt{g(\beta)})$

The following procedure implements this algorithm. It outputs a point with affine coordinates. It requires knowledge of A and B, the constants from the curve Weierstrass form.

map2curve_simple_swu(alpha)

   Input:

     alpha - value to be encoded, an octet string

   Output:

     (x, y) - a point in E

   Steps:

   1.     t = HashToBase(alpha)
   2. alpha = t^2 (mod p)
   3. alpha = alpha * -1 (mod p)
   4. right = alpha^2 + alpha (mod p)
   5. right = right^(-1) (mod p)
   6. right = right + 1 (mod p)
   7.  left = B * -1 (mod p)
   8.  left = left / A (mod p)
   9.    x2 = left * right (mod p)
   10.   x3 = alpha * x2 (mod p)
   11.   h2 = x2 ^ 3 (mod p)
   12.   i2 = x2 * A (mod p)
   13.   i2 = i2 + B (mod p)
   14.   h2 = h2 + i2 (mod p)
   15.   h3 = x3 ^ 3 (mod p)
   16.   i3 = x3 * A (mod p)
   17.   i3 = i3 + B (mod p)
   18.   h3 = h3 + i3 (mod p)
   19.   y1 = h2 ^ ((p + 1) / 4) (mod p)
   20.   y2 = h3 ^ ((p + 1) / 4) (mod p)
   21.    e = CTEQ(y1 ^ 2, h2)   // Constant-time equality
   22.    x = CMOV(x2, x3, e)    // If e = 1, choose x2, else choose x3
   23.    y = CMOV(y1, y2, e)    // If e = 1, choose y1, else choose y2
   24. Output (x, y)

Helper functions

o  CMOV(a, b, c): If c = 1, return a, else return b.

  Common software implementations of constant-time selects assume c
  = 1 or c = 0.  CMOV may be implemented by computing the desired
  selector (0 or 1) by ORing all bits of c together.  The end result
  will be either 0 if all bits of c are zero, or 1 if at least one
  bit of c is 1.

o  CTEQ(a, b): Returns a == b.  Inputs a and b must be the same
  length (as bytestrings) and the comparison must be implemented in
  constant time.
print("CHECK: ensure p = {} = 3 mod 4, {}mod4 = 3mod4: {}\n".format(PRIME, PRIME%4, PRIME%4==3))

# https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-02#section-5.2.3
# Implementation of H' which maps from bytearray -> g \in G
def map2curve_simple_swu(alpha: bytearray) -> (int, int):
    '''Maps the octet bytearray alpha into the elliptic curve, and returns a
       point from the elliptic curve.
    '''
    t =  HashToBase(alpha, 1)
    alpha = pow(t, 2, PRIME)
    alpha = -alpha % PRIME
    right = (pow(alpha, 2, PRIME) + alpha) % PRIME
    right = pow(right, PRIME-2, PRIME) # right^(-1) % PRIME    
    right = (right + 1)  % PRIME
    left = -B % PRIME
    left =  (left * pow(A, PRIME-2, PRIME)) % PRIME # (left * A^-1) % PRIME
    x2 = (left * right) % PRIME
    x3 = (alpha * x2)  % PRIME
    h2 = pow(x2, 3, PRIME) # x2 ^ 3  % PRIME
    i2 = (x2 * A)  % PRIME
    i2 = (i2 + B) % PRIME
    h2 = (h2 + i2)  % PRIME
    h3 = pow(x3, 3, PRIME) # x3 ^ 3  % PRIME
    i3 = (x3 * A)  % PRIME
    i3 = (i3 + B)  % PRIME
    h3 = (h3 + i3)  % PRIME
    y1 = pow(h2, (PRIME+1) // 4, PRIME) # h2 ^ ((p + 1) / 4)  % PRIME
    y2 = pow(h3, (PRIME+1) // 4, PRIME) # h3 ^ ((p + 1) / 4)  % PRIME
    if pow(y1, 2, PRIME) == h2:
        return ecc.Point(curve_256, x2, y1)
    else:
        return ecc.Point(curve_256, x3, y2)

# Implemented via the Simple SWU paper: https://eprint.iacr.org/2009/340.pdf
# 1. alpha = -t^2
# 2. X2 = -B/A * (1 + 1/(alpha^2 + alpha))
# 3. X3 = alpha*X2
# 4. h2 = g(X2), h3 = g(x3), if g(x) = x^3 + Ax + B
# 5. if h2 is square, return (X2, sqrt(g(X2))), else return (X3, sqrt(g(X3)))
def my_swu(alpha: bytearray, debug: bool=False) -> (int, int):
    # 1. alpha = -t^2
    t = HashToBase(alpha,1)
    print("0. HashToBase(alpha)= \t\t\t", t) if debug else None
    alpha = (-pow(t, 2, PRIME)) % PRIME
    print("1. alpha=-t^2= \t\t\t\t", alpha) if debug else None
    
    #X2 = -B/A * (1 + 1/(alpha^2 + alpha))
    X2_left = -B % PRIME
    X2_left = (X2_left * pow(A, PRIME-2, PRIME)) % PRIME
    # X2_left = 52283484311836130297341192243151613979733528143761346583456295874302188418414
    print("2.1 X2_left=-B/A= \t\t\t", X2_left) if debug else None
    
    X2_right = (alpha+1) % PRIME
    X2_right = (X2_right*alpha) % PRIME
    X2_right = pow(X2_right, PRIME-2, PRIME)
    X2_right = (X2_right + 1) % PRIME
    print("2.2 X2_right=1+1/(alpha^2+alpha)= \t", X2_right) if debug else None
    X2 = (X2_left * X2_right) % PRIME
    print("2.3 X2= \t\t\t\t", X2) if debug else None
    
    # X3 = alpha*X2
    X3 = (alpha*X2) % PRIME
    print("3. X3=alpha*X2= \t\t\t", X3) if debug else None
    
    # h2 = g(X2), h3 = g(x3), if g(x) = x^3 + Ax + B
    h2 = (pow(X2, 3, PRIME) + (A * X2)%PRIME + B) % PRIME
    h3 = (pow(X3, 3, PRIME) + (A * X3)%PRIME + B) % PRIME
    print("4.1 g(X2)= \t\t\t\t", h2) if debug else None
    print("4.2 g(X3)= \t\t\t\t", h3) if debug else None
    sh2 = pow(h2, (PRIME+1)//4, PRIME)
    sh3 = pow(h3, (PRIME+1)//4, PRIME)
    print("5.1 sqrt(g(X2))= \t\t\t", sh2) if debug else None
    print("5.2 sqrt(g(X3))= \t\t\t", sh3) if debug else None
    if pow(sh2, 2, PRIME) == h2:
        print("X2, sh2^2 = h2") if debug else None
        return ecc.Point(curve_256, X2, sh2)
    else:
        print("X3, sh3^2 = h3? {}".format(pow(sh3,2,PRIME) == h3)) if debug else None
        return ecc.Point(curve_256, X3, sh3)
CHECK: ensure p = 115792089210356248762697446949407573530086143415290314195533631308867097853951 = 3 mod 4, 3mod4 = 3mod4: True

1.2.1 Testing Correctness

Testing the SWU's implementation vs HashToBase's implementation, and showing it can successfully hash into the curve with all inputs range(0, test_cases)

test_cases = 30
correct = 0
correct_list = []
for test in range(test_cases):
    p = map2curve_simple_swu(I2OSP(test))
    if curve_256.contains_point(p.x(), p.y()):
        correct += 1
        correct_list.append(test)
    
print("correct: {}, are {}".format(correct, correct_list))
correct: 30, are [0, 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]
test_cases = 30
correct = 0
correct_list = []
for test in range(test_cases):
    p = my_swu(I2OSP(test))
    if curve_256.contains_point(p.x(), p.y()):
        correct += 1
        correct_list.append(test)
    
print("correct: {}, are {}".format(correct, correct_list))
correct: 30, are [0, 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]

1.3 Usage of SWU

print(map2curve_simple_swu(b"passwordwww.facebook.com"))

# or you can use the og swu implementation
print(my_swu(b"passwordwww.facebook.com"))
(50560896577634328033374378148020771698070026914100043083631439008968958738601,96434689098651036750287593872426977420942250920898794682863763170885260996734)
(50560896577634328033374378148020771698070026914100043083631439008968958738601,96434689098651036750287593872426977420942250920898794682863763170885260996734)

2 SPHINX architecture

steps to reproduce

Notes:

  • x is a byte array
  • $g \in G$
  1. client(x: bytearray) -> Point:
    • takes $x=$masterpassword || domainname
    • calculates $H'(x)$: bytearray -> $P(x,y) \in G$
    • picks a random number $\rho$
    • calculates and returns the blinded result $\alpha = H'(x)^\rho$
  2. device($\alpha$: Point) -> Point:
    • checks if $\alpha$ is in group $G$
    • retrieves (or creates and store) $d$ in database
    • calculates and returns $\beta = \alpha^d$
  3. client(\beta: Point) -> bytearray:
    • checks if $\beta$ is in group $G$
    • calculates $\beta^\frac{1}{\rho}$ and unblinds the result
    • calculates and returns H(x || \beta^(1/\rho))
x = "masterpasswordwww.google.com"
hdashx = map2curve_simple_swu(x.encode())
print("H'(x):")
print(hdashx)
print(hex(hdashx.x()), hex(hdashx.y()))
rho = 23 # generate a random number here
alpha = hdashx * rho # hdashx^rho
print("alpha = (H'(x))^rho:")
print(alpha)
print(hex(alpha.x()), hex(alpha.y()))
H'(x):
(9647045120380072016896367801436216089517385294113691900224368540592147852881,63780912247584663447060568763839704993950614958143814955131411688625580648907)
0x155408b6f6f87082d9d97dbb4d9322e279709ae7e591ec2a2cd02db7e548ee51 0x8d02b7900d4d11105589f57d5c2b8a871f372b26a0c1464b884671c6dad9a9cb
alpha = (H'(x))^rho:
(96060363318793445308142303247957725785598212468546760216643989368154363422182,67253679145713470788686215502821137612357123005677477254415963606436937150077)
0xd4603d2897e719a966dd41810e0d547f8e5141bb24b02792af8a4f0b401665e6 0x94b03bc36fb71a95dc6c50198870a85f7f0d52e0f0fb5ee0fdd41d09f3c72e7d
assert (curve_256.contains_point(alpha.x(), alpha.y()) == True)
d = HashToBase(b"some random way to produce this d key", 1)
print("d = ", d)
print(hex(d))
beta = d * alpha
print("beta = alpha^d:")
print(beta)
print(hex(beta.x()), hex(beta.y()))
d =  49593046683349734658837698885206452611033450058247891428920244266019060051104
0x6da4ab71e4602e1eb332ad0143eddb1ba07bd258ed2716a4f9271821e6a5eca0
beta = alpha^d:
(20902386769593356422803755726142762975644522947047497806519940156789227647566,37820471632289113646378000102094571407461625006655530832469911478145755153402)
0x2e3654e7b2bf3d4ff2ad21cdfc9f73670a42bf5a807e799e726ebd048f91f24e 0x539da0dc00fcea9ef0d42ec0fca00aafad897c01e1ba5cf901992ac4f67883fa
assert (curve_256.contains_point(beta.x(), beta.y()) == True)
# n.b.
print("rho^-1 * rho = ",pow(rho, PRIME-2, PRIME) * rho % PRIME)
final = beta * pow(rho, ORDER-2, ORDER)
print("final = beta^(1/rho)")
print(final)
print(hex(final.x()), hex(final.y()))
rho^-1 * rho =  1
final = beta^(1/rho)
(34550786935032766327755133052490331274309469427745266322787923543983364811861,49690625575606612910815333321718443879931197308857771898142620989441619363396)
0x4c630d6a1aea8b7ff03f4c9996dd21f978f9609eb387b3bb7b73b20ce694ac55 0x6ddbe5bc2a5b6a1514a6b6b7a1340ce4debe089a530c5b5cee2ba720b6b58644
check_final = hdashx * d
print(check_final)
assert (curve_256.contains_point(check_final.x(), check_final.y()) == True)
print("Check that this result `check final` equals `final`:")
print (check_final == final)
(34550786935032766327755133052490331274309469427745266322787923543983364811861,49690625575606612910815333321718443879931197308857771898142620989441619363396)
Check that this result `check final` equals `final`:
True
# Oblivious Psuedo-Random Function
def OPRF(x: str, point: ecc.Point) -> bytearray:
    '''Performs the actual Hash H of H(x, (H'(X))^d), which is the hash of a
       bytearray x and a Point on the curve. Returns a bytearray result.
    '''
    H = sha256()
    H.update(x.encode())
    H.update(I2OSP(point.x(), 256))
    H.update(I2OSP(point.y(), 256))
    return H.digest()

rwdbytes = OPRF(x, final)
print(rwdbytes, len(rwdbytes))

# convert this to a password
import os

def gen_password(rwd: bytearray, length: int=32, charset: str="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()") -> str:
    '''Generates the password based on the result of the OPRF function
    '''
    len_charset = len(charset)
    indices = [int(len_charset * (ord(chr(byte)) / 256.0)) for byte in rwd]
    return "".join([charset[index] for index in indices])

print("Your facebook password is: ", gen_password(rwdbytes))
b"\xec\xf8J\xf9\xd7\xe6\xa9e\xa5h\x1duj\x16\x91''\xd4\xe4\x89\x1c\xc9\xef\xfeo}\x17\xb7\x10\x8d.$" 32
Your facebook password is:  %*U(8#vcudIgdGoKK7#mH4^)fjGzEnMK

2.1 Overview

Here is all the basic functionality of the each part captured as a function.

They follow the naming convension "AToB", where A is the current Entity (Client, Device), as shown below.

x = "masterpasswordwww.google.com"

# Client 1
def clientToPoint(x: str) -> ecc.Point:
    '''input the master password pwd and returns a point on the curve alpha
       with the random integer that was used to blind it.
    '''
    hdashx = map2curve_simple_swu(x.encode())
    rho = OS2IP(os.urandom(32))
    return hdashx * rho # alpha = hdashx^rho

# Device
def deviceToClient(alpha: ecc.Point, index: int=1) -> ecc.Point:
    '''input the point on the curve. If it is in the Group, we store
       a random key D that corresponds to this point, and return the point
       exponeniated to D.
    '''
    if curve_256.contains_point(alpha.x(), alpha.y()) != True:
        return 0
    print("ALPHAS: ", hex(alpha.x()), hex(alpha.y()))
    randomBytes = os.urandom(32)
    d = HashToBase(randomBytes, index)
    print("DEVICE: I am going to store d: ", d)
    return d * alpha # beta = alpha^d

#Client 2
def clientToPassword(beta: ecc.Point) -> str:
    '''input the point on the curve. If it is in the Group, we compute
       this point exponeniated to the inverse of rho, and then we use the
       OPRF to create the byte array which generates the final password rwd
    '''
    if curve_256.contains_point(beta.x(), beta.y()) != True:
        return 0
    final = beta * pow(rho, ORDER-2, ORDER)
    print("FINAL: ", hex(final.x()), hex(final.y()))
    rwdbytes = OPRF(x, final)
    return gen_password(rwdbytes, charset="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()")

# Usage
# --------- Start Client --------- 
alpha = clientToPoint(x)
# ---------  End Client  ---------

# send alpha to Device

# --------- Start Device ---------
beta = deviceToClient(alpha)
# ---------  End Device  ---------

# send beta to Client

# --------- Start Client ---------
rwd = clientToPassword(beta)
print("CLIENT: my password is", rwd)
# ---------  End Client  ---------
ALPHAS:  0x6d68db7cb3dc8e41218e21c99686a1c4b15f4b28b042c20e9cf1a5ec4269f77a 0x6caf2ff7e2661f15eabc9b92dac5c99847ee9af38eade284e2fe4dcdc41246cf
DEVICE: I am going to store d:  39293753817579684587358563962791485292162063136866083282799165565461415876781
FINAL:  0x7e5b03b2c70b79dbf3ebee465c1af0dacf48abb1659a26251cf90c9324da7d43 0x7ef85457e3fa4289a7b49cd37d6ca2b285fd51ad68433a70598cad63033cb52b
CLIENT: my password is 5tce4XdQQHobplMPqhfUIwtR%RbOnlAw

2.2 Issues I ran Into during Implementation

Here is a collection of issues I've ran into over the past week about my implementation experience, having basically no experience in cryptography and trying to play around with elliptic curves.

  • Implementing Octet String <-> Integer Primitives
    • In order to implement HashToBase, I had to first implement these primitive functions. I looked at the papers but had trouble figuring out what I had to implement, and the implementation presented in the second paper was not very helpful in terms of how to actually implement it.
      • it said stuff like, "convert x to it's base 256 form such that x = x_{i-1} 256^{i-1} + ... + x_1 256 + x_0"
    • I was wondering, Do I need to actually calculate this value, when I've read online that this is literally the byte representation of an integer (in python its called bytearray!)
    • Mixed up octet with octal, and when was sad when I realised I could not use the python built-in oct().
  • HashToBase
    • I was pretty confused with the notation of the hash function and how it accepted multiple arguments
      • found out during week 4 tutorial and from personal research that you should concatenate it, but be careful of length extension attacks
      • many implementations of hash functions (for example, hashlib.sha256 in python), implement the hash function in a special way:
        • myHash = hashlib.sha256(). # instantiation
          
        • myHash.update(b'This is the byte representation of something I want to hash') # update
          myHash.update(b' , and you can keep hash multi-part lines like this')
          
        • myHash.digest() # result of the hash is returned when you run digest
          
        • I.e. use the update function sequentially to add multiple parts to the hash, this is done for multiple reasons
          • encourage good standards, so people hash multiple arguments through the update() function rather than concatenating the strings before applying the hash
          • so that each call to hash runs in constant time, no need to worry about variable-length string hashing
  • Simple SWU
    • modular arithmetic is very particular
      • A // B is usually how you do integer division, however, this does not work for modular arithmetic.
        • Use A * pow(B, PRIME-2, PRIME), where B^-1 % PRIME == pow(B, PRIME-2, PRIME) and PRIME mod 4 == 3 mod 4 is required.
      • make sure the entire result is enclosed in brackets before applying %PRIME or else you may only be applying modulo to part of the result
      • I was really just stuck on that first point for while, the algorithm was failing to hash 8/30 cases, and I was having a hard time figuring out why until I played around with genius, the cli calculator capable of modular arithmetic.
  • SPHINX
    • I could not get the actual math to work, when I exponiated $\beta$ to the inverse of $\rho$, it was not equal to what the OPRF function dictated, when I knew that the paper said that this unblinding process should do so
      • Make sure you know the difference between the prime number p of a Finite Integer Field Z_p, and the ORDER of Z_p. They are different numbers, It should be inverted w.r.t ORDER, not PRIME.

3. Further Work

This is basically most of the functionality of SPHINX completed in python. Now It will be time to prove that this concept works by implementing it into a working password store to demonstrate the power. I have a few ideas that also need to be implemented before I can go towards creating the front ends:

  • Have a configurable bytes to password generating system, for example, If passwords need to be a certain length or if they only contain certain characters.