Skip to content

Commit 16a8a74

Browse files
Implement new pinserver oracle protocol (#8)
Implement the new pinserver oracle protocol that only requires 2 steps.
1 parent 66c704b commit 16a8a74

File tree

9 files changed

+168
-222
lines changed

9 files changed

+168
-222
lines changed

README.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ SimpleJadePinServer
33

44
A simple reimplementation of the [blind_pin_server](https://github.com/Blockstream/blind_pin_server) for the Blockstream Jade hardware wallet, along with a very basic web interface.
55

6+
Note: The newest version of `SimpleJadePinServer` requires Jade firmware that includes support for the shorter two-step blind oracle protocol, which was first implemented in version 1.0.28. If you need to use the old four-step protocol, you can revert to [v1](https://github.com/Filiprogrammer/SimpleJadePinServer/tree/v1).
7+
68
Running SimpleJadePinServer
79
---------------------------
810

@@ -91,7 +93,7 @@ Scan the the generated QR code and then confirm the details on screen.
9193

9294
![Jade Confirm Pin Server](docs/images/jade_confirm_pin_server.png)
9395

94-
Note that it really does not matter where the URL is pointed to since the `SimpleJadePinServer` does not use the BCUR QR code from step 1/4. The only important parameter here is the public key of the pin server.
96+
Note that it really does not matter where the URL is pointed to. The only important parameter here is the public key of the pin server.
9597

9698
<details>
9799
<summary>Alternative setup via USB</summary>
@@ -112,24 +114,26 @@ Using SimpleJadePinServer
112114

113115
Once the Jade is configured to work with `SimpleJadePinServer`, initialize the wallet. When asked to select a connection, choose QR.
114116

115-
1. After providing a six digit pin, the Jade will display Step 1/4 with a series of BC-UR QR codes. This step can be skipped.
117+
### Step 1/2
118+
119+
After providing a six digit pin, the Jade will display Step 1/2 with a series of BC-UR QR codes. Click "Step 1/2 pin request - Jade &rarr; pin server" in the web interface. This will use the computer's camera to scan the QR codes displayed on the Jade.
116120

117-
2. The Jade will then display Step 2/4 and ask you to scan a BC-UR QR code. Make sure that `SimpleJadePinServer` is running and navigate to https://127.0.0.1:4443 in your web browser. Click the button labelled "Step 2/4 start_handshake". This will show a series of BC-UR QR codes you can scan with your Jade.
121+
![Web UI Step 1/2](docs/images/webui_step1.png)
118122

119-
![Step 2/4](docs/images/webui_step2.png)
123+
Once it is done scanning, the camera interface will automatically disappear.
120124

121-
3. Once scanning is complete, the Jade will proceed to Step 3/4 and show a series of BC-UR QR codes on its screen. Click "Step 3/4 start_handshake reply" in the web interface. This will use the computer's camera to scan the QR codes displayed on the Jade.
125+
### Step 2/2
122126

123-
![Step 3/4](docs/images/webui_step3.png)
127+
Next click the button labelled "Step 2/2 pin reply - pin server &rarr; Jade". This will show a series of BC-UR QR codes.
124128

125-
Once it is done scanning, the camera interface will automatically disappear.
129+
![Web UI Step 2/2](docs/images/webui_step2.png)
126130

127-
4. Continue to Step 4/4 on the Jade and click "set_pin" in the web interface. This will show another series of BC-UR QR codes which can be scanned by the Jade.
131+
On the Jade continue to Step 2/2 and scan the BC-UR QR codes.
128132

129-
![Step 4/4](docs/images/webui_step4.png)
133+
![Jade scanning Step 2/2](docs/images/jade_scanning_step2.png)
130134

131-
Once that is scanned, you are done and the wallet is ready to be used.
135+
Once scanning is complete, you are done and the wallet is ready to be used.
132136

133137
### Unlocking the wallet
134138

135-
If you want to unlock the wallet at some later point, select "QR Mode" -> "QR PIN Unlock" on the Jade. Enter your PIN and perform the same steps as described before. The only difference being at step 4/4 where you will have to click "get_pin" instead of "set_pin" in the web interface.
139+
If you want to unlock the wallet at some later point, select "QR Mode" -> "QR PIN Unlock" on the Jade. Enter your PIN and perform the same steps as described before.

SimpleJadePinServer.py

Lines changed: 84 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from http.server import BaseHTTPRequestHandler, HTTPServer
44
from hashlib import sha256
55
import argparse
6+
import base64
67
import os
78
import wallycore as wally
89
import urllib.parse
@@ -29,147 +30,130 @@ class MyServer(BaseHTTPRequestHandler):
2930
def do_GET(self):
3031
request = urllib.parse.urlparse(self.path)
3132

32-
if request.path == "/start_handshake":
33-
print("start_handshake")
34-
global private_key
35-
private_key = generate_private_key()
36-
ske = wally.ec_public_key_from_private_key(private_key)
37-
public_key_hash = sha256(ske).digest()
38-
sig = wally.ec_sig_from_bytes(STATIC_SERVER_PRIVATE_KEY, public_key_hash, wally.EC_FLAG_ECDSA)
39-
40-
self.send_response(200)
41-
self.send_header("Content-type", "application/json")
42-
self.end_headers()
43-
self.wfile.write(bytes('{"id":"0","method":"handshake_init","params":{"sig":"' + bytes2hex(sig) + '","ske":"' + bytes2hex(ske) + '"}}', "utf-8"))
44-
elif request.path == "/set_pin":
33+
if request.path == "/set_pin":
4534
print("set_pin")
4635
params = urllib.parse.parse_qs(request.query)
4736

48-
if not ('ske' in params and 'cke' in params and 'encrypted_data' in params and 'hmac_encrypted_data' in params):
37+
if not 'data' in params:
4938
self.send_response(400)
5039
self.send_header("Content-type", "text/html")
5140
self.end_headers()
5241
self.wfile.write(bytes("<html><head><title>Bad request</title></head><body>Bad request</body></html>", "utf-8"))
5342
return
5443

55-
if not 'private_key' in globals():
56-
self.send_response(409)
57-
self.send_header("Content-type", "text/html")
58-
self.end_headers()
59-
self.wfile.write(bytes("<html><head><title>Conflict</title></head><body>You have to start_handshake first</body></html>", "utf-8"))
60-
return
44+
data = base64.b64decode(params['data'][0].replace(' ', '+'))
45+
assert len(data) > 37
46+
cke = data[:33]
47+
replay_counter = data[33:37]
48+
encrypted_data = data[37:]
6149

62-
ske = hex2bytes(params['ske'][0])
63-
cke = hex2bytes(params['cke'][0])
64-
encrypted_data = hex2bytes(params['encrypted_data'][0])
65-
hmac_encrypted_data = hex2bytes(params['hmac_encrypted_data'][0])
50+
private_key, public_key = generate_ec_key_pair(replay_counter, cke)
6651

67-
master_shared_key = wally.ecdh(cke, private_key)
68-
request_encryption_key = wally.hmac_sha256(master_shared_key, bytearray([0]))
69-
request_hmac_key = wally.hmac_sha256(master_shared_key, bytearray([1]))
70-
response_encryption_key = wally.hmac_sha256(master_shared_key, bytearray([2]))
71-
response_hmac_key = wally.hmac_sha256(master_shared_key, bytearray([3]))
72-
hmac_calculated = wally.hmac_sha256(request_hmac_key, cke + encrypted_data)
73-
74-
iv = encrypted_data[:16]
75-
payload = wally.aes_cbc(request_encryption_key, iv, encrypted_data[16:], wally.AES_FLAG_DECRYPT)
52+
payload = wally.aes_cbc_with_ecdh_key(private_key, None, encrypted_data, cke, b'blind_oracle_request', wally.AES_FLAG_DECRYPT)
7653

54+
# set_pin requires client-passed entropy
55+
assert len(payload) == 32 + 32 + 65
7756
pin_secret = payload[:32]
7857
entropy = payload[32:64]
7958
sig = payload[64:]
80-
signed_msg = bytearray(sha256(cke + pin_secret + entropy).digest())
59+
signed_msg = bytearray(sha256(cke + replay_counter + pin_secret + entropy).digest())
8160
pin_pubkey = wally.ec_sig_to_public_key(signed_msg, sig)
8261

83-
our_random = bytearray(os.urandom(32))
84-
new_key = wally.hmac_sha256(our_random, entropy)
85-
8662
pin_pubkey_hash = sha256(pin_pubkey).digest()
87-
hash_pin_secret = sha256(pin_secret).digest()
8863

89-
save_pin_fields(pin_pubkey_hash, hash_pin_secret, new_key, pin_pubkey, 0)
64+
replay_local = None
65+
try:
66+
_, _, _, replay_local = load_pin_fields(pin_pubkey_hash, pin_pubkey)
67+
68+
# Enforce anti replay (client counter must be greater than the server counter)
69+
client_counter = int.from_bytes(replay_counter, byteorder='little', signed=False)
70+
server_counter = int.from_bytes(replay_local, byteorder='little', signed=False)
71+
assert client_counter > server_counter
72+
except FileNotFoundError:
73+
pass
74+
75+
our_random = os.urandom(32)
76+
new_key = wally.hmac_sha256(our_random, entropy)
9077

91-
response = wally.hmac_sha256(new_key, pin_secret)
78+
hash_pin_secret = sha256(pin_secret).digest()
79+
replay_bytes = b'\x00\x00\x00\x00'
80+
save_pin_fields(pin_pubkey_hash, hash_pin_secret, new_key, pin_pubkey, 0, replay_bytes)
81+
aes_key = wally.hmac_sha256(new_key, pin_secret)
9282

9383
iv = os.urandom(16)
94-
encrypted_key = iv + wally.aes_cbc(response_encryption_key, iv, response, wally.AES_FLAG_ENCRYPT)
95-
hmac = wally.hmac_sha256(response_hmac_key, encrypted_key)
84+
encrypted_key = wally.aes_cbc_with_ecdh_key(private_key, iv, aes_key, cke, b'blind_oracle_response', wally.AES_FLAG_ENCRYPT)
9685

9786
self.send_response(200)
9887
self.send_header("Content-type", "application/json")
9988
self.end_headers()
100-
self.wfile.write(bytes('{"id":"0","method":"handshake_complete","params":{"encrypted_key":"' + bytes2hex(encrypted_key) + '","hmac":"' + bytes2hex(hmac) + '"}}', "utf-8"))
89+
self.wfile.write(b'{"data":"' + base64.b64encode(encrypted_key) + b'"}')
10190
elif request.path == "/get_pin":
10291
print("get_pin")
10392
params = urllib.parse.parse_qs(request.query)
10493

105-
if not ('ske' in params and 'cke' in params and 'encrypted_data' in params and 'hmac_encrypted_data' in params):
94+
if not 'data' in params:
10695
self.send_response(400)
10796
self.send_header("Content-type", "text/html")
10897
self.end_headers()
10998
self.wfile.write(bytes("<html><head><title>Bad request</title></head><body>Bad request</body></html>", "utf-8"))
11099
return
111100

112-
if not 'private_key' in globals():
113-
self.send_response(409)
114-
self.send_header("Content-type", "text/html")
115-
self.end_headers()
116-
self.wfile.write(bytes("<html><head><title>Conflict</title></head><body>You have to start_handshake first</body></html>", "utf-8"))
117-
return
118-
119-
ske = hex2bytes(params['ske'][0])
120-
cke = hex2bytes(params['cke'][0])
121-
encrypted_data = hex2bytes(params['encrypted_data'][0])
122-
hmac_encrypted_data = hex2bytes(params['hmac_encrypted_data'][0])
101+
data = base64.b64decode(params['data'][0].replace(' ', '+'))
102+
assert len(data) > 37
103+
cke = data[:33]
104+
replay_counter = data[33:37]
105+
encrypted_data = data[37:]
123106

124-
master_shared_key = wally.ecdh(cke, private_key)
125-
request_encryption_key = wally.hmac_sha256(master_shared_key, bytearray([0]))
126-
request_hmac_key = wally.hmac_sha256(master_shared_key, bytearray([1]))
127-
response_encryption_key = wally.hmac_sha256(master_shared_key, bytearray([2]))
128-
response_hmac_key = wally.hmac_sha256(master_shared_key, bytearray([3]))
129-
hmac_calculated = wally.hmac_sha256(request_hmac_key, cke + encrypted_data)
107+
private_key, public_key = generate_ec_key_pair(replay_counter, cke)
130108

131-
iv = encrypted_data[:16]
132-
payload = wally.aes_cbc(request_encryption_key, iv, encrypted_data[16:], wally.AES_FLAG_DECRYPT)
109+
payload = wally.aes_cbc_with_ecdh_key(private_key, None, encrypted_data, cke, b'blind_oracle_request', wally.AES_FLAG_DECRYPT)
133110

111+
# get_pin does not need client-passed entropy
112+
assert len(payload) == 32 + 65
134113
pin_secret = payload[:32]
135-
entropy = payload[32:64]
136-
sig = payload[64:]
137-
signed_msg = bytearray(sha256(cke + pin_secret + entropy).digest())
114+
sig = payload[32:]
115+
signed_msg = bytearray(sha256(cke + replay_counter + pin_secret).digest())
138116
pin_pubkey = wally.ec_sig_to_public_key(signed_msg, sig)
139117

140118
pin_pubkey_hash = sha256(pin_pubkey).digest()
141119

142-
saved_hash_pin_secret, saved_key, counter = load_pin_fields(pin_pubkey_hash, pin_pubkey)
120+
try:
121+
saved_hash_pin_secret, saved_key, counter, replay_local = load_pin_fields(pin_pubkey_hash, pin_pubkey)
143122

144-
hash_pin_secret = sha256(pin_secret).digest()
145-
146-
if hash_pin_secret == saved_hash_pin_secret:
147-
print("Correct pin on the " + str(counter + 1) + ". attempt")
148-
149-
if counter != 0:
150-
save_pin_fields(pin_pubkey_hash, hash_pin_secret, saved_key, pin_pubkey, 0)
123+
# Enforce anti replay (client counter must be greater than the server counter)
124+
client_counter = int.from_bytes(replay_counter, byteorder='little', signed=False)
125+
server_counter = int.from_bytes(replay_local, byteorder='little', signed=False)
126+
assert client_counter > server_counter
127+
except FileNotFoundError:
128+
# Return a random incorrect key to the Jade
129+
saved_key = os.urandom(32)
151130
else:
152-
print("Wrong pin (" + str(counter + 1) + ". attempt)")
131+
hash_pin_secret = sha256(pin_secret).digest()
153132

154-
if counter >= 2:
155-
os.remove(pins_path + "/" + bytes2hex(pin_pubkey_hash) + ".pin")
156-
print("Too many wrong attempts")
133+
if hash_pin_secret == saved_hash_pin_secret:
134+
print("Correct pin on the " + str(counter + 1) + ". attempt")
135+
save_pin_fields(pin_pubkey_hash, hash_pin_secret, saved_key, pin_pubkey, 0, replay_counter)
157136
else:
158-
save_pin_fields(pin_pubkey_hash, saved_hash_pin_secret, saved_key, pin_pubkey, counter + 1)
137+
print("Wrong pin (" + str(counter + 1) + ". attempt)")
159138

160-
# Return a random incorrect key to the Jade
161-
saved_key = os.urandom(32)
139+
if counter >= 2:
140+
os.remove(pins_path + "/" + bytes2hex(pin_pubkey_hash) + ".pin")
141+
print("Too many wrong attempts")
142+
else:
143+
save_pin_fields(pin_pubkey_hash, saved_hash_pin_secret, saved_key, pin_pubkey, counter + 1, replay_counter)
144+
145+
# Return a random incorrect key to the Jade
146+
saved_key = os.urandom(32)
162147

163-
response = wally.hmac_sha256(saved_key, pin_secret)
148+
aes_key = wally.hmac_sha256(saved_key, pin_secret)
164149

165150
iv = os.urandom(16)
166-
encrypted_key = iv + wally.aes_cbc(response_encryption_key, iv, response, wally.AES_FLAG_ENCRYPT)
167-
hmac = wally.hmac_sha256(response_hmac_key, encrypted_key)
151+
encrypted_key = wally.aes_cbc_with_ecdh_key(private_key, iv, aes_key, cke, b'blind_oracle_response', wally.AES_FLAG_ENCRYPT)
168152

169153
self.send_response(200)
170154
self.send_header("Content-type", "application/json")
171155
self.end_headers()
172-
self.wfile.write(bytes('{"id":"0","method":"handshake_complete","params":{"encrypted_key":"' + bytes2hex(encrypted_key) + '","hmac":"' + bytes2hex(hmac) + '"}}', "utf-8"))
156+
self.wfile.write(b'{"data":"' + base64.b64encode(encrypted_key) + b'"}')
173157
elif request.path == "/qrcode.js":
174158
self.send_response(200)
175159
self.send_header("Content-type", "text/javascript")
@@ -199,14 +183,14 @@ def do_GET(self):
199183
self.end_headers()
200184
self.wfile.write(bytes("<html><head><title>Not found</title></head><body>Not found</body></html>", "utf-8"))
201185

202-
def save_pin_fields(pin_pubkey_hash, hash_pin_secret, aes_key, pin_pubkey, counter):
186+
def save_pin_fields(pin_pubkey_hash, hash_pin_secret, aes_key, pin_pubkey, counter, replay_counter):
203187
storage_aes_key = wally.hmac_sha256(STATIC_SERVER_AES_PIN_DATA, pin_pubkey)
204188
count_bytes = counter.to_bytes(1)
205-
plaintext = hash_pin_secret + aes_key + count_bytes
189+
plaintext = hash_pin_secret + aes_key + count_bytes + replay_counter
206190
iv = os.urandom(16)
207191
encrypted = iv + wally.aes_cbc(storage_aes_key, iv, plaintext, wally.AES_FLAG_ENCRYPT)
208192
pin_auth_key = wally.hmac_sha256(STATIC_SERVER_AES_PIN_DATA, pin_pubkey_hash)
209-
version_bytes = b'\x00'
193+
version_bytes = b'\x01'
210194
hmac_payload = wally.hmac_sha256(pin_auth_key, version_bytes + encrypted)
211195

212196
os.makedirs(pins_path, exist_ok=True)
@@ -220,7 +204,7 @@ def load_pin_fields(pin_pubkey_hash, pin_pubkey):
220204

221205
assert len(data) == 129
222206
version_bytes = data[:1]
223-
assert version_bytes[0] == 0
207+
assert version_bytes[0] == 1
224208
hmac_received = data[1:33]
225209
encrypted = data[33:]
226210
pin_auth_key = wally.hmac_sha256(STATIC_SERVER_AES_PIN_DATA, pin_pubkey_hash)
@@ -230,20 +214,29 @@ def load_pin_fields(pin_pubkey_hash, pin_pubkey):
230214
storage_aes_key = wally.hmac_sha256(STATIC_SERVER_AES_PIN_DATA, pin_pubkey)
231215
iv = encrypted[:16]
232216
plaintext = wally.aes_cbc(storage_aes_key, iv, encrypted[16:], wally.AES_FLAG_DECRYPT)
233-
assert len(plaintext) == 32 + 32 + 1
217+
assert len(plaintext) == 32 + 32 + 1 + 4
234218

235219
saved_hash_pin_secret = plaintext[:32]
236220
saved_key = plaintext[32:64]
237221
counter = plaintext[64]
222+
replay_counter_persisted = plaintext[65:69]
238223

239-
return saved_hash_pin_secret, saved_key, counter
224+
return saved_hash_pin_secret, saved_key, counter, replay_counter_persisted
240225

241226
def bytes2hex(byte_array):
242227
return ''.join('{:02x}'.format(x) for x in byte_array)
243228

244229
def hex2bytes(hex):
245230
return bytearray(bytes.fromhex(hex))
246231

232+
def generate_ec_key_pair(replay_counter, cke):
233+
tweak = sha256(wally.hmac_sha256(cke, replay_counter)).digest()
234+
private_key = wally.ec_private_key_bip341_tweak(STATIC_SERVER_PRIVATE_KEY, tweak, 0)
235+
wally.ec_private_key_verify(private_key)
236+
public_key = wally.ec_public_key_from_private_key(private_key)
237+
238+
return private_key, public_key
239+
247240
def generate_private_key():
248241
while True:
249242
private_key = bytearray(os.urandom(32))
214 KB
Loading

docs/images/webui.png

-2.67 KB
Loading

docs/images/webui_step1.png

213 KB
Loading

docs/images/webui_step2.png

-1.9 KB
Loading

docs/images/webui_step3.png

-51.5 KB
Binary file not shown.

docs/images/webui_step4.png

-7.58 KB
Binary file not shown.

0 commit comments

Comments
 (0)