pakstr 0.19.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android-template/app/build.gradle.kts +4 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +273 -0
- package/android-template/app/src/main/assets/www/index.html +0 -6
- package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +20 -4
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +136 -12
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Hash.kt +56 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Signer.kt +104 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +20 -170
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip44Crypto.kt +513 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip46ClientKey.kt +140 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +3 -28
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventSerializer.kt +74 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +100 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +32 -7
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +36 -9
- package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +240 -163
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConfig.kt +175 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConnectionStore.kt +144 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Client.kt +79 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Models.kt +319 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingClient.kt +159 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingQrCode.kt +35 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingUri.kt +104 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Protocol.kt +695 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46RelayTransport.kt +489 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/RelayNip46Client.kt +403 -0
- package/android-template/app/src/main/java/com/pakstr/app/utils/BunkerPairingDialog.kt +82 -0
- package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +78 -17
- package/android-template/app/src/main/res/layout/activity_layout.xml +11 -0
- package/android-template/app/src/main/res/layout/dialog_bunker_pairing.xml +75 -0
- package/android-template/app/src/main/res/layout/dialog_picker.xml +60 -31
- package/android-template/app/src/main/res/values/strings.xml +27 -0
- package/android-template/app/src/test/java/com/pakstr/app/LocalServerQueryKeepAliveTest.kt +118 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Bip340SignerTest.kt +133 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip44CryptoTest.kt +199 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyProtocolTest.kt +82 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyTest.kt +52 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/NostrEventSerializerTest.kt +42 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeErrorTest.kt +32 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/BunkerConfigTest.kt +120 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ModelsTest.kt +111 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingClientTest.kt +271 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingQrCodeTest.kt +54 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingUriTest.kt +80 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ProtocolTest.kt +578 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46RelayTransportTest.kt +508 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/RelayNip46ClientTest.kt +678 -0
- package/android-template/gradle/libs.versions.toml +8 -0
- package/dist/cli.js +8 -0
- package/dist/commands/init.js +16 -0
- package/dist/commands/nsite.js +38 -0
- package/dist/commands/publish.js +50 -2
- package/dist/commands/run.js +5 -5
- package/dist/core/blossom.js +2 -2
- package/dist/core/nsite.js +176 -0
- package/dist/core/pakstrConfig.js +85 -6
- package/package.json +1 -1
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<LinearLayout xmlns:
|
|
3
|
-
xmlns:
|
|
2
|
+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
4
4
|
android:layout_width="match_parent"
|
|
5
5
|
android:layout_height="wrap_content"
|
|
6
6
|
android:background="@drawable/bg_bottom_sheet"
|
|
7
7
|
android:orientation="vertical"
|
|
8
|
-
android:paddingBottom="32dp"
|
|
9
|
-
android:paddingEnd="24dp"
|
|
10
8
|
android:paddingStart="24dp"
|
|
11
|
-
android:paddingTop="16dp"
|
|
9
|
+
android:paddingTop="16dp"
|
|
10
|
+
android:paddingEnd="24dp"
|
|
11
|
+
android:paddingBottom="32dp">
|
|
12
12
|
|
|
13
|
-
<!-- Handle Indicator (Modern Pill) -->
|
|
14
13
|
<View
|
|
15
14
|
android:layout_width="48dp"
|
|
16
15
|
android:layout_height="5dp"
|
|
@@ -18,34 +17,30 @@
|
|
|
18
17
|
android:layout_marginBottom="28dp"
|
|
19
18
|
android:background="#E5E7EB" />
|
|
20
19
|
|
|
21
|
-
<!-- Header Title -->
|
|
22
20
|
<TextView
|
|
23
21
|
android:layout_width="wrap_content"
|
|
24
22
|
android:layout_height="wrap_content"
|
|
25
|
-
android:
|
|
23
|
+
android:fontFamily="sans-serif-medium"
|
|
24
|
+
android:text="@string/signer_picker_title"
|
|
26
25
|
android:textColor="#1F2937"
|
|
27
26
|
android:textSize="24sp"
|
|
28
|
-
android:fontFamily="sans-serif-medium"
|
|
29
27
|
android:textStyle="bold" />
|
|
30
28
|
|
|
31
|
-
<!-- Header Subtitle -->
|
|
32
29
|
<TextView
|
|
33
30
|
android:layout_width="wrap_content"
|
|
34
31
|
android:layout_height="wrap_content"
|
|
35
|
-
android:layout_marginBottom="24dp"
|
|
36
32
|
android:layout_marginTop="6dp"
|
|
37
|
-
android:
|
|
33
|
+
android:layout_marginBottom="24dp"
|
|
34
|
+
android:text="@string/signer_picker_subtitle"
|
|
38
35
|
android:textColor="#4B5563"
|
|
39
36
|
android:textSize="15sp" />
|
|
40
37
|
|
|
41
|
-
<!-- RadioGroup za selekciju opcija -->
|
|
42
38
|
<RadioGroup
|
|
43
39
|
android:id="@+id/options_radio_group"
|
|
44
40
|
android:layout_width="match_parent"
|
|
45
41
|
android:layout_height="wrap_content"
|
|
46
42
|
android:orientation="vertical">
|
|
47
43
|
|
|
48
|
-
<!-- OPCIJA 1: Launch Web Application -->
|
|
49
44
|
<RadioButton
|
|
50
45
|
android:id="@+id/radio_option_one"
|
|
51
46
|
android:layout_width="match_parent"
|
|
@@ -54,48 +49,82 @@
|
|
|
54
49
|
android:background="@drawable/selector_radio_card"
|
|
55
50
|
android:button="@null"
|
|
56
51
|
android:checked="true"
|
|
57
|
-
android:drawableEnd="@drawable/selector_radio_checkmark"
|
|
58
52
|
android:drawableStart="@drawable/ic_menu_browser"
|
|
53
|
+
android:drawableEnd="@drawable/selector_radio_checkmark"
|
|
59
54
|
android:drawablePadding="16dp"
|
|
60
55
|
android:drawableTint="@drawable/selector_radio_tint"
|
|
61
|
-
android:
|
|
56
|
+
android:fontFamily="sans-serif-medium"
|
|
62
57
|
android:paddingStart="20dp"
|
|
63
|
-
android:
|
|
58
|
+
android:paddingEnd="20dp"
|
|
59
|
+
android:text="@string/signer_picker_amber"
|
|
64
60
|
android:textColor="#1F2937"
|
|
65
|
-
android:textSize="16sp"
|
|
66
|
-
android:fontFamily="sans-serif-medium" />
|
|
61
|
+
android:textSize="16sp" />
|
|
67
62
|
|
|
68
|
-
<!-- OPCIJA 2: Bunker -->
|
|
69
63
|
<RadioButton
|
|
70
64
|
android:id="@+id/radio_option_two"
|
|
71
65
|
android:layout_width="match_parent"
|
|
72
66
|
android:layout_height="64dp"
|
|
73
|
-
android:layout_marginBottom="
|
|
67
|
+
android:layout_marginBottom="12dp"
|
|
74
68
|
android:background="@drawable/selector_radio_card"
|
|
75
69
|
android:button="@null"
|
|
76
|
-
android:drawableEnd="@drawable/selector_radio_checkmark"
|
|
77
70
|
android:drawableStart="@drawable/ic_menu_browser"
|
|
71
|
+
android:drawableEnd="@drawable/selector_radio_checkmark"
|
|
78
72
|
android:drawablePadding="16dp"
|
|
79
73
|
android:drawableTint="@drawable/selector_radio_tint"
|
|
80
|
-
android:
|
|
74
|
+
android:fontFamily="sans-serif-medium"
|
|
81
75
|
android:paddingStart="20dp"
|
|
82
|
-
android:
|
|
76
|
+
android:paddingEnd="20dp"
|
|
77
|
+
android:text="@string/signer_picker_bunker"
|
|
83
78
|
android:textColor="#1F2937"
|
|
84
|
-
android:textSize="16sp"
|
|
85
|
-
android:fontFamily="sans-serif-medium" />
|
|
86
|
-
|
|
79
|
+
android:textSize="16sp" />
|
|
87
80
|
</RadioGroup>
|
|
88
81
|
|
|
89
|
-
|
|
82
|
+
<com.google.android.material.textfield.TextInputLayout
|
|
83
|
+
android:id="@+id/bunker_uri_container"
|
|
84
|
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
85
|
+
android:layout_width="match_parent"
|
|
86
|
+
android:layout_height="wrap_content"
|
|
87
|
+
android:layout_marginBottom="12dp"
|
|
88
|
+
android:hint="@string/signer_picker_bunker_hint"
|
|
89
|
+
android:visibility="gone">
|
|
90
|
+
|
|
91
|
+
<com.google.android.material.textfield.TextInputEditText
|
|
92
|
+
android:id="@+id/bunker_uri"
|
|
93
|
+
android:layout_width="match_parent"
|
|
94
|
+
android:layout_height="wrap_content"
|
|
95
|
+
android:inputType="textUri"
|
|
96
|
+
android:maxLines="4" />
|
|
97
|
+
</com.google.android.material.textfield.TextInputLayout>
|
|
98
|
+
|
|
99
|
+
<com.google.android.material.button.MaterialButton
|
|
100
|
+
android:id="@+id/button_share_pairing"
|
|
101
|
+
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
|
102
|
+
android:layout_width="match_parent"
|
|
103
|
+
android:layout_height="48dp"
|
|
104
|
+
android:layout_marginBottom="12dp"
|
|
105
|
+
android:text="@string/signer_picker_share_pairing"
|
|
106
|
+
android:textAllCaps="false"
|
|
107
|
+
android:visibility="gone" />
|
|
108
|
+
|
|
109
|
+
<com.google.android.material.button.MaterialButton
|
|
110
|
+
android:id="@+id/button_logout_bunker"
|
|
111
|
+
style="@style/Widget.MaterialComponents.Button.TextButton"
|
|
112
|
+
android:layout_width="match_parent"
|
|
113
|
+
android:layout_height="48dp"
|
|
114
|
+
android:layout_marginBottom="12dp"
|
|
115
|
+
android:text="@string/signer_picker_logout_bunker"
|
|
116
|
+
android:textAllCaps="false"
|
|
117
|
+
android:visibility="gone" />
|
|
118
|
+
|
|
90
119
|
<com.google.android.material.button.MaterialButton
|
|
91
120
|
android:id="@+id/button_confirm"
|
|
92
121
|
android:layout_width="match_parent"
|
|
93
122
|
android:layout_height="56dp"
|
|
94
|
-
android:
|
|
123
|
+
android:backgroundTint="#2563EB"
|
|
124
|
+
android:text="@string/signer_picker_continue"
|
|
95
125
|
android:textAllCaps="false"
|
|
96
|
-
android:textSize="16sp"
|
|
97
126
|
android:textColor="@android:color/white"
|
|
98
|
-
android:
|
|
127
|
+
android:textSize="16sp"
|
|
99
128
|
app:cornerRadius="16dp" />
|
|
100
129
|
|
|
101
130
|
</LinearLayout>
|
|
@@ -44,4 +44,31 @@
|
|
|
44
44
|
|
|
45
45
|
<string name="developer_tools">Open Developer Tools</string>
|
|
46
46
|
<string name="diagnostics_mode_enabled">Diagnostics mode enabled</string>
|
|
47
|
+
|
|
48
|
+
<string name="signer_settings">Signer settings</string>
|
|
49
|
+
<string name="signer_picker_title">Choose a signer</string>
|
|
50
|
+
<string name="signer_picker_subtitle">Use Amber or connect to a NIP-46 remote signer.</string>
|
|
51
|
+
<string name="signer_picker_amber">Continue with Amber</string>
|
|
52
|
+
<string name="signer_picker_bunker">Connect to remote signer (Bunker)</string>
|
|
53
|
+
<string name="signer_picker_bunker_hint">Paste a bunker:// connection</string>
|
|
54
|
+
<string name="signer_picker_share_pairing">Share a nostrconnect:// pairing</string>
|
|
55
|
+
<string name="signer_picker_logout_bunker">Disconnect remote signer</string>
|
|
56
|
+
<string name="signer_picker_continue">Continue</string>
|
|
57
|
+
<string name="signer_picker_invalid_bunker">Enter a valid bunker:// connection.</string>
|
|
58
|
+
<string name="signer_picker_pairing_relays">Pairing relays (wss://, one per line)</string>
|
|
59
|
+
<string name="signer_picker_pairing_relays_default">wss://nos.lol\nwss://relay.primal.net\nwss://offchain.pub</string>
|
|
60
|
+
<string name="signer_picker_pairing_share_title">Pair with remote signer</string>
|
|
61
|
+
<string name="signer_picker_pairing_qr_title">Scan with Amber</string>
|
|
62
|
+
<string name="signer_picker_pairing_qr_instructions">In Amber, choose “Scan a QR code”, then scan this code. You can also copy or share the exact pairing URI.</string>
|
|
63
|
+
<string name="signer_picker_pairing_qr_content_description">QR code containing the nostrconnect pairing URI</string>
|
|
64
|
+
<string name="signer_picker_pairing_copy">Copy pairing URI</string>
|
|
65
|
+
<string name="signer_picker_pairing_share">Share pairing URI</string>
|
|
66
|
+
<string name="signer_picker_pairing_clip_label">Nostrconnect pairing URI</string>
|
|
67
|
+
<string name="signer_picker_pairing_copied">Pairing URI copied.</string>
|
|
68
|
+
<string name="signer_picker_pairing_waiting">Waiting for the remote signer to connect…</string>
|
|
69
|
+
<string name="signer_picker_pairing_connected">Remote signer connected.</string>
|
|
70
|
+
<string name="signer_picker_pairing_failed">Remote signer pairing timed out.</string>
|
|
71
|
+
<string name="signer_picker_connection_failed">Unable to configure the remote signer.</string>
|
|
72
|
+
<string name="signer_picker_disconnected">Remote signer disconnected.</string>
|
|
73
|
+
<string name="signer_auth_browser_unavailable">No browser is available for signer authorization.</string>
|
|
47
74
|
</resources>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package com.pakstr.app
|
|
2
|
+
|
|
3
|
+
import fi.iki.elonen.NanoHTTPD
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertFalse
|
|
6
|
+
import org.junit.Assert.assertNull
|
|
7
|
+
import org.junit.Assert.assertTrue
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import java.io.BufferedInputStream
|
|
10
|
+
import java.io.ByteArrayOutputStream
|
|
11
|
+
import java.io.InputStream
|
|
12
|
+
import java.net.Socket
|
|
13
|
+
import java.nio.charset.StandardCharsets
|
|
14
|
+
import java.util.Collections
|
|
15
|
+
|
|
16
|
+
class LocalServerQueryKeepAliveTest {
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun queryDoesNotLeakToNextRequestOnKeepAliveSession() {
|
|
20
|
+
val requests = Collections.synchronizedList(mutableListOf<CapturedRequest>())
|
|
21
|
+
val server = object : NanoHTTPD(0) {
|
|
22
|
+
override fun serve(session: IHTTPSession): Response {
|
|
23
|
+
val query = currentRawQueryString(session)
|
|
24
|
+
requests += CapturedRequest(
|
|
25
|
+
uri = session.uri,
|
|
26
|
+
rawQuery = session.queryParameterString,
|
|
27
|
+
query = query,
|
|
28
|
+
localUrl = localRequestUrl(session.uri, query).toString()
|
|
29
|
+
)
|
|
30
|
+
return newFixedLengthResponse("ok")
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
val encodedEvent = "%7B%22content%22%3A%22${"event".repeat(512)}%22%7D"
|
|
34
|
+
val callbackQuery = "nip55_event=$encodedEvent"
|
|
35
|
+
|
|
36
|
+
server.start()
|
|
37
|
+
try {
|
|
38
|
+
Socket("127.0.0.1", server.listeningPort).use { socket ->
|
|
39
|
+
socket.soTimeout = 5000
|
|
40
|
+
val input = BufferedInputStream(socket.getInputStream())
|
|
41
|
+
val output = socket.getOutputStream()
|
|
42
|
+
|
|
43
|
+
output.write(
|
|
44
|
+
(
|
|
45
|
+
"GET /?$callbackQuery HTTP/1.1\r\n" +
|
|
46
|
+
"Host: 127.0.0.1\r\n\r\n"
|
|
47
|
+
).toByteArray(StandardCharsets.ISO_8859_1)
|
|
48
|
+
)
|
|
49
|
+
output.flush()
|
|
50
|
+
readResponse(input)
|
|
51
|
+
|
|
52
|
+
output.write(
|
|
53
|
+
(
|
|
54
|
+
"POST /api/auth/login HTTP/1.1\r\n" +
|
|
55
|
+
"Host: 127.0.0.1\r\n" +
|
|
56
|
+
"Content-Length: 0\r\n\r\n"
|
|
57
|
+
).toByteArray(StandardCharsets.ISO_8859_1)
|
|
58
|
+
)
|
|
59
|
+
output.flush()
|
|
60
|
+
readResponse(input)
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
server.stop()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
assertEquals(2, requests.size)
|
|
67
|
+
assertEquals(callbackQuery, requests[0].query)
|
|
68
|
+
|
|
69
|
+
val loginRequest = requests[1]
|
|
70
|
+
assertEquals("/api/auth/login", loginRequest.uri)
|
|
71
|
+
assertEquals(callbackQuery, loginRequest.rawQuery)
|
|
72
|
+
assertNull(loginRequest.query)
|
|
73
|
+
assertEquals(
|
|
74
|
+
"http://127.0.0.1:${Config.PORT}/api/auth/login",
|
|
75
|
+
loginRequest.localUrl
|
|
76
|
+
)
|
|
77
|
+
assertFalse(loginRequest.localUrl.contains("nip55_event"))
|
|
78
|
+
assertFalse(loginRequest.localUrl.contains(encodedEvent))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun readResponse(input: InputStream) {
|
|
82
|
+
val headers = mutableMapOf<String, String>()
|
|
83
|
+
assertTrue(readLine(input).startsWith("HTTP/1.1 200"))
|
|
84
|
+
while (true) {
|
|
85
|
+
val line = readLine(input)
|
|
86
|
+
if (line.isEmpty()) break
|
|
87
|
+
val separator = line.indexOf(':')
|
|
88
|
+
assertTrue(separator > 0)
|
|
89
|
+
headers[line.substring(0, separator).lowercase()] =
|
|
90
|
+
line.substring(separator + 1).trim()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
val contentLength = headers["content-length"]?.toInt() ?: 0
|
|
94
|
+
repeat(contentLength) {
|
|
95
|
+
assertTrue(input.read() != -1)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun readLine(input: InputStream): String {
|
|
100
|
+
val bytes = ByteArrayOutputStream()
|
|
101
|
+
while (true) {
|
|
102
|
+
val value = input.read()
|
|
103
|
+
assertTrue(value != -1)
|
|
104
|
+
if (value == '\r'.code) {
|
|
105
|
+
assertEquals('\n'.code, input.read())
|
|
106
|
+
return bytes.toString(StandardCharsets.ISO_8859_1.name())
|
|
107
|
+
}
|
|
108
|
+
bytes.write(value)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private data class CapturedRequest(
|
|
113
|
+
val uri: String,
|
|
114
|
+
val rawQuery: String?,
|
|
115
|
+
val query: String?,
|
|
116
|
+
val localUrl: String
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertArrayEquals
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertThrows
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
class Bip340SignerTest {
|
|
10
|
+
|
|
11
|
+
@Test
|
|
12
|
+
fun matches_official_signing_vectors() {
|
|
13
|
+
VECTORS.forEach { vector ->
|
|
14
|
+
val privateKey = Bip340Hash.hexToBytes(vector.privateKey)
|
|
15
|
+
val message = Bip340Hash.hexToBytes(vector.message)
|
|
16
|
+
val auxiliaryRandom =
|
|
17
|
+
Bip340Hash.hexToBytes(vector.auxiliaryRandom)
|
|
18
|
+
|
|
19
|
+
val signature =
|
|
20
|
+
Bip340Signer.sign(
|
|
21
|
+
privateKey,
|
|
22
|
+
message,
|
|
23
|
+
auxiliaryRandom
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assertEquals(
|
|
27
|
+
vector.publicKey.lowercase(),
|
|
28
|
+
Bip340Hash.bytesToHex(
|
|
29
|
+
Secp256k1.xOnlyPublicKey(privateKey)
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
assertEquals(
|
|
33
|
+
vector.signature.lowercase(),
|
|
34
|
+
Bip340Hash.bytesToHex(signature)
|
|
35
|
+
)
|
|
36
|
+
assertTrue(
|
|
37
|
+
Bip340Verifier.verify(
|
|
38
|
+
vector.publicKey,
|
|
39
|
+
vector.message,
|
|
40
|
+
vector.signature
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun rejects_invalid_signing_inputs() {
|
|
48
|
+
val validKey = ByteArray(32).also { it[31] = 1 }
|
|
49
|
+
|
|
50
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
51
|
+
Bip340Signer.sign(
|
|
52
|
+
ByteArray(32),
|
|
53
|
+
ByteArray(32),
|
|
54
|
+
ByteArray(32)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
58
|
+
Bip340Signer.sign(
|
|
59
|
+
validKey,
|
|
60
|
+
ByteArray(31),
|
|
61
|
+
ByteArray(32)
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
65
|
+
Bip340Signer.sign(
|
|
66
|
+
validKey,
|
|
67
|
+
ByteArray(32),
|
|
68
|
+
ByteArray(31)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
fun x_only_public_key_is_defensive_copy() {
|
|
75
|
+
val key =
|
|
76
|
+
Nip46ClientKey.fromPrivateKey(
|
|
77
|
+
ByteArray(32).also { it[31] = 3 }
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
key.use {
|
|
81
|
+
val first = key.publicKey
|
|
82
|
+
first.fill(0)
|
|
83
|
+
|
|
84
|
+
assertArrayEquals(
|
|
85
|
+
Bip340Hash.hexToBytes(VECTORS.first().publicKey),
|
|
86
|
+
key.publicKey
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private data class Vector(
|
|
92
|
+
val privateKey: String,
|
|
93
|
+
val publicKey: String,
|
|
94
|
+
val auxiliaryRandom: String,
|
|
95
|
+
val message: String,
|
|
96
|
+
val signature: String
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
companion object {
|
|
100
|
+
// https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv
|
|
101
|
+
private val VECTORS =
|
|
102
|
+
listOf(
|
|
103
|
+
Vector(
|
|
104
|
+
"0000000000000000000000000000000000000000000000000000000000000003",
|
|
105
|
+
"F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
|
|
106
|
+
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
107
|
+
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
108
|
+
"E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0"
|
|
109
|
+
),
|
|
110
|
+
Vector(
|
|
111
|
+
"B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF",
|
|
112
|
+
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
|
|
113
|
+
"0000000000000000000000000000000000000000000000000000000000000001",
|
|
114
|
+
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
|
|
115
|
+
"6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A"
|
|
116
|
+
),
|
|
117
|
+
Vector(
|
|
118
|
+
"C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9",
|
|
119
|
+
"DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",
|
|
120
|
+
"C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906",
|
|
121
|
+
"7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C",
|
|
122
|
+
"5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7"
|
|
123
|
+
),
|
|
124
|
+
Vector(
|
|
125
|
+
"0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710",
|
|
126
|
+
"25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517",
|
|
127
|
+
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
|
128
|
+
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
|
129
|
+
"7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3"
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertArrayEquals
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertFalse
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Assert.assertThrows
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import java.util.Base64
|
|
10
|
+
|
|
11
|
+
class Nip44CryptoTest {
|
|
12
|
+
|
|
13
|
+
@Test
|
|
14
|
+
fun matches_official_conversation_key_vector() {
|
|
15
|
+
val conversationKey =
|
|
16
|
+
Nip44Crypto.deriveConversationKey(
|
|
17
|
+
hex("315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268"),
|
|
18
|
+
hex("c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133")
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
assertEquals(
|
|
22
|
+
"3dfef0ce2a4d80a25e7a328accf73448ef67096f65f79588e358d9a0eb9013f1",
|
|
23
|
+
Bip340Hash.bytesToHex(conversationKey)
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
fun matches_official_message_key_vector() {
|
|
29
|
+
val keys =
|
|
30
|
+
Nip44Crypto.deriveMessageKeys(
|
|
31
|
+
hex("a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54"),
|
|
32
|
+
hex("e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72")
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
keys.use {
|
|
36
|
+
assertEquals(
|
|
37
|
+
"f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76",
|
|
38
|
+
Bip340Hash.bytesToHex(keys.chachaKey)
|
|
39
|
+
)
|
|
40
|
+
assertEquals(
|
|
41
|
+
"c4ad129bb01180c0933a160c",
|
|
42
|
+
Bip340Hash.bytesToHex(keys.chachaNonce)
|
|
43
|
+
)
|
|
44
|
+
assertEquals(
|
|
45
|
+
"027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4",
|
|
46
|
+
Bip340Hash.bytesToHex(keys.hmacKey)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Test
|
|
52
|
+
fun matches_official_encryption_vector() {
|
|
53
|
+
val conversationKey =
|
|
54
|
+
hex("c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d")
|
|
55
|
+
val nonce =
|
|
56
|
+
hex("0000000000000000000000000000000000000000000000000000000000000001")
|
|
57
|
+
|
|
58
|
+
val payload =
|
|
59
|
+
Nip44Crypto.encryptWithNonce(
|
|
60
|
+
"a",
|
|
61
|
+
conversationKey,
|
|
62
|
+
nonce
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
assertEquals(EXPECTED_PAYLOAD, payload)
|
|
66
|
+
assertEquals(
|
|
67
|
+
"a",
|
|
68
|
+
Nip44Crypto.decryptWithConversationKey(
|
|
69
|
+
payload,
|
|
70
|
+
conversationKey
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
fun padding_matches_official_boundaries() {
|
|
77
|
+
mapOf(
|
|
78
|
+
1L to 32L,
|
|
79
|
+
16L to 32L,
|
|
80
|
+
32L to 32L,
|
|
81
|
+
33L to 64L,
|
|
82
|
+
65L to 96L,
|
|
83
|
+
200L to 224L,
|
|
84
|
+
250L to 256L,
|
|
85
|
+
257L to 320L,
|
|
86
|
+
515L to 640L,
|
|
87
|
+
65535L to 65536L,
|
|
88
|
+
65536L to 65536L,
|
|
89
|
+
65537L to 81920L
|
|
90
|
+
).forEach { (input, expected) ->
|
|
91
|
+
assertEquals(
|
|
92
|
+
expected,
|
|
93
|
+
Nip44Crypto.calculatePaddedLength(input)
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
fun encrypt_round_trip_uses_fresh_nonce() {
|
|
100
|
+
val privateKey = ByteArray(32).also { it[31] = 1 }
|
|
101
|
+
val publicKey =
|
|
102
|
+
Secp256k1.xOnlyPublicKey(
|
|
103
|
+
ByteArray(32).also { it[31] = 2 }
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
val first = Nip44Crypto.encrypt("hello", privateKey, publicKey)
|
|
107
|
+
val second = Nip44Crypto.encrypt("hello", privateKey, publicKey)
|
|
108
|
+
|
|
109
|
+
assertFalse(first == second)
|
|
110
|
+
assertEquals(
|
|
111
|
+
"hello",
|
|
112
|
+
Nip44Crypto.decrypt(first, privateKey, publicKey)
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Test
|
|
117
|
+
fun rejects_empty_plaintext_and_non_canonical_base64() {
|
|
118
|
+
val conversationKey =
|
|
119
|
+
hex("c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d")
|
|
120
|
+
val nonce =
|
|
121
|
+
hex("0000000000000000000000000000000000000000000000000000000000000001")
|
|
122
|
+
|
|
123
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
124
|
+
Nip44Crypto.encryptWithNonce("", conversationKey, nonce)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
val canonical = Nip44Crypto.encryptWithNonce(
|
|
128
|
+
"a".repeat(33),
|
|
129
|
+
conversationKey,
|
|
130
|
+
nonce
|
|
131
|
+
)
|
|
132
|
+
assertTrue(canonical.endsWith("="))
|
|
133
|
+
val alphabet =
|
|
134
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
135
|
+
val finalDataIndex = canonical.length - 2
|
|
136
|
+
val canonicalValue = alphabet.indexOf(canonical[finalDataIndex])
|
|
137
|
+
val nonCanonical = canonical.replaceRange(
|
|
138
|
+
finalDataIndex,
|
|
139
|
+
finalDataIndex + 1,
|
|
140
|
+
alphabet[canonicalValue xor 1].toString()
|
|
141
|
+
)
|
|
142
|
+
assertArrayEquals(
|
|
143
|
+
Base64.getDecoder().decode(canonical),
|
|
144
|
+
Base64.getDecoder().decode(nonCanonical)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
148
|
+
Nip44Crypto.decryptWithConversationKey(
|
|
149
|
+
nonCanonical,
|
|
150
|
+
conversationKey
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
fun rejects_tampered_mac_and_unknown_version() {
|
|
157
|
+
val conversationKey =
|
|
158
|
+
hex("c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d")
|
|
159
|
+
val decoded = Base64.getDecoder().decode(EXPECTED_PAYLOAD)
|
|
160
|
+
|
|
161
|
+
decoded[decoded.lastIndex] =
|
|
162
|
+
(decoded.last().toInt() xor 1).toByte()
|
|
163
|
+
val tampered = Base64.getEncoder().encodeToString(decoded)
|
|
164
|
+
|
|
165
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
166
|
+
Nip44Crypto.decryptWithConversationKey(
|
|
167
|
+
tampered,
|
|
168
|
+
conversationKey
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
decoded[0] = 3
|
|
173
|
+
val unknownVersion =
|
|
174
|
+
Base64.getEncoder().encodeToString(decoded)
|
|
175
|
+
|
|
176
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
177
|
+
Nip44Crypto.decryptWithConversationKey(
|
|
178
|
+
unknownVersion,
|
|
179
|
+
conversationKey
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
183
|
+
Nip44Crypto.decryptWithConversationKey(
|
|
184
|
+
"#$EXPECTED_PAYLOAD",
|
|
185
|
+
conversationKey
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private fun hex(value: String): ByteArray {
|
|
191
|
+
return Bip340Hash.hexToBytes(value)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
companion object {
|
|
195
|
+
// https://github.com/paulmillr/nip44/blob/master/nip44.vectors.json
|
|
196
|
+
private const val EXPECTED_PAYLOAD =
|
|
197
|
+
"AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"
|
|
198
|
+
}
|
|
199
|
+
}
|