pakstr 0.28.0 → 0.29.1
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 +10 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +65 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/signer/local/LocalAccountStoreTest.kt +78 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/utils/SignerPickerBottomSheetTest.kt +40 -6
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +124 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip19Codec.kt +181 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +25 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/local/LocalAccountImportCandidate.kt +42 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/local/LocalAccountStore.kt +17 -2
- package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +80 -4
- package/android-template/app/src/main/res/layout/activity_developer_tools.xml +15 -0
- package/android-template/app/src/main/res/layout/dialog_picker.xml +62 -0
- package/android-template/app/src/main/res/values/strings.xml +15 -2
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip19CodecTest.kt +178 -0
- package/dist/cli.js +13 -6
- package/dist/commands/artifactPublisher.js +125 -0
- package/dist/commands/build.js +16 -30
- package/dist/commands/publish.js +37 -3
- package/dist/commands/run.js +60 -29
- package/dist/core/apkSignature.js +197 -0
- package/dist/core/nsite.js +49 -0
- package/dist/runners/DockerGradleRunner.js +45 -24
- package/dist/runners/DockerSignRunner.js +58 -19
- package/dist/runners/LocalGradleRunner.js +7 -0
- package/dist/runners/process.js +51 -0
- package/dist/runners/timeouts.js +66 -0
- package/package.json +4 -3
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package com.pakstr.app.signer.local
|
|
2
|
+
|
|
3
|
+
import com.pakstr.app.crypto.Nip19Codec
|
|
4
|
+
import com.pakstr.app.crypto.Nip46ClientKey
|
|
5
|
+
|
|
6
|
+
internal class LocalAccountImportCandidate private constructor(
|
|
7
|
+
private val key: Nip46ClientKey,
|
|
8
|
+
val npub: String
|
|
9
|
+
) : AutoCloseable {
|
|
10
|
+
|
|
11
|
+
internal fun <T> withPrivateKey(
|
|
12
|
+
block: (ByteArray) -> T
|
|
13
|
+
): T = key.withPrivateKey(block)
|
|
14
|
+
|
|
15
|
+
override fun close() {
|
|
16
|
+
key.close()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
fun parse(nsec: String): LocalAccountImportCandidate {
|
|
21
|
+
val privateKey = Nip19Codec.decodeNsec(nsec)
|
|
22
|
+
return try {
|
|
23
|
+
val key = Nip46ClientKey.fromPrivateKey(privateKey)
|
|
24
|
+
try {
|
|
25
|
+
val publicKey = key.publicKey
|
|
26
|
+
val npub =
|
|
27
|
+
try {
|
|
28
|
+
Nip19Codec.encodeNpub(publicKey)
|
|
29
|
+
} finally {
|
|
30
|
+
publicKey.fill(0)
|
|
31
|
+
}
|
|
32
|
+
LocalAccountImportCandidate(key, npub)
|
|
33
|
+
} catch (error: Exception) {
|
|
34
|
+
key.close()
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
privateKey.fill(0)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -16,6 +16,9 @@ internal class LocalAccountStorageException(
|
|
|
16
16
|
cause: Throwable? = null
|
|
17
17
|
) : Exception(message, cause)
|
|
18
18
|
|
|
19
|
+
internal class LocalAccountAlreadyExistsException :
|
|
20
|
+
IllegalStateException("Local account already exists")
|
|
21
|
+
|
|
19
22
|
internal class LocalAccountStore(
|
|
20
23
|
context: Context,
|
|
21
24
|
private val preferencesName: String = PREFERENCES_NAME,
|
|
@@ -47,6 +50,18 @@ internal class LocalAccountStore(
|
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
fun importAccount(privateKey: ByteArray): String =
|
|
54
|
+
synchronized(STORAGE_LOCK) {
|
|
55
|
+
if (hasAccountLocked()) {
|
|
56
|
+
throw LocalAccountAlreadyExistsException()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Nip46ClientKey.fromPrivateKey(privateKey).use { key ->
|
|
60
|
+
key.withPrivateKey(::saveNewKey)
|
|
61
|
+
key.publicKeyHex
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
fun hasAccount(): Boolean =
|
|
51
66
|
synchronized(STORAGE_LOCK) {
|
|
52
67
|
hasAccountLocked()
|
|
@@ -105,8 +120,8 @@ internal class LocalAccountStore(
|
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
private fun saveNewKey(privateKey: ByteArray) {
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
if (hasAccountLocked()) {
|
|
124
|
+
throw LocalAccountAlreadyExistsException()
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
val wrappingKey = getOrCreateWrappingKey()
|
|
@@ -5,6 +5,7 @@ import android.os.Bundle
|
|
|
5
5
|
import android.view.LayoutInflater
|
|
6
6
|
import android.view.View
|
|
7
7
|
import android.view.ViewGroup
|
|
8
|
+
import android.view.WindowManager
|
|
8
9
|
import android.widget.Button
|
|
9
10
|
import android.widget.RadioGroup
|
|
10
11
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
@@ -14,9 +15,11 @@ import com.pakstr.app.R
|
|
|
14
15
|
|
|
15
16
|
class SignerPickerBottomSheet(
|
|
16
17
|
private val hasBunkerSession: Boolean,
|
|
18
|
+
private val hasLocalAccount: Boolean,
|
|
17
19
|
private val onAmberSelected: () -> Unit,
|
|
18
20
|
private val onBunkerSelected: (String) -> Boolean,
|
|
19
21
|
private val onLocalSelected: () -> Unit,
|
|
22
|
+
private val onLocalImportSelected: (String) -> Boolean,
|
|
20
23
|
private val onSharePairing: (List<String>) -> Unit,
|
|
21
24
|
private val onBunkerLogout: () -> Unit,
|
|
22
25
|
private val onDismissed: () -> Unit = {}
|
|
@@ -34,6 +37,11 @@ class SignerPickerBottomSheet(
|
|
|
34
37
|
)
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
override fun onStart() {
|
|
41
|
+
super.onStart()
|
|
42
|
+
dialog?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
override fun onViewCreated(
|
|
38
46
|
view: View,
|
|
39
47
|
savedInstanceState: Bundle?
|
|
@@ -42,6 +50,11 @@ class SignerPickerBottomSheet(
|
|
|
42
50
|
val radioGroup = view.findViewById<RadioGroup>(R.id.options_radio_group)
|
|
43
51
|
val uriContainer = view.findViewById<TextInputLayout>(R.id.bunker_uri_container)
|
|
44
52
|
val uriInput = view.findViewById<TextInputEditText>(R.id.bunker_uri)
|
|
53
|
+
val localActions = view.findViewById<View>(R.id.local_account_actions)
|
|
54
|
+
val localExisting = view.findViewById<View>(R.id.local_account_existing)
|
|
55
|
+
val localActionGroup = view.findViewById<RadioGroup>(R.id.local_action_radio_group)
|
|
56
|
+
val nsecContainer = view.findViewById<TextInputLayout>(R.id.local_nsec_container)
|
|
57
|
+
val nsecInput = view.findViewById<TextInputEditText>(R.id.local_nsec)
|
|
45
58
|
val confirm = view.findViewById<Button>(R.id.button_confirm)
|
|
46
59
|
val sharePairing = view.findViewById<Button>(R.id.button_share_pairing)
|
|
47
60
|
val logout = view.findViewById<Button>(R.id.button_logout_bunker)
|
|
@@ -49,19 +62,58 @@ class SignerPickerBottomSheet(
|
|
|
49
62
|
logout.visibility =
|
|
50
63
|
if (hasBunkerSession) View.VISIBLE else View.GONE
|
|
51
64
|
|
|
52
|
-
fun
|
|
65
|
+
fun updateControls() {
|
|
53
66
|
val bunkerSelected =
|
|
54
67
|
radioGroup.checkedRadioButtonId == R.id.radio_option_two
|
|
68
|
+
val localSelected =
|
|
69
|
+
radioGroup.checkedRadioButtonId == R.id.radio_option_three
|
|
55
70
|
uriContainer.visibility =
|
|
56
71
|
if (bunkerSelected) View.VISIBLE else View.GONE
|
|
57
72
|
sharePairing.visibility =
|
|
58
73
|
if (bunkerSelected) View.VISIBLE else View.GONE
|
|
74
|
+
localActions.visibility =
|
|
75
|
+
if (localSelected && !hasLocalAccount) {
|
|
76
|
+
View.VISIBLE
|
|
77
|
+
} else {
|
|
78
|
+
View.GONE
|
|
79
|
+
}
|
|
80
|
+
localExisting.visibility =
|
|
81
|
+
if (localSelected && hasLocalAccount) {
|
|
82
|
+
View.VISIBLE
|
|
83
|
+
} else {
|
|
84
|
+
View.GONE
|
|
85
|
+
}
|
|
86
|
+
nsecContainer.visibility =
|
|
87
|
+
if (
|
|
88
|
+
localSelected &&
|
|
89
|
+
!hasLocalAccount &&
|
|
90
|
+
localActionGroup.checkedRadioButtonId ==
|
|
91
|
+
R.id.radio_local_import
|
|
92
|
+
) {
|
|
93
|
+
View.VISIBLE
|
|
94
|
+
} else {
|
|
95
|
+
View.GONE
|
|
96
|
+
}
|
|
59
97
|
}
|
|
60
98
|
radioGroup.setOnCheckedChangeListener { _, _ ->
|
|
61
99
|
uriContainer.error = null
|
|
62
|
-
|
|
100
|
+
nsecContainer.error = null
|
|
101
|
+
if (radioGroup.checkedRadioButtonId != R.id.radio_option_three) {
|
|
102
|
+
nsecInput.text?.clear()
|
|
103
|
+
}
|
|
104
|
+
updateControls()
|
|
63
105
|
}
|
|
64
|
-
|
|
106
|
+
localActionGroup.setOnCheckedChangeListener { _, _ ->
|
|
107
|
+
nsecContainer.error = null
|
|
108
|
+
if (
|
|
109
|
+
localActionGroup.checkedRadioButtonId !=
|
|
110
|
+
R.id.radio_local_import
|
|
111
|
+
) {
|
|
112
|
+
nsecInput.text?.clear()
|
|
113
|
+
}
|
|
114
|
+
updateControls()
|
|
115
|
+
}
|
|
116
|
+
updateControls()
|
|
65
117
|
|
|
66
118
|
confirm.setOnClickListener {
|
|
67
119
|
when (radioGroup.checkedRadioButtonId) {
|
|
@@ -73,7 +125,24 @@ class SignerPickerBottomSheet(
|
|
|
73
125
|
return@setOnClickListener
|
|
74
126
|
}
|
|
75
127
|
}
|
|
76
|
-
R.id.radio_option_three ->
|
|
128
|
+
R.id.radio_option_three -> {
|
|
129
|
+
val importing =
|
|
130
|
+
!hasLocalAccount &&
|
|
131
|
+
localActionGroup.checkedRadioButtonId ==
|
|
132
|
+
R.id.radio_local_import
|
|
133
|
+
if (importing) {
|
|
134
|
+
val value =
|
|
135
|
+
nsecInput.text?.toString().orEmpty().trim()
|
|
136
|
+
if (!onLocalImportSelected(value)) {
|
|
137
|
+
nsecContainer.error =
|
|
138
|
+
getString(R.string.signer_picker_local_invalid)
|
|
139
|
+
return@setOnClickListener
|
|
140
|
+
}
|
|
141
|
+
nsecInput.text?.clear()
|
|
142
|
+
} else {
|
|
143
|
+
onLocalSelected()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
77
146
|
else -> onAmberSelected()
|
|
78
147
|
}
|
|
79
148
|
dismiss()
|
|
@@ -104,6 +173,13 @@ class SignerPickerBottomSheet(
|
|
|
104
173
|
}
|
|
105
174
|
}
|
|
106
175
|
|
|
176
|
+
override fun onDestroyView() {
|
|
177
|
+
view?.findViewById<TextInputEditText>(R.id.local_nsec)
|
|
178
|
+
?.text
|
|
179
|
+
?.clear()
|
|
180
|
+
super.onDestroyView()
|
|
181
|
+
}
|
|
182
|
+
|
|
107
183
|
override fun onDismiss(dialog: DialogInterface) {
|
|
108
184
|
super.onDismiss(dialog)
|
|
109
185
|
onDismissed()
|
|
@@ -208,6 +208,21 @@
|
|
|
208
208
|
android:layout_height="wrap_content"
|
|
209
209
|
android:textColor="?android:attr/textColorSecondary" />
|
|
210
210
|
</LinearLayout>
|
|
211
|
+
|
|
212
|
+
<TextView
|
|
213
|
+
style="@style/TextAppearance.MaterialComponents.Subtitle2"
|
|
214
|
+
android:layout_width="match_parent"
|
|
215
|
+
android:layout_height="wrap_content"
|
|
216
|
+
android:layout_marginTop="12dp"
|
|
217
|
+
android:text="@string/developer_tools_chrome_webview_inspection" />
|
|
218
|
+
|
|
219
|
+
<TextView
|
|
220
|
+
style="@style/TextAppearance.MaterialComponents.Caption"
|
|
221
|
+
android:layout_width="match_parent"
|
|
222
|
+
android:layout_height="wrap_content"
|
|
223
|
+
android:layout_marginTop="4dp"
|
|
224
|
+
android:text="@string/developer_tools_chrome_webview_inspection_note"
|
|
225
|
+
android:textColor="?android:attr/textColorSecondary" />
|
|
211
226
|
</LinearLayout>
|
|
212
227
|
</com.google.android.material.card.MaterialCardView>
|
|
213
228
|
|
|
@@ -97,6 +97,68 @@
|
|
|
97
97
|
android:textSize="16sp" />
|
|
98
98
|
</RadioGroup>
|
|
99
99
|
|
|
100
|
+
<LinearLayout
|
|
101
|
+
android:id="@+id/local_account_actions"
|
|
102
|
+
android:layout_width="match_parent"
|
|
103
|
+
android:layout_height="wrap_content"
|
|
104
|
+
android:layout_marginBottom="12dp"
|
|
105
|
+
android:orientation="vertical"
|
|
106
|
+
android:paddingStart="16dp"
|
|
107
|
+
android:paddingEnd="16dp"
|
|
108
|
+
android:visibility="gone">
|
|
109
|
+
|
|
110
|
+
<RadioGroup
|
|
111
|
+
android:id="@+id/local_action_radio_group"
|
|
112
|
+
android:layout_width="match_parent"
|
|
113
|
+
android:layout_height="wrap_content"
|
|
114
|
+
android:orientation="vertical">
|
|
115
|
+
|
|
116
|
+
<RadioButton
|
|
117
|
+
android:id="@+id/radio_local_create"
|
|
118
|
+
android:layout_width="match_parent"
|
|
119
|
+
android:layout_height="wrap_content"
|
|
120
|
+
android:checked="true"
|
|
121
|
+
android:text="@string/signer_picker_local_create" />
|
|
122
|
+
|
|
123
|
+
<RadioButton
|
|
124
|
+
android:id="@+id/radio_local_import"
|
|
125
|
+
android:layout_width="match_parent"
|
|
126
|
+
android:layout_height="wrap_content"
|
|
127
|
+
android:text="@string/signer_picker_local_import" />
|
|
128
|
+
</RadioGroup>
|
|
129
|
+
|
|
130
|
+
<com.google.android.material.textfield.TextInputLayout
|
|
131
|
+
android:id="@+id/local_nsec_container"
|
|
132
|
+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
133
|
+
android:layout_width="match_parent"
|
|
134
|
+
android:layout_height="wrap_content"
|
|
135
|
+
android:layout_marginTop="8dp"
|
|
136
|
+
android:hint="@string/signer_picker_local_nsec_hint"
|
|
137
|
+
android:visibility="gone"
|
|
138
|
+
app:endIconMode="password_toggle">
|
|
139
|
+
|
|
140
|
+
<com.google.android.material.textfield.TextInputEditText
|
|
141
|
+
android:id="@+id/local_nsec"
|
|
142
|
+
android:layout_width="match_parent"
|
|
143
|
+
android:layout_height="wrap_content"
|
|
144
|
+
android:importantForAutofill="no"
|
|
145
|
+
android:inputType="textPassword|textNoSuggestions"
|
|
146
|
+
android:maxLines="1"
|
|
147
|
+
android:saveEnabled="false" />
|
|
148
|
+
</com.google.android.material.textfield.TextInputLayout>
|
|
149
|
+
</LinearLayout>
|
|
150
|
+
|
|
151
|
+
<TextView
|
|
152
|
+
android:id="@+id/local_account_existing"
|
|
153
|
+
android:layout_width="match_parent"
|
|
154
|
+
android:layout_height="wrap_content"
|
|
155
|
+
android:layout_marginBottom="12dp"
|
|
156
|
+
android:paddingStart="16dp"
|
|
157
|
+
android:paddingEnd="16dp"
|
|
158
|
+
android:text="@string/signer_picker_local_existing"
|
|
159
|
+
android:textColor="#4B5563"
|
|
160
|
+
android:visibility="gone" />
|
|
161
|
+
|
|
100
162
|
<com.google.android.material.textfield.TextInputLayout
|
|
101
163
|
android:id="@+id/bunker_uri_container"
|
|
102
164
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
<string name="developer_tools_api_base_save_failed">Unable to persist the API Base URL setting.</string>
|
|
33
33
|
<string name="developer_tools_api_base_invalid">Enter an absolute HTTPS URL without credentials, query, or fragment.</string>
|
|
34
34
|
<string name="developer_tools_webview_debug">WebView Debug</string>
|
|
35
|
+
<string name="developer_tools_chrome_webview_inspection">Chrome WebView Inspection</string>
|
|
36
|
+
<string name="developer_tools_chrome_webview_inspection_note">To inspect this app in Chrome DevTools, enable Developer options → USB debugging on your Android device. This is required even when connecting over Wi-Fi.</string>
|
|
35
37
|
<string name="developer_tools_enabled">Enabled</string>
|
|
36
38
|
<string name="developer_tools_disabled">Disabled</string>
|
|
37
39
|
|
|
@@ -66,11 +68,22 @@
|
|
|
66
68
|
|
|
67
69
|
<string name="signer_settings">Signer settings</string>
|
|
68
70
|
<string name="signer_picker_title">Choose a signer</string>
|
|
69
|
-
<string name="signer_picker_subtitle">Use Amber, connect to a remote signer, or
|
|
71
|
+
<string name="signer_picker_subtitle">Use Amber, connect to a remote signer, or use an app-local account.</string>
|
|
70
72
|
<string name="signer_picker_amber">Continue with Amber</string>
|
|
71
73
|
<string name="signer_picker_bunker">Connect to remote signer (Bunker)</string>
|
|
72
|
-
<string name="signer_picker_local">
|
|
74
|
+
<string name="signer_picker_local">App-local account</string>
|
|
75
|
+
<string name="signer_picker_local_create">Create new account</string>
|
|
76
|
+
<string name="signer_picker_local_import">Import existing account</string>
|
|
77
|
+
<string name="signer_picker_local_nsec_hint">Enter your nsec</string>
|
|
78
|
+
<string name="signer_picker_local_existing">Use the existing app-local account on this device.</string>
|
|
79
|
+
<string name="signer_picker_local_invalid">Enter a valid lowercase nsec.</string>
|
|
80
|
+
<string name="signer_picker_local_confirm_title">Confirm account import</string>
|
|
81
|
+
<string name="signer_picker_local_confirm_message">Import this account?\n\n%1$s</string>
|
|
82
|
+
<string name="signer_picker_local_confirm">Import</string>
|
|
83
|
+
<string name="signer_picker_local_cancel">Cancel</string>
|
|
73
84
|
<string name="signer_picker_local_failed">Unable to create the app-local account.</string>
|
|
85
|
+
<string name="signer_picker_local_status_failed">Unable to check the app-local account.</string>
|
|
86
|
+
<string name="signer_picker_local_import_failed">Unable to import the app-local account.</string>
|
|
74
87
|
<string name="signer_picker_bunker_hint">Paste a bunker:// connection</string>
|
|
75
88
|
<string name="signer_picker_share_pairing">Share a nostrconnect:// pairing</string>
|
|
76
89
|
<string name="signer_picker_logout_bunker">Disconnect remote signer</string>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
import com.pakstr.app.signer.local.LocalAccountImportCandidate
|
|
4
|
+
import org.junit.Assert.assertArrayEquals
|
|
5
|
+
import org.junit.Assert.assertEquals
|
|
6
|
+
import org.junit.Assert.assertFalse
|
|
7
|
+
import org.junit.Assert.assertThrows
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
|
|
10
|
+
class Nip19CodecTest {
|
|
11
|
+
|
|
12
|
+
@Test
|
|
13
|
+
fun decodes_known_nsec_vector() {
|
|
14
|
+
val decoded = Nip19Codec.decodeNsec(NSEC_ONE)
|
|
15
|
+
|
|
16
|
+
assertArrayEquals(
|
|
17
|
+
ByteArray(32).also { it[31] = 1 },
|
|
18
|
+
decoded
|
|
19
|
+
)
|
|
20
|
+
decoded.fill(0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
fun encodes_known_npub_vector() {
|
|
25
|
+
val publicKey = Bip340Hash.hexToBytes(PUBLIC_KEY_ONE)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
assertEquals(NPUB_ONE, Nip19Codec.encodeNpub(publicKey))
|
|
29
|
+
} finally {
|
|
30
|
+
publicKey.fill(0)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
fun import_candidate_derives_expected_npub_and_closes_key() {
|
|
36
|
+
val candidate = LocalAccountImportCandidate.parse(NSEC_ONE)
|
|
37
|
+
|
|
38
|
+
assertEquals(NPUB_ONE, candidate.npub)
|
|
39
|
+
assertFalse(candidate.toString().contains(NSEC_ONE))
|
|
40
|
+
candidate.close()
|
|
41
|
+
assertThrows(IllegalStateException::class.java) {
|
|
42
|
+
candidate.withPrivateKey { }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun rejects_noncanonical_or_corrupt_nsec() {
|
|
48
|
+
val invalid = listOf(
|
|
49
|
+
NSEC_ONE.uppercase(),
|
|
50
|
+
"Nsec" + NSEC_ONE.substring(4),
|
|
51
|
+
"xsec" + NSEC_ONE.substring(4),
|
|
52
|
+
NSEC_ONE.dropLast(1) + "q",
|
|
53
|
+
NSEC_ONE.replaceRange(8, 9, "i"),
|
|
54
|
+
NSEC_ONE.dropLast(1),
|
|
55
|
+
NSEC_ONE + "q"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
invalid.forEach { value ->
|
|
59
|
+
assertThrows(InvalidNsecException::class.java) {
|
|
60
|
+
Nip19Codec.decodeNsec(value)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Test
|
|
66
|
+
fun rejects_invalid_private_scalars() {
|
|
67
|
+
val zero = encodeNsecForTest(ByteArray(32))
|
|
68
|
+
val order = Bip340Hash.hexToBytes(SECP256K1_ORDER)
|
|
69
|
+
val aboveOrder = order.copyOf().also {
|
|
70
|
+
it[it.lastIndex] = (it.last().toInt() + 1).toByte()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
75
|
+
LocalAccountImportCandidate.parse(zero)
|
|
76
|
+
}
|
|
77
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
78
|
+
LocalAccountImportCandidate.parse(
|
|
79
|
+
encodeNsecForTest(order)
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
83
|
+
LocalAccountImportCandidate.parse(
|
|
84
|
+
encodeNsecForTest(aboveOrder)
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
order.fill(0)
|
|
89
|
+
aboveOrder.fill(0)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
fun rejects_wrong_payload_lengths_and_accepts_leading_zero() {
|
|
95
|
+
assertThrows(InvalidNsecException::class.java) {
|
|
96
|
+
Nip19Codec.decodeNsec(encodeNsecForTest(ByteArray(31)))
|
|
97
|
+
}
|
|
98
|
+
assertThrows(InvalidNsecException::class.java) {
|
|
99
|
+
Nip19Codec.decodeNsec(encodeNsecForTest(ByteArray(33)))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
LocalAccountImportCandidate.parse(NSEC_ONE).use { candidate ->
|
|
103
|
+
assertEquals(NPUB_ONE, candidate.npub)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun encodeNsecForTest(bytes: ByteArray): String {
|
|
108
|
+
val data = convertBits(bytes)
|
|
109
|
+
val values = hrpExpand("nsec") + data + IntArray(6)
|
|
110
|
+
val checksum = polymod(values) xor 1
|
|
111
|
+
val check = IntArray(6) { index ->
|
|
112
|
+
(checksum shr (5 * (5 - index))) and 31
|
|
113
|
+
}
|
|
114
|
+
return "nsec1" + (data + check).joinToString("") {
|
|
115
|
+
CHARSET[it].toString()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private fun convertBits(bytes: ByteArray): IntArray {
|
|
120
|
+
var accumulator = 0
|
|
121
|
+
var bits = 0
|
|
122
|
+
val output = ArrayList<Int>()
|
|
123
|
+
bytes.forEach { byte ->
|
|
124
|
+
accumulator = (accumulator shl 8) or (byte.toInt() and 0xff)
|
|
125
|
+
bits += 8
|
|
126
|
+
while (bits >= 5) {
|
|
127
|
+
bits -= 5
|
|
128
|
+
output += (accumulator shr bits) and 31
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (bits > 0) {
|
|
132
|
+
output += (accumulator shl (5 - bits)) and 31
|
|
133
|
+
}
|
|
134
|
+
return output.toIntArray()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun hrpExpand(hrp: String): IntArray =
|
|
138
|
+
IntArray(hrp.length * 2 + 1).also { result ->
|
|
139
|
+
hrp.forEachIndexed { index, character ->
|
|
140
|
+
result[index] = character.code shr 5
|
|
141
|
+
result[hrp.length + 1 + index] = character.code and 31
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private fun polymod(values: IntArray): Int {
|
|
146
|
+
var checksum = 1
|
|
147
|
+
val generators = intArrayOf(
|
|
148
|
+
0x3b6a57b2,
|
|
149
|
+
0x26508e6d,
|
|
150
|
+
0x1ea119fa,
|
|
151
|
+
0x3d4233dd,
|
|
152
|
+
0x2a1462b3
|
|
153
|
+
)
|
|
154
|
+
values.forEach { value ->
|
|
155
|
+
val top = checksum ushr 25
|
|
156
|
+
checksum = (checksum and 0x1ffffff) shl 5 xor value
|
|
157
|
+
generators.forEachIndexed { index, generator ->
|
|
158
|
+
if ((top shr index) and 1 != 0) {
|
|
159
|
+
checksum = checksum xor generator
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return checksum
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private companion object {
|
|
167
|
+
const val CHARSET =
|
|
168
|
+
"qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
169
|
+
const val NSEC_ONE =
|
|
170
|
+
"nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsmhltgl"
|
|
171
|
+
const val PUBLIC_KEY_ONE =
|
|
172
|
+
"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
|
173
|
+
const val NPUB_ONE =
|
|
174
|
+
"npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d"
|
|
175
|
+
const val SECP256K1_ORDER =
|
|
176
|
+
"fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"
|
|
177
|
+
}
|
|
178
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runCLI = runCLI;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
7
8
|
const build_1 = require("./commands/build");
|
|
8
9
|
const sign_1 = require("./commands/sign");
|
|
9
10
|
const publish_1 = require("./commands/publish");
|
|
@@ -15,9 +16,14 @@ const deletePublisher_1 = require("./commands/deletePublisher");
|
|
|
15
16
|
const dotenv_1 = require("./core/dotenv");
|
|
16
17
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
17
18
|
async function runCLI(argv) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
(
|
|
19
|
+
const command = argv[0];
|
|
20
|
+
const args = argv.slice(1);
|
|
21
|
+
const configFlag = extractFlag(args, "--config");
|
|
22
|
+
// Auto-load the `.env` (gitignored) next to the config file, where
|
|
23
|
+
// `pakstr init` writes it, so `pakstr run` works out of the box. Without
|
|
24
|
+
// --config that is the current directory. Existing env vars (e.g. CI
|
|
25
|
+
// secrets) always win.
|
|
26
|
+
(0, dotenv_1.loadDotEnv)(configDir(configFlag));
|
|
21
27
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
22
28
|
printHelp();
|
|
23
29
|
return;
|
|
@@ -26,13 +32,10 @@ async function runCLI(argv) {
|
|
|
26
32
|
console.log(package_json_1.default.version);
|
|
27
33
|
return;
|
|
28
34
|
}
|
|
29
|
-
const command = argv[0];
|
|
30
|
-
const args = argv.slice(1);
|
|
31
35
|
if (!command) {
|
|
32
36
|
printHelp();
|
|
33
37
|
return;
|
|
34
38
|
}
|
|
35
|
-
const configFlag = extractFlag(args, "--config");
|
|
36
39
|
switch (command) {
|
|
37
40
|
case "init":
|
|
38
41
|
await (0, init_1.initCommand)({ force: args.includes("--force"), out: configFlag });
|
|
@@ -99,6 +102,10 @@ function extractFlag(args, flag) {
|
|
|
99
102
|
return undefined;
|
|
100
103
|
return args[i + 1];
|
|
101
104
|
}
|
|
105
|
+
/** Directory of the config file, resolved the same way as loadPakstrConfig. */
|
|
106
|
+
function configDir(configFlag) {
|
|
107
|
+
return configFlag ? path_1.default.dirname(path_1.default.resolve(configFlag)) : process.cwd();
|
|
108
|
+
}
|
|
102
109
|
function printHelp() {
|
|
103
110
|
console.log(`
|
|
104
111
|
Pakstr — one-step nsec -> build -> sign -> publish.
|