Decoupling NIP01 methods from CryptoUtils

Decoupling Encryption and Decryptions from CryptoUtils
Decoupling live instances of JNI bindings for Secp and LibSodium from CryptoUtils
Decoupling key cache from CryptoUtils
Reorganizes NIP-04 to match new package structure
Adjusts test structures to match
This commit is contained in:
Vitor Pamplona
2025-02-17 19:13:05 -05:00
parent 7ad8b2ce46
commit 6d9964a1cb
89 changed files with 1048 additions and 1015 deletions
@@ -34,9 +34,9 @@ import com.vitorpamplona.amethyst.service.uploads.nip96.ServerInfoRetriever
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.utils.sha256
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.fail
import kotlinx.coroutines.CoroutineScope
@@ -81,7 +81,7 @@ class ImageUploadTesting {
private suspend fun testBlossom(server: ServerName) {
val paylod = getBitmap()
val initialHash = CryptoUtils.sha256(paylod).toHexKey()
val initialHash = sha256(paylod).toHexKey()
val inputStream = paylod.inputStream()
val result =
BlossomUploader()
@@ -111,7 +111,7 @@ class ImageUploadTesting {
return
}
val downloadedHash = CryptoUtils.sha256(imageData).toHexKey()
val downloadedHash = sha256(imageData).toHexKey()
assertEquals(initialHash, downloadedHash)
}
@@ -102,8 +102,8 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactTag
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.reply
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.reply
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip14Subject.subject
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -69,7 +69,7 @@ import com.vitorpamplona.quartz.nip01Core.verify
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.VerificationState
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -34,8 +34,8 @@ import coil3.key.Keyer
import coil3.request.ImageRequest
import coil3.request.Options
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.base64contentPattern
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.utils.sha256
import java.util.Base64
@Stable
@@ -83,7 +83,7 @@ class Base64Fetcher(
options: Options,
): String? =
if (data.scheme == "data") {
CryptoUtils.sha256(data.toString().toByteArray()).toHexKey()
sha256(data.toString().toByteArray()).toHexKey()
} else {
null
}
@@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") {
@@ -26,7 +26,7 @@ import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
@@ -30,13 +30,13 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.nns.NNSEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
@@ -80,7 +80,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
}
when (val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity) {
is NSec -> KeyPair(privKey = parsed.hex.bechToBytes()).pubKey.toHexKey()
is NSec -> Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey()
is NPub -> parsed.hex
is NProfile -> parsed.hex
is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> parsed.hex
@@ -25,7 +25,7 @@ import android.util.LruCache
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.utils.RandomInstance
import okhttp3.EventListener
import okhttp3.Protocol
import okhttp3.Request
@@ -70,7 +70,7 @@ object OnlineChecker {
.url(url.replace("wss+livekit://", "wss://"))
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Key", CryptoUtils.random(16).toByteString().base64())
.header("Sec-WebSocket-Key", RandomInstance.bytes(16).toByteString().base64())
.header("Sec-WebSocket-Version", "13")
.header("Sec-WebSocket-Extensions", "permessage-deflate")
.build()
@@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip19Bech32.toNpub
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service.uploads
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
import com.vitorpamplona.quartz.utils.sha256
import java.io.File
class EncryptFilesResult(
@@ -48,9 +48,9 @@ class EncryptFiles {
resolver.openInputStream(inputFile)!!.use { inputStream ->
val bytes = inputStream.readBytes()
val originalHash = CryptoUtils.sha256(bytes).toHexKey()
val originalHash = sha256(bytes).toHexKey()
val encrypted = cipher.encrypt(bytes)
val encryptedHash = CryptoUtils.sha256(encrypted).toHexKey()
val encryptedHash = sha256(encrypted).toHexKey()
encryptedFile.outputStream().use { outputStream ->
outputStream.write(encrypted)
@@ -30,9 +30,9 @@ import android.os.Build
import android.util.Log
import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
import com.vitorpamplona.amethyst.service.Blurhash
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.utils.sha256
import kotlinx.coroutines.CancellationException
import java.io.IOException
@@ -74,7 +74,7 @@ class FileHeader(
dimPrecomputed: DimensionTag?,
): Result<FileHeader> =
try {
val hash = CryptoUtils.sha256(data).toHexKey()
val hash = sha256(data).toHexKey()
val size = data.size
val (blurHash, dim) =
@@ -35,10 +35,10 @@ import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.utils.sha256
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody
@@ -88,7 +88,7 @@ class BlossomUploader {
checkNotNull(payload) { "Can't open the image input stream" }
val hash = CryptoUtils.sha256(payload).toHexKey()
val hash = sha256(payload).toHexKey()
val imageInputStream = contentResolver.openInputStream(uri)
@@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
@@ -180,7 +180,7 @@ class NewMessageTagger(
val restOfWord = key.substring(63)
// Converts to npub
val pubkey =
Nip19Parser.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
Nip19Parser.uriToRoute(Nip01.pubKeyCreate(keyB32.bechToBytes()).toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
} else if (key.startsWith("npub1", true)) {
@@ -77,7 +77,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.pTags
import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
@@ -67,7 +67,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
@@ -93,11 +93,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size30dp
import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.utils.sha256
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
@@ -695,7 +695,7 @@ private suspend fun verifyHash(content: MediaUrlContent): Boolean? {
if (content.hash == null) return null
Amethyst.instance.coilCache.openSnapshot(content.url)?.use { snapshot ->
val hash = CryptoUtils.sha256(snapshot.data.toFile().readBytes()).toHexKey()
val hash = sha256(snapshot.data.toFile().readBytes()).toHexKey()
Log.d("Image Hash Verification", "$hash == ${content.hash}")
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.relays.updated
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -160,7 +160,7 @@ import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableKind
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull
@@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip19Bech32.toNpub
@@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
@@ -45,6 +44,7 @@ import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactTag
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
@@ -56,6 +56,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.Hex
@@ -142,9 +143,9 @@ class AccountStateViewModel : ViewModel() {
transientAccount = transientAccount,
torSettings = TorSettingsFlow.build(torSettings),
)
} else if (key.contains(" ") && CryptoUtils.isValidMnemonic(key)) {
} else if (key.contains(" ") && Nip06().isValidMnemonic(key)) {
AccountSettings(
keyPair = KeyPair(privKey = CryptoUtils.privateKeyFromMnemonic(key)),
keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key)),
transientAccount = transientAccount,
torSettings = TorSettingsFlow.build(torSettings),
)
@@ -206,7 +207,11 @@ class AccountStateViewModel : ViewModel() {
if (key.startsWith("ncryptsec")) {
val newKey =
try {
CryptoUtils.decryptNIP49(key, password)
if (key.isEmpty() || password.isEmpty()) {
null
} else {
Nip49().decrypt(key, password)
}
} catch (e: Exception) {
if (e is CancellationException) throw e
onError(e.message)
@@ -102,9 +102,9 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip19Bech32.toNsec
import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -444,7 +444,7 @@ private fun encryptCopyNSec(
}
} else {
accountViewModel.account.settings.keyPair.privKey?.let {
val key = CryptoUtils.encryptNIP49(it.toHexKey(), password.value.text)
val key = runCatching { Nip49().encrypt(it.toHexKey(), password.value.text) }.getOrNull()
if (key != null) {
clipboardManager.setText(AnnotatedString(key))
scope.launch {
@@ -540,7 +540,7 @@ private fun QrCodeButtonEncrypted(
onDialogShow = {
accountViewModel.account.settings.keyPair.privKey
?.toHexKey()
?.let { CryptoUtils.encryptNIP49(it, password.value.text) }
?.let { Nip49().encrypt(it, password.value.text) }
},
)
}
@@ -111,8 +111,8 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.TimeUtils
import fr.acinq.secp256k1.Hex
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
@@ -1753,7 +1753,6 @@ fun mockAccountViewModel(): AccountViewModel {
KeyPair(
privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"),
pubKey = Hex.decode("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"),
forcePubKeyCheck = false,
),
),
sharedPreferencesViewModel.sharedPrefs,
@@ -1771,7 +1770,6 @@ fun mockVitorAccountViewModel(): AccountViewModel {
keyPair =
KeyPair(
pubKey = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
forcePubKeyCheck = false,
),
),
sharedPreferencesViewModel.sharedPrefs,
@@ -104,7 +104,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
@@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState
import com.vitorpamplona.ammolite.relays.BundledInsert
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -175,7 +175,7 @@ import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableKind
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
@@ -65,11 +65,20 @@ class EventBenchmark {
}
@Test
fun eventFactoryPerformanceTest() {
fun eventFactoryKind1PerformanceTest() {
val now = TimeUtils.now()
val tags = arrayOf(arrayOf(""))
benchmarkRule.measureRepeated {
EventFactory.create("id", "pubkey", now, 1, tags, "content", "sig")
}
}
@Test
fun eventFactoryKind30818PerformanceTest() {
val now = TimeUtils.now()
val tags = arrayOf(arrayOf(""))
benchmarkRule.measureRepeated {
EventFactory.create("id", "pubkey", now, 30818, tags, "content", "sig")
}
}
}
@@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.checkSignature
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
@@ -132,7 +131,6 @@ class GiftWrapBenchmark {
// Simulate Receiver
benchmarkRule.measureRepeated {
CryptoUtils.clearCache()
val counter = CountDownLatch(1)
val wrap = Event.fromJson(giftWrapJson) as GiftWrapEvent
@@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
@@ -35,6 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.verifySignature
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip44Encryption.Nip44
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
@@ -171,7 +171,7 @@ class GiftWrapReceivingBenchmark {
benchmarkRule.measureRepeated {
assertNotNull(
CryptoUtils.decryptNIP44(
Nip44.decrypt(
wrap.content,
receiver.keyPair.privKey!!,
wrap.pubKey.hexToByteArray(),
@@ -188,7 +188,7 @@ class GiftWrapReceivingBenchmark {
val wrap = createWrap(sender, receiver)
val innerJson =
CryptoUtils.decryptNIP44(
Nip44.decrypt(
wrap.content,
receiver.keyPair.privKey!!,
wrap.pubKey.hexToByteArray(),
@@ -206,7 +206,7 @@ class GiftWrapReceivingBenchmark {
benchmarkRule.measureRepeated {
assertNotNull(
CryptoUtils.decryptNIP44(
Nip44.decrypt(
seal.content,
receiver.keyPair.privKey!!,
seal.pubKey.hexToByteArray(),
@@ -223,7 +223,7 @@ class GiftWrapReceivingBenchmark {
val seal = createSeal(sender, receiver)
val innerJson =
CryptoUtils.decryptNIP44(
Nip44.decrypt(
seal.content,
receiver.keyPair.privKey!!,
seal.pubKey.hexToByteArray(),
@@ -25,7 +25,7 @@ import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.EventHasher
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.utils.sha256Hash
import com.vitorpamplona.quartz.utils.sha256
import junit.framework.TestCase.assertNotNull
import org.junit.Rule
import org.junit.Test
@@ -49,7 +49,7 @@ class Sha256Benchmark {
benchmarkRule.measureRepeated {
// Should pass
assertNotNull(sha256Hash(byteArray))
assertNotNull(sha256(byteArray))
}
}
}
@@ -23,15 +23,17 @@ package com.vitorpamplona.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip04Dm.crypto.Encryption
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
import com.vitorpamplona.quartz.nip44Encryption.Nip44v2
import junit.framework.TestCase.assertNotNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CryptoBenchmark {
class SharedKeyBenchmark {
@get:Rule val benchmarkRule = BenchmarkRule()
@Test
@@ -40,7 +42,7 @@ class CryptoBenchmark {
val keyPair2 = KeyPair()
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.getSharedSecretNIP04(keyPair1.privKey!!, keyPair2.pubKey))
assertNotNull(Nip04.getSharedSecret(keyPair1.privKey!!, keyPair2.pubKey))
}
}
@@ -48,9 +50,10 @@ class CryptoBenchmark {
fun getSharedKeyNip44() {
val keyPair1 = KeyPair()
val keyPair2 = KeyPair()
val nip44v2 = Nip44v2()
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.nip44.v1.getSharedSecret(keyPair1.privKey!!, keyPair2.pubKey))
assertNotNull(nip44v2.getConversationKey(keyPair1.privKey!!, keyPair2.pubKey))
}
}
@@ -58,9 +61,10 @@ class CryptoBenchmark {
fun computeSharedKeyNip04() {
val keyPair1 = KeyPair()
val keyPair2 = KeyPair()
val nip04 = Encryption()
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.computeSharedSecretNIP04(keyPair1.privKey!!, keyPair2.pubKey))
assertNotNull(nip04.computeSharedSecret(keyPair1.privKey!!, keyPair2.pubKey))
}
}
@@ -68,40 +72,10 @@ class CryptoBenchmark {
fun computeSharedKeyNip44() {
val keyPair1 = KeyPair()
val keyPair2 = KeyPair()
val nip44v2 = Nip44v2()
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.nip44.v1.computeSharedSecret(keyPair1.privKey!!, keyPair2.pubKey))
}
}
@Test
fun random() {
benchmarkRule.measureRepeated { assertNotNull(CryptoUtils.random(1000)) }
}
@Test
fun sha256() {
val keyPair = KeyPair()
benchmarkRule.measureRepeated { assertNotNull(CryptoUtils.sha256(keyPair.pubKey)) }
}
@Test
fun sign() {
val keyPair = KeyPair()
val msg = CryptoUtils.sha256(CryptoUtils.random(1000))
benchmarkRule.measureRepeated { assertNotNull(CryptoUtils.sign(msg, keyPair.privKey!!)) }
}
@Test
fun verify() {
val keyPair = KeyPair()
val msg = CryptoUtils.sha256(CryptoUtils.random(1000))
val signature = CryptoUtils.sign(msg, keyPair.privKey!!)
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.verifySignature(signature, msg, keyPair.pubKey))
assertNotNull(nip44v2.computeConversationKey(keyPair1.privKey!!, keyPair2.pubKey))
}
}
}
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.sha256
import junit.framework.TestCase.assertNotNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SignVerifyBenchmark {
@get:Rule val benchmarkRule = BenchmarkRule()
@Test
fun random() {
benchmarkRule.measureRepeated { assertNotNull(RandomInstance.bytes(1000)) }
}
@Test
fun sign() {
val keyPair = KeyPair()
val msg = sha256(RandomInstance.bytes(1000))
benchmarkRule.measureRepeated { assertNotNull(Nip01.sign(msg, keyPair.privKey!!)) }
}
@Test
fun verify() {
val keyPair = KeyPair()
val msg = sha256(RandomInstance.bytes(1000))
val signature = Nip01.sign(msg, keyPair.privKey!!)
benchmarkRule.measureRepeated {
assertNotNull(Nip01.verify(signature, msg, keyPair.pubKey))
}
}
}
@@ -85,8 +85,8 @@ import com.vitorpamplona.amethyst.commons.robohash.parts.mouth6Cell
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth7Happy
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth8Buttons
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth9Closed
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.sha256
val Black = SolidColor(Color.Black)
val Gray = SolidColor(Color(0xFF6d6e70))
@@ -168,7 +168,7 @@ class RobohashAssembler {
Hex.decode(msg)
} else {
Log.w("Robohash", "$msg is not a hex")
CryptoUtils.sha256(msg.toByteArray())
sha256(msg.toByteArray())
}
val bgColor = SolidColor(bytesToColor(hash[0], hash[1], hash[2], isLightTheme))
@@ -1,140 +0,0 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CryptoUtilsTest {
@Test
fun testGetPublicFromPrivateKey() {
val privateKey =
"f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
val publicKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
assertEquals("7d4b8806f1fd713c287235411bf95aa81b7242ead892733ec84b3f2719845be6", publicKey)
}
@Test
fun testSharedSecretCompatibilityWithCoracle() {
val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561"
val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f"
val key =
CryptoUtils.nip44.v1.getSharedSecret(
privateKey = privateKey.hexToByteArray(),
pubKey = publicKey.hexToByteArray(),
)
assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey())
}
@Test
fun testSharedSecret() {
val sender = KeyPair()
val receiver = KeyPair()
val sharedSecret1 = CryptoUtils.nip44.v1.getSharedSecret(sender.privKey!!, receiver.pubKey)
val sharedSecret2 = CryptoUtils.nip44.v1.getSharedSecret(receiver.privKey!!, sender.pubKey)
assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey())
val secretKey1 = KeyPair(privKey = sharedSecret1)
val secretKey2 = KeyPair(privKey = sharedSecret2)
assertEquals(secretKey1.pubKey.toHexKey(), secretKey2.pubKey.toHexKey())
assertEquals(secretKey1.privKey?.toHexKey(), secretKey2.privKey?.toHexKey())
}
@Test
fun encryptDecryptNIP4Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val encrypted = CryptoUtils.encryptNIP04(msg, privateKey, publicKey)
val decrypted = CryptoUtils.decryptNIP04(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptDecryptNIP44v1Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val encrypted = CryptoUtils.nip44.v1.encrypt(msg, privateKey, publicKey)
val decrypted = CryptoUtils.nip44.v1.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptSharedSecretDecryptNIP4Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val encrypted = CryptoUtils.encryptNIP04(msg, privateKey, publicKey)
val decrypted = CryptoUtils.decryptNIP04(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptSharedSecretDecryptNIP44v1Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val sharedSecret = CryptoUtils.nip44.v1.getSharedSecret(privateKey, publicKey)
val encrypted = CryptoUtils.nip44.v1.encrypt(msg, sharedSecret)
val decrypted = CryptoUtils.nip44.v1.decrypt(encrypted, sharedSecret)
assertEquals(msg, decrypted)
}
@Test
fun signString() {
val random = "319cc5596fdd6cd767e5a59d976e8e059c61306af90dff1e6ee1067b3a1fdbc0".hexToByteArray()
val message = "8e58c8251bb406b6ded69e9eb14f55282a9a53bdab16fc49a3218c2ad3abc887".hexToByteArray()
val keyPair = KeyPair("a5ab474552c8f9c46c2eda5a0b68f27430ad81f96cb405e0cb4e34bf0c6494a2".hexToByteArray())
val signedMessage = CryptoUtils.sign(message, keyPair.privKey!!, random).toHexKey()
val expectedValue = "0f9be7e01ba53d5ee6874b9180c7956269fda7a5be424634c3d17b5cfcea6da001be89183876415ba08b7dafa6cff4555e393dc228fb8769b384344e9a27b77c"
assertEquals(expectedValue, signedMessage)
val message2 = "Hello"
val signedMessage2 = CryptoUtils.signString(message2, keyPair.privKey!!, random).toHexKey()
val expectedValue2 = "7ec8194a585bfb513564113b6b7bfeaafa0254c99d24eaf92280657c2291bab908b1b7bc553c83276a0254aef5041bbe6a50e93381edc4de3d859efa1c3a5a1e"
assertEquals(expectedValue2, signedMessage2)
}
}
@@ -21,9 +21,10 @@
package com.vitorpamplona.quartz.bloom
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.utils.RandomInstance
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
@@ -41,7 +42,7 @@ class BloomFilter(
private val size: Int,
private val rounds: Int,
private val bits: BitSet = BitSet(size),
private val salt: ByteArray = CryptoUtils.random(8),
private val salt: ByteArray = RandomInstance.bytes(8),
) {
private val hash = MessageDigest.getInstance("SHA-256")
private val lock = ReentrantReadWriteLock()
@@ -135,7 +136,7 @@ class BloomFilterTest {
var failureCounter = 0
for (seed in 0..1000000) {
if (bloomFilter.mightContains(CryptoUtils.pubkeyCreate(CryptoUtils.privkeyCreate()))) {
if (bloomFilter.mightContains(Nip01.pubKeyCreate(Nip01.privKeyCreate()))) {
failureCounter++
}
}
@@ -22,25 +22,23 @@ package com.vitorpamplona.quartz.nip01Core
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.utils.sha256Hash
import fr.acinq.secp256k1.Secp256k1
import com.vitorpamplona.quartz.utils.Secp256k1Instance
import com.vitorpamplona.quartz.utils.sha256
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.security.SecureRandom
@RunWith(AndroidJUnit4::class)
class Nip01Test {
private val nip01 = Nip01(Secp256k1.get(), SecureRandom())
private val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
@Test
fun testGetPublicFromPrivateKey() {
assertEquals(
"7d4b8806f1fd713c287235411bf95aa81b7242ead892733ec84b3f2719845be6",
nip01.pubkeyCreate(privateKey).toHexKey(),
Nip01.pubKeyCreate(privateKey).toHexKey(),
)
}
@@ -49,7 +47,7 @@ class Nip01Test {
val key = "e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0".hexToByteArray()
assertEquals(
"029fa4ce8c87ca546b196e6518db80a6780e1bd5552b61f9f17bafee5d4e34e09b",
nip01.compressedPubkeyCreate(key).toHexKey(),
Secp256k1Instance.compressedPubKeyFor(key).toHexKey(),
)
}
@@ -58,7 +56,7 @@ class Nip01Test {
val key = "65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a".hexToByteArray()
assertEquals(
"033dcef7585efbdb68747d919152bd481e21f5e952aaaef5a19604fbd096a93dd5",
nip01.compressedPubkeyCreate(key).toHexKey(),
Secp256k1Instance.compressedPubKeyFor(key).toHexKey(),
)
}
@@ -66,7 +64,7 @@ class Nip01Test {
fun testDeterministicSign() {
assertEquals(
"1484d0e0bd62165e822e31f1f4cc8e1ce8e20c30a060e24fb0ecd7baf7c624f661fb7a3e4f0ddb43018e5f0b4892c929af64d8b7a86021aa081ec8231e3dfa37",
nip01.signDeterministic(sha256Hash("Test".toByteArray()), privateKey).toHexKey(),
Nip01.sign(sha256("Test".toByteArray()), privateKey, null).toHexKey(),
)
}
@@ -74,17 +72,17 @@ class Nip01Test {
fun testSha256() {
assertEquals(
"532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25",
sha256Hash("Test".toByteArray()).toHexKey(),
sha256("Test".toByteArray()).toHexKey(),
)
}
@Test
fun testDeterministicVerify() {
assertTrue(
nip01.verify(
Nip01.verify(
"1484d0e0bd62165e822e31f1f4cc8e1ce8e20c30a060e24fb0ecd7baf7c624f661fb7a3e4f0ddb43018e5f0b4892c929af64d8b7a86021aa081ec8231e3dfa37".hexToByteArray(),
sha256Hash("Test".toByteArray()),
nip01.pubkeyCreate(privateKey),
sha256("Test".toByteArray()),
Nip01.pubKeyCreate(privateKey),
),
)
}
@@ -93,18 +91,18 @@ class Nip01Test {
fun testNonDeterministicSign() {
assertNotEquals(
"1484d0e0bd62165e822e31f1f4cc8e1ce8e20c30a060e24fb0ecd7baf7c624f661fb7a3e4f0ddb43018e5f0b4892c929af64d8b7a86021aa081ec8231e3dfa37",
nip01.sign(sha256Hash("Test".toByteArray()), privateKey).toHexKey(),
Nip01.sign(sha256("Test".toByteArray()), privateKey).toHexKey(),
)
}
@Test
fun testNonDeterministicSignVerify() {
val signature = nip01.sign(sha256Hash("Test".toByteArray()), privateKey)
val signature = Nip01.sign(sha256("Test".toByteArray()), privateKey)
assertTrue(
nip01.verify(
Nip01.verify(
signature,
sha256Hash("Test".toByteArray()),
nip01.pubkeyCreate(privateKey),
sha256("Test".toByteArray()),
Nip01.pubKeyCreate(privateKey),
),
)
}
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip01CryptoTest {
@Test
fun testGetPublicFromPrivateKey() {
val privateKey =
"f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
val publicKey = Nip01.pubKeyCreate(privateKey).toHexKey()
assertEquals("7d4b8806f1fd713c287235411bf95aa81b7242ead892733ec84b3f2719845be6", publicKey)
}
}
@@ -23,21 +23,19 @@ package com.vitorpamplona.quartz.nip04Dm
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import fr.acinq.secp256k1.Secp256k1
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
import com.vitorpamplona.quartz.nip04Dm.crypto.Encryption
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import org.junit.Test
import java.security.SecureRandom
class Nip04EncryptionTest {
private val random = SecureRandom()
private val nip01 = Nip01(Secp256k1.get(), random)
private val nip04 = Nip04Encryption(Secp256k1.get(), random)
class EncryptionTest {
private val nip04 = Encryption()
val sk1 = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe".hexToByteArray()
val sk2 = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220".hexToByteArray()
val pk1 = nip01.pubkeyCreate(sk1)
val pk2 = nip01.pubkeyCreate(sk2)
val pk1 = Nip01.pubKeyCreate(sk1)
val pk2 = Nip01.pubKeyCreate(sk2)
val expectedShared = "7ce22696eb0e303ddaa491bdf2a56b79d249f2d861b8e012a933e01dc4beba81"
@@ -89,17 +87,17 @@ class Nip04EncryptionTest {
@Test
fun isNIP04Encode() {
assertTrue(Nip04Encryption.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA=="))
assertTrue(Nip04Encryption.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="))
assertTrue(EncryptedInfo.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA=="))
assertTrue(EncryptedInfo.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="))
assertTrue(
Nip04Encryption.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="),
EncryptedInfo.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="),
)
}
@Test
fun isNIP04EncodeWithBug() {
assertTrue(
Nip04Encryption.isNIP04(
EncryptedInfo.isNIP04(
"QOAYBWa88ConWs2C4kSvNqAcowCtg0ZRtAl7FyLSv9VMaJH4oCiDx0h8VLBnV97HdE4lv" +
"TW7AYC1eEw8/t1dbe0qRc3XrOt7MrPAO8yqpy1/3lFB1+10kip0+KdgT8Quvv02wTP8Dqi" +
"xpr2fliAIG2ONvDn+O5V0q9aVUN9HitgL/myTyR0T42edmxWeZoMBEOKvJyO80FekSsgVL" +
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip04Test {
@Test
fun encryptDecryptNIP4Test() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val encrypted = Nip04.encrypt(msg, privateKey, publicKey)
val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptSharedSecretDecryptNIP4Test() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val encrypted = Nip04.encrypt(msg, privateKey, publicKey)
val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
}
@@ -22,14 +22,13 @@ package com.vitorpamplona.quartz.nip06KeyDerivation
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.toHexKey
import fr.acinq.secp256k1.Secp256k1
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Bip32SeedDerivationTest {
val seedDerivation = Bip32SeedDerivation(Secp256k1.get())
val seedDerivation = Bip32SeedDerivation()
val masterBitcoin =
seedDerivation.generate(
@@ -23,9 +23,9 @@ package com.vitorpamplona.quartz.nip06KeyDerivation
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.RandomInstance
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
@@ -69,7 +69,7 @@ class Bip39MnemonicsTest {
fun validateMnemonicsValid() {
for (i in 0..99) {
for (length in listOf(16, 20, 24, 28, 32, 36, 40)) {
val mnemonics = Bip39Mnemonics.toMnemonics(CryptoUtils.random(length))
val mnemonics = Bip39Mnemonics.toMnemonics(RandomInstance.bytes(length))
Bip39Mnemonics.validate(mnemonics)
}
}
@@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip06KeyDerivation
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.toHexKey
import fr.acinq.secp256k1.Secp256k1
import junit.framework.TestCase.assertEquals
import org.junit.Ignore
import org.junit.Test
@@ -30,7 +29,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip06Test {
val nip06 = Nip06(Secp256k1.get())
val nip06 = Nip06()
// private key (hex): 7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a
// nsec: nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip44Encryption
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip44v1Test {
private val nip44v1 = Nip44v1()
@Test
fun testSharedSecretCompatibilityWithCoracle() {
val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561"
val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f"
val key =
nip44v1.getSharedSecret(
privateKey = privateKey.hexToByteArray(),
pubKey = publicKey.hexToByteArray(),
)
assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey())
}
@Test
fun testSharedSecret() {
val sender = KeyPair()
val receiver = KeyPair()
val sharedSecret1 = nip44v1.getSharedSecret(sender.privKey!!, receiver.pubKey)
val sharedSecret2 = nip44v1.getSharedSecret(receiver.privKey!!, sender.pubKey)
assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey())
val secretKey1 = KeyPair(privKey = sharedSecret1)
val secretKey2 = KeyPair(privKey = sharedSecret2)
assertEquals(secretKey1.pubKey.toHexKey(), secretKey2.pubKey.toHexKey())
assertEquals(secretKey1.privKey?.toHexKey(), secretKey2.privKey?.toHexKey())
}
@Test
fun encryptDecrypt() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val encrypted = nip44v1.encrypt(msg, privateKey, publicKey)
val decrypted = nip44v1.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptDecryptSharedSecret() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val sharedSecret = nip44v1.getSharedSecret(privateKey, publicKey)
val encrypted = nip44v1.encrypt(msg, sharedSecret)
val decrypted = nip44v1.decrypt(encrypted, sharedSecret)
assertEquals(msg, decrypted)
}
}
@@ -27,18 +27,17 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.utils.sha256Hash
import fr.acinq.secp256k1.Secp256k1
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.sha256
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertNull
import junit.framework.TestCase.fail
import org.junit.Test
import org.junit.runner.RunWith
import java.security.SecureRandom
@RunWith(AndroidJUnit4::class)
class NIP44v2Test {
class Nip44v2Test {
private val vectors: VectorFile =
jacksonObjectMapper()
.readValue(
@@ -46,9 +45,7 @@ class NIP44v2Test {
VectorFile::class.java,
)
private val random = SecureRandom()
private val nip44v2 = Nip44v2(Secp256k1.get(), random)
private val nip01 = Nip01(Secp256k1.get(), random)
private val nip44v2 = Nip44v2()
@Test
fun conversationKeyTest() {
@@ -73,8 +70,8 @@ class NIP44v2Test {
val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
val privateKeyB = "65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a".hexToByteArray()
val publicKeyA = nip01.pubkeyCreate(privateKeyA)
val publicKeyB = nip01.pubkeyCreate(privateKeyB)
val publicKeyA = Nip01.pubKeyCreate(privateKeyA)
val publicKeyB = Nip01.pubKeyCreate(privateKeyB)
assertEquals(
nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(),
@@ -87,8 +84,8 @@ class NIP44v2Test {
val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
val privateKeyB = "e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0".hexToByteArray()
val publicKeyA = nip01.pubkeyCreate(privateKeyA)
val publicKeyB = nip01.pubkeyCreate(privateKeyB)
val publicKeyA = Nip01.pubKeyCreate(privateKeyA)
val publicKeyB = Nip01.pubKeyCreate(privateKeyB)
assertEquals(
nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(),
@@ -149,8 +146,7 @@ class NIP44v2Test {
@Test
fun invalidMessageLengths() {
for (v in vectors.v2?.invalid?.encryptMsgLengths!!) {
val key = ByteArray(32)
random.nextBytes(key)
val key = RandomInstance.bytes(32)
try {
nip44v2.encrypt("a".repeat(v), key)
fail("Should Throw for $v")
@@ -185,5 +181,5 @@ class NIP44v2Test {
}
}
private fun sha256Hex(data: ByteArray) = sha256Hash(data).toHexKey()
private fun sha256Hex(data: ByteArray) = sha256(data).toHexKey()
}
@@ -22,13 +22,11 @@ package com.vitorpamplona.quartz.nip49PrivKeyEnc
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.toHexKey
import fr.acinq.secp256k1.Secp256k1
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.fail
import org.junit.Test
import org.junit.runner.RunWith
import java.security.SecureRandom
@RunWith(AndroidJUnit4::class)
public class NIP49Test {
@@ -53,8 +51,7 @@ public class NIP49Test {
)
}
val random = SecureRandom()
val nip49 = Nip49(Secp256k1.get(), random)
val nip49 = Nip49()
@Test
fun decodeBech32() {
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip55AndroidSigner
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SignStringTest {
@Test
fun signString() {
val random = "319cc5596fdd6cd767e5a59d976e8e059c61306af90dff1e6ee1067b3a1fdbc0".hexToByteArray()
val message = "8e58c8251bb406b6ded69e9eb14f55282a9a53bdab16fc49a3218c2ad3abc887".hexToByteArray()
val keyPair = KeyPair("a5ab474552c8f9c46c2eda5a0b68f27430ad81f96cb405e0cb4e34bf0c6494a2".hexToByteArray())
val signedMessage = Nip01.sign(message, keyPair.privKey!!, random).toHexKey()
val expectedValue = "0f9be7e01ba53d5ee6874b9180c7956269fda7a5be424634c3d17b5cfcea6da001be89183876415ba08b7dafa6cff4555e393dc228fb8769b384344e9a27b77c"
assertEquals(expectedValue, signedMessage)
val message2 = "Hello"
val signedMessage2 = signString(message2, keyPair.privKey!!, random).toHexKey()
val expectedValue2 = "7ec8194a585bfb513564113b6b7bfeaafa0254c99d24eaf92280657c2291bab908b1b7bc553c83276a0254aef5041bbe6a50e93381edc4de3d859efa1c3a5a1e"
assertEquals(expectedValue2, signedMessage2)
}
}
@@ -21,7 +21,6 @@
package com.vitorpamplona.quartz.utils
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.CryptoUtils
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -56,7 +55,7 @@ class HexEncodingTest {
@Test
fun testRandoms() {
for (i in 0..1000) {
val bytes = CryptoUtils.privkeyCreate()
val bytes = RandomInstance.bytes(32)
val hex =
fr.acinq.secp256k1.Hex
.encode(bytes)
@@ -90,7 +89,7 @@ class HexEncodingTest {
@Test
fun testRandomsIsHex() {
for (i in 0..10000) {
val bytes = CryptoUtils.privkeyCreate()
val bytes = RandomInstance.bytes(32)
val hex = bytes.toHexString(HexFormat.Default)
assertTrue(hex, Hex.isHex(hex))
val hexUpper = bytes.toHexString(HexFormat.UpperCase)
@@ -102,7 +101,7 @@ class HexEncodingTest {
@Test
fun testRandomsUppercase() {
for (i in 0..1000) {
val bytes = CryptoUtils.privkeyCreate()
val bytes = RandomInstance.bytes(32)
val hex = bytes.toHexString(HexFormat.UpperCase)
assertEquals(
bytes.toList(),
@@ -1,189 +0,0 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip04Dm.Nip04Encoder
import com.vitorpamplona.quartz.nip04Dm.Nip04Encryption
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
import com.vitorpamplona.quartz.nip44Encryption.Nip44
import com.vitorpamplona.quartz.nip44Encryption.Nip44v2
import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49
import com.vitorpamplona.quartz.utils.nextBytes
import com.vitorpamplona.quartz.utils.sha256Hash
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
object CryptoUtils {
private val secp256k1 = Secp256k1.get()
private val random = SecureRandom()
val nip01 = Nip01(secp256k1, random)
val nip06 = Nip06(secp256k1)
val nip04 = Nip04Encryption(secp256k1, random)
val nip44 = Nip44(secp256k1, random, nip04)
val nip49 = Nip49(secp256k1, random)
fun clearCache() {
nip04.clearCache()
nip44.clearCache()
}
/** Provides a 32B "private key" aka random number */
fun privkeyCreate() = nip01.privkeyCreate()
fun pubkeyCreate(privKey: ByteArray) = nip01.pubkeyCreate(privKey)
fun randomInt(bound: Int) = random.nextInt(bound)
fun random(size: Int) = random.nextBytes(size)
fun signString(
message: String,
privKey: ByteArray,
auxrand32: ByteArray = random(32),
): ByteArray = nip01.signString(message, privKey, auxrand32)
fun sign(
data: ByteArray,
privKey: ByteArray,
auxrand32: ByteArray? = null,
): ByteArray = nip01.sign(data, privKey, auxrand32)
fun verifySignature(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean = nip01.verify(signature, hash, pubKey)
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return sha256Hash(data)
}
fun decrypt(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? =
if (Nip04Encryption.isNIP04(msg)) {
decryptNIP04(msg, privateKey, pubKey)
} else {
decryptNIP44(msg, privateKey, pubKey)
}
/** NIP 04 Utils */
fun encryptNIP04(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String = nip04.encrypt(msg, privateKey, pubKey)
fun encryptNIP04(
msg: String,
sharedSecret: ByteArray,
): Nip04Encoder = nip04.encrypt(msg, sharedSecret)
fun decryptNIP04(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String = nip04.decrypt(msg, privateKey, pubKey)
fun decryptNIP04(
encryptedInfo: Nip04Encoder,
privateKey: ByteArray,
pubKey: ByteArray,
): String = nip04.decrypt(encryptedInfo, privateKey, pubKey)
fun decryptNIP04(
msg: String,
sharedSecret: ByteArray,
): String = nip04.decrypt(msg, sharedSecret)
private fun decryptNIP04(
cipher: String,
nonce: String,
sharedSecret: ByteArray,
): String = nip04.decrypt(cipher, nonce, sharedSecret)
private fun decryptNIP04(
encryptedMsg: ByteArray,
iv: ByteArray,
sharedSecret: ByteArray,
): String = nip04.decrypt(encryptedMsg, iv, sharedSecret)
fun getSharedSecretNIP04(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = nip04.getSharedSecret(privateKey, pubKey)
fun computeSharedSecretNIP04(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = nip04.computeSharedSecret(privateKey, pubKey)
/** NIP 06 Utils */
fun isValidMnemonic(mnemonic: String): Boolean = nip06.isValidMnemonic(mnemonic)
fun privateKeyFromMnemonic(
mnemonic: String,
account: Long = 0,
) = nip06.privateKeyFromMnemonic(mnemonic, account)
/** NIP 44 Utils */
fun getSharedSecretNIP44(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = nip44.getSharedSecret(privateKey, pubKey)
fun computeSharedSecretNIP44(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = nip44.computeSharedSecret(privateKey, pubKey)
fun encryptNIP44(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): Nip44v2.EncryptedInfo = nip44.encrypt(msg, privateKey, pubKey)
fun decryptNIP44(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? = nip44.decrypt(payload, privateKey, pubKey)
/** NIP 49 Utils */
fun decryptNIP49(
payload: String,
password: String,
): String? {
if (payload.isEmpty() || password.isEmpty()) return null
return nip49.decrypt(payload, password)
}
fun encryptNIP49(
key: HexKey,
password: String,
): String = nip49.encrypt(key, password)
}
@@ -42,7 +42,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip04Dm.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
@@ -21,8 +21,8 @@
package com.vitorpamplona.quartz.nip01Core
import android.util.Log
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.utils.Hex
fun Event.generateId(): String = EventHasher.hashId(pubKey, createdAt, kind, tags, content)
@@ -34,7 +34,7 @@ fun Event.verifyId(): Boolean {
fun Event.verifySignature(): Boolean {
if (id.isEmpty() || sig.isEmpty()) return false
return CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
return Nip01.verify(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
}
/** Checks if the ID is correct and then if the pubKey's secret key signed the event. */
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.utils.sha256Hash
import com.vitorpamplona.quartz.utils.sha256
class EventHasher {
companion object {
@@ -61,7 +61,7 @@ class EventHasher {
kind: Int,
tags: Array<Array<String>>,
content: String,
): ByteArray = sha256Hash(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
): ByteArray = sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
fun hashId(
pubKey: HexKey,
@@ -20,13 +20,11 @@
*/
package com.vitorpamplona.quartz.nip01Core.crypto
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
class KeyPair(
privKey: ByteArray? = null,
pubKey: ByteArray? = null,
forcePubKeyCheck: Boolean = true,
) {
val privKey: ByteArray?
val pubKey: ByteArray
@@ -35,8 +33,8 @@ class KeyPair(
if (privKey == null) {
if (pubKey == null) {
// create new, random keys
this.privKey = CryptoUtils.privkeyCreate()
this.pubKey = CryptoUtils.pubkeyCreate(this.privKey)
this.privKey = Nip01.privKeyCreate()
this.pubKey = Nip01.pubKeyCreate(this.privKey)
} else {
// this is a read-only account
check(pubKey.size == 32)
@@ -46,12 +44,7 @@ class KeyPair(
} else {
// as private key is provided, ignore the public key and set keys according to private key
this.privKey = privKey
if (pubKey == null || forcePubKeyCheck) {
this.pubKey = CryptoUtils.pubkeyCreate(privKey)
} else {
this.pubKey = pubKey
}
this.pubKey = Nip01.pubKeyCreate(privKey)
}
}
@@ -20,45 +20,23 @@
*/
package com.vitorpamplona.quartz.nip01Core.crypto
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.utils.nextBytes
import com.vitorpamplona.quartz.utils.sha256Hash
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.Secp256k1Instance
class Nip01(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
/** Provides a 32B "private key" aka random number */
fun privkeyCreate() = random.nextBytes(32)
object Nip01 {
fun privKeyCreate() = RandomInstance.bytes(32)
fun compressedPubkeyCreate(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
fun pubkeyCreate(privKey: ByteArray) = compressedPubkeyCreate(privKey).copyOfRange(1, 33)
fun pubKeyCreate(privKey: ByteArray) = Secp256k1Instance.compressedPubKeyFor(privKey).copyOfRange(1, 33)
fun sign(
data: ByteArray,
privKey: ByteArray,
nonce: ByteArray? = random.nextBytes(32),
): ByteArray = secp256k1.signSchnorr(data, privKey, nonce)
fun signDeterministic(
data: ByteArray,
privKey: ByteArray,
): ByteArray = secp256k1.signSchnorr(data, privKey, null)
nonce: ByteArray? = RandomInstance.bytes(32),
): ByteArray = Secp256k1Instance.signSchnorr(data, privKey, nonce)
fun verify(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean = secp256k1.verifySchnorr(signature, hash, pubKey)
fun sha256(data: ByteArray) = sha256Hash(data)
fun signString(
message: String,
privKey: ByteArray,
nonce: ByteArray = random.nextBytes(32),
): ByteArray = sign(CryptoUtils.sha256(message.toByteArray()), privKey, nonce)
): Boolean = Secp256k1Instance.verifySchnorr(signature, hash, pubKey)
}
@@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.EventFactory
import com.vitorpamplona.quartz.nip01Core.EventHasher
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip04Dm.Nip04Encryption
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
@@ -78,7 +78,7 @@ abstract class NostrSigner(
fromPublicKey: HexKey,
onReady: (String) -> Unit,
) {
if (Nip04Encryption.isNIP04(encryptedContent)) {
if (EncryptedInfo.isNIP04(encryptedContent)) {
nip04Decrypt(encryptedContent, fromPublicKey, onReady)
} else {
nip44Decrypt(encryptedContent, fromPublicKey, onReady)
@@ -21,14 +21,16 @@
package com.vitorpamplona.quartz.nip01Core.signers
import android.util.Log
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.EventFactory
import com.vitorpamplona.quartz.nip01Core.EventHasher
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
import com.vitorpamplona.quartz.nip44Encryption.Nip44
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.PrivateZapRequestBuilder
@@ -69,7 +71,7 @@ class NostrSignerSync(
if (keyPair.privKey == null) return null
val id = EventHasher.hashIdBytes(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, keyPair.privKey).toHexKey()
val sig = Nip01.sign(id, keyPair.privKey).toHexKey()
return EventFactory.create(
id.toHexKey(),
@@ -88,7 +90,7 @@ class NostrSignerSync(
): String? {
if (keyPair.privKey == null) return null
return CryptoUtils.encryptNIP04(
return Nip04.encrypt(
decryptedContent,
keyPair.privKey,
toPublicKey.hexToByteArray(),
@@ -102,10 +104,7 @@ class NostrSignerSync(
if (keyPair.privKey == null) return null
return try {
val sharedSecret =
CryptoUtils.getSharedSecretNIP04(keyPair.privKey, fromPublicKey.hexToByteArray())
CryptoUtils.decryptNIP04(encryptedContent, sharedSecret)
Nip04.decrypt(encryptedContent, keyPair.privKey, fromPublicKey.hexToByteArray())
} catch (e: Exception) {
Log.w("NIP04Decrypt", "Error decrypting the message ${e.message} on $encryptedContent")
null
@@ -118,8 +117,8 @@ class NostrSignerSync(
): String? {
if (keyPair.privKey == null) return null
return CryptoUtils
.encryptNIP44(
return Nip44
.encrypt(
decryptedContent,
keyPair.privKey,
toPublicKey.hexToByteArray(),
@@ -132,12 +131,11 @@ class NostrSignerSync(
): String? {
if (keyPair.privKey == null) return null
return CryptoUtils
.decryptNIP44(
payload = encryptedContent,
privateKey = keyPair.privKey,
pubKey = fromPublicKey.hexToByteArray(),
)
return Nip44.decrypt(
payload = encryptedContent,
privateKey = keyPair.privKey,
pubKey = fromPublicKey.hexToByteArray(),
)
}
fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent? = PrivateZapRequestBuilder().decryptZapEvent(event, this)
@@ -1,130 +0,0 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm
import com.vitorpamplona.quartz.nip44Encryption.SharedKeyCache
import com.vitorpamplona.quartz.utils.Hex
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class Nip04Encryption(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val sharedKeyCache = SharedKeyCache()
private val h02 = Hex.decode("02")
fun clearCache() {
sharedKeyCache.clearCache()
}
fun encrypt(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String = encrypt(msg, getSharedSecret(privateKey, pubKey)).encodeToNIP04()
fun encrypt(
msg: String,
sharedSecret: ByteArray,
): Nip04Encoder {
val iv = ByteArray(16)
random.nextBytes(iv)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
val encryptedMsg = cipher.doFinal(msg.toByteArray())
return Nip04Encoder(encryptedMsg, iv)
}
fun decrypt(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
val sharedSecret = getSharedSecret(privateKey, pubKey)
return decrypt(msg, sharedSecret)
}
fun decrypt(
encryptedInfo: Nip04Encoder,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
val sharedSecret = getSharedSecret(privateKey, pubKey)
return decrypt(encryptedInfo.ciphertext, encryptedInfo.nonce, sharedSecret)
}
fun decrypt(
msg: String,
sharedSecret: ByteArray,
): String {
val decoded = Nip04Encoder.decodeFromNIP04(msg)
check(decoded != null) { "Unable to decode msg $msg as NIP04" }
return decrypt(decoded.ciphertext, decoded.nonce, sharedSecret)
}
fun decrypt(
cipher: String,
nonce: String,
sharedSecret: ByteArray,
): String {
val iv = Base64.getDecoder().decode(nonce)
val encryptedMsg = Base64.getDecoder().decode(cipher)
return decrypt(encryptedMsg, iv, sharedSecret)
}
fun decrypt(
encryptedMsg: ByteArray,
iv: ByteArray,
sharedSecret: ByteArray,
): String {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv))
return String(cipher.doFinal(encryptedMsg))
}
fun getSharedSecret(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
val preComputed = sharedKeyCache.get(privateKey, pubKey)
if (preComputed != null) return preComputed
val computed = computeSharedSecret(privateKey, pubKey)
sharedKeyCache.add(privateKey, pubKey, computed)
return computed
}
/** @return 32B shared secret */
fun computeSharedSecret(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
companion object {
fun isNIP04(encoded: String) = Nip04Encoder.isNIP04(encoded)
}
}
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm.crypto
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
import com.vitorpamplona.quartz.utils.RandomInstance
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class AESCBC(
val keyBytes: ByteArray = RandomInstance.bytes(32),
val iv: ByteArray = RandomInstance.bytes(16),
) : NostrCipher {
private fun newCipher() = Cipher.getInstance("AES/CBC/PKCS5Padding")
private fun keySpec() = SecretKeySpec(keyBytes, "AES")
private fun param() = IvParameterSpec(iv)
override fun name() = NAME
override fun encrypt(bytesToEncrypt: ByteArray): ByteArray =
with(newCipher()) {
init(Cipher.ENCRYPT_MODE, keySpec(), param())
doFinal(bytesToEncrypt)
}
override fun decrypt(bytesToDecrypt: ByteArray): ByteArray =
with(newCipher()) {
init(Cipher.DECRYPT_MODE, keySpec(), param())
doFinal(bytesToDecrypt)
}
companion object {
const val NAME = "aes-cbc"
}
}
@@ -18,32 +18,20 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm
package com.vitorpamplona.quartz.nip04Dm.crypto
import android.util.Log
import java.util.Base64
class Nip04Encoder(
class EncryptedInfo(
val ciphertext: ByteArray,
val nonce: ByteArray,
) {
fun encodeToNIP04() = encode(ciphertext, nonce)
companion object {
const val V: Int = 0
fun decodePayload(payload: String): Nip04Encoder? {
return try {
val byteArray = Base64.getDecoder().decode(payload)
check(byteArray[0].toInt() == V)
return Nip04Encoder(
nonce = byteArray.copyOfRange(1, 25),
ciphertext = byteArray.copyOfRange(25, byteArray.size),
)
} catch (e: Exception) {
Log.w("NIP04", "Unable to Parse encrypted payload: $payload")
null
}
}
fun isNIP04(encoded: String): Boolean {
// cleaning up some bug from some client.
val cleanedUp = encoded.removeSuffix("-null")
@@ -56,11 +44,20 @@ class Nip04Encoder(
cleanedUp[l - 25] == '='
}
fun decodeFromNIP04(payload: String): Nip04Encoder? =
fun encode(
ciphertext: ByteArray,
nonce: ByteArray,
): String {
val nonceB64 = Base64.getEncoder().encodeToString(nonce)
val ciphertextB64 = Base64.getEncoder().encodeToString(ciphertext)
return "$ciphertextB64?iv=$nonceB64"
}
fun decode(payload: String): EncryptedInfo? =
try {
// cleaning up some bug from some client.
val parts = payload.removeSuffix("-null").split("?iv=")
Nip04Encoder(
EncryptedInfo(
ciphertext = Base64.getDecoder().decode(parts[0]),
nonce = Base64.getDecoder().decode(parts[1]),
)
@@ -69,10 +66,4 @@ class Nip04Encoder(
null
}
}
fun encodeToNIP04(): String {
val nonce = Base64.getEncoder().encodeToString(nonce)
val ciphertext = Base64.getEncoder().encodeToString(ciphertext)
return "$ciphertext?iv=$nonce"
}
}
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm.crypto
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.Secp256k1Instance
class Encryption {
fun encrypt(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String = encrypt(msg, computeSharedSecret(privateKey, pubKey))
fun decrypt(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String = decrypt(msg, computeSharedSecret(privateKey, pubKey))
fun encrypt(
msg: String,
sharedSecret: ByteArray,
): String = encryptToEncoder(msg, sharedSecret).encodeToNIP04()
fun encryptToEncoder(
msg: String,
sharedSecret: ByteArray,
): EncryptedInfo {
val iv = RandomInstance.bytes(16)
return EncryptedInfo(AESCBC(sharedSecret, iv).encrypt(msg.toByteArray()), iv)
}
fun decrypt(
msg: String,
sharedSecret: ByteArray,
): String {
val decoded = EncryptedInfo.decode(msg)
check(decoded != null) { "Unable to decode msg $msg as NIP04" }
return decrypt(decoded, sharedSecret)
}
fun decrypt(
msg: EncryptedInfo,
sharedSecret: ByteArray,
): String = decrypt(msg.ciphertext, msg.nonce, sharedSecret)
fun decrypt(
encryptedMsg: ByteArray,
iv: ByteArray,
sharedSecret: ByteArray,
): String = String(AESCBC(sharedSecret, iv).decrypt(encryptedMsg))
fun computeSharedSecret(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = Secp256k1Instance.pubKeyTweakMulCompact(pubKey, privateKey)
}
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm.crypto
import com.vitorpamplona.quartz.nip44Encryption.SharedKeyCache
object Nip04 {
private val sharedKeyCache = SharedKeyCache()
private val nip04 = Encryption()
fun clearCache() {
sharedKeyCache.clearCache()
}
fun getSharedSecret(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
val preComputed = sharedKeyCache.get(privateKey, pubKey)
if (preComputed != null) return preComputed
val computed = nip04.computeSharedSecret(privateKey, pubKey)
sharedKeyCache.add(privateKey, pubKey, computed)
return computed
}
fun encrypt(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String = nip04.encrypt(msg, getSharedSecret(privateKey, pubKey))
fun decrypt(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String = nip04.decrypt(msg, getSharedSecret(privateKey, pubKey))
fun decrypt(
msg: EncryptedInfo,
privateKey: ByteArray,
pubKey: ByteArray,
): String = nip04.decrypt(msg, getSharedSecret(privateKey, pubKey))
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm
package com.vitorpamplona.quartz.nip04Dm.messages
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm
package com.vitorpamplona.quartz.nip04Dm.messages
import com.vitorpamplona.quartz.nip01Core.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
@@ -20,16 +20,13 @@
*/
package com.vitorpamplona.quartz.nip06KeyDerivation
import fr.acinq.secp256k1.Secp256k1
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import com.vitorpamplona.quartz.utils.Secp256k1Instance
import com.vitorpamplona.quartz.utils.hmac512
/*
Simplified from: https://github.com/ACINQ/bitcoin-kmp/
*/
class Bip32SeedDerivation(
val secp256k1: Secp256k1,
) {
class Bip32SeedDerivation {
class ExtendedPrivateKey(
val secretkeybytes: ByteArray,
val chaincode: ByteArray,
@@ -46,24 +43,6 @@ class Bip32SeedDerivation(
return ExtendedPrivateKey(il, ir)
}
fun hmac512(
key: ByteArray,
data: ByteArray,
): ByteArray {
val mac = Mac.getInstance("HmacSHA512")
mac.init(SecretKeySpec(key, "HmacSHA512"))
return mac.doFinal(data)
}
fun isPrivKeyValid(il: ByteArray): Boolean = secp256k1.secKeyVerify(il)
fun pubkeyCreateBitcoin(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
fun sum(
first: ByteArray,
second: ByteArray,
): ByteArray = secp256k1.privKeyTweakAdd(first, second)
fun derivePrivateKey(
parent: ExtendedPrivateKey,
index: Long,
@@ -73,17 +52,17 @@ class Bip32SeedDerivation(
val data = arrayOf(0.toByte()).toByteArray() + parent.secretkeybytes + writeInt32BE(index.toInt())
hmac512(parent.chaincode, data)
} else {
val data = pubkeyCreateBitcoin(parent.secretkeybytes) + writeInt32BE(index.toInt())
val data = Secp256k1Instance.compressedPubKeyFor(parent.secretkeybytes) + writeInt32BE(index.toInt())
hmac512(parent.chaincode, data)
}
val il = i.take(32).toByteArray()
val ir = i.takeLast(32).toByteArray()
require(isPrivKeyValid(il)) { "cannot generate child private key: IL is invalid" }
require(Secp256k1Instance.isPrivateKeyValid(il)) { "cannot generate child private key: IL is invalid" }
val key = sum(il, parent.secretkeybytes)
val key = Secp256k1Instance.privateKeyAdd(il, parent.secretkeybytes)
require(isPrivKeyValid(key)) { "cannot generate child private key: resulting private key is invalid" }
require(Secp256k1Instance.isPrivateKeyValid(key)) { "cannot generate child private key: resulting private key is invalid" }
return ExtendedPrivateKey(key, ir)
}
@@ -21,7 +21,7 @@
package com.vitorpamplona.quartz.nip06KeyDerivation
import com.vitorpamplona.quartz.nip49PrivKeyEnc.PBKDF
import com.vitorpamplona.quartz.utils.sha256Hash
import com.vitorpamplona.quartz.utils.sha256
// CODE FROM: https://github.com/ACINQ/bitcoin-kmp/
@@ -79,7 +79,7 @@ object Bip39Mnemonics {
val databits = bits.subList(0, bitlength)
val checksumbits = bits.subList(bitlength, bits.size)
val data = group(databits, 8).map { fromBinary(it) }.map { it.toByte() }.toByteArray()
val check = toBinary(sha256Hash(data)).take(data.size / 4)
val check = toBinary(sha256(data)).take(data.size / 4)
require(check == checksumbits) { "invalid checksum" }
}
@@ -106,7 +106,7 @@ object Bip39Mnemonics {
wordlist: Array<String>,
): List<String> {
require(wordlist.size == 2048) { "invalid word list (size should be 2048)" }
val digits = toBinary(entropy) + toBinary(sha256Hash(entropy)).take(entropy.size / 4)
val digits = toBinary(entropy) + toBinary(sha256(entropy)).take(entropy.size / 4)
return group(digits, 11).map(Bip39Mnemonics::fromBinary).map { wordlist[it] }
}
@@ -20,12 +20,8 @@
*/
package com.vitorpamplona.quartz.nip06KeyDerivation
import fr.acinq.secp256k1.Secp256k1
class Nip06(
val secp256k1: Secp256k1,
) {
val derivation = Bip32SeedDerivation(secp256k1)
class Nip06 {
val derivation = Bip32SeedDerivation()
// m/44'/1237'/<account>'/0/0
private val nip6Base: KeyPath =
@@ -20,15 +20,15 @@
*/
package com.vitorpamplona.quartz.nip17Dm.files.encryption
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.utils.RandomInstance
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
class AESGCM(
val keyBytes: ByteArray = CryptoUtils.random(32),
val nonce: ByteArray = CryptoUtils.random(16),
val keyBytes: ByteArray = RandomInstance.bytes(32),
val nonce: ByteArray = RandomInstance.bytes(16),
) : NostrCipher {
private fun newCipher() = Cipher.getInstance("AES/GCM/NoPadding")
@@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip19Bech32
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
@@ -152,7 +152,7 @@ object Nip19Parser {
fun decodePublicKey(key: String): ByteArray =
when (val parsed = Nip19Parser.uriToRoute(key)?.entity) {
is NSec -> KeyPair(privKey = key.bechToBytes()).pubKey
is NSec -> Nip01.pubKeyCreate(parsed.hex.hexToByteArray())
is NPub -> parsed.hex.hexToByteArray()
is NProfile -> parsed.hex.hexToByteArray()
else -> Hex.decode(key) // crashes on purpose
@@ -179,7 +179,7 @@ fun decodePrivateKeyAsHexOrNull(key: String): HexKey? =
fun decodePublicKeyAsHexOrNull(key: String): HexKey? =
try {
when (val parsed = Nip19Parser.uriToRoute(key)?.entity) {
is NSec -> KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey()
is NSec -> Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey()
is NPub -> parsed.hex
is NProfile -> parsed.hex
is Note -> null
@@ -18,8 +18,11 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
package com.vitorpamplona.quartz.nip44Encryption
import java.security.SecureRandom
fun SecureRandom.nextBytes(size: Int) = ByteArray(size).also { nextBytes(it) }
class EncryptedInfoString(
val ciphertext: String,
val nonce: String,
val v: Int,
val mac: String?,
)
@@ -22,36 +22,19 @@ package com.vitorpamplona.quartz.nip44Encryption
import android.util.Log
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip04Dm.Nip04Encoder
import com.vitorpamplona.quartz.nip04Dm.Nip04Encryption
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
import java.util.Base64
class Nip44(
secp256k1: Secp256k1,
random: SecureRandom,
val nip04: Nip04Encryption,
) {
public val v1 = Nip44v1(secp256k1, random)
public val v2 = Nip44v2(secp256k1, random)
object Nip44 {
val v1 = Nip44v1()
val v2 = Nip44v2()
fun clearCache() {
v1.clearCache()
v2.clearCache()
}
/** NIP 44v2 Utils */
fun getSharedSecret(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = v2.getConversationKey(privateKey, pubKey)
fun computeSharedSecret(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray = v2.computeConversationKey(privateKey, pubKey)
fun encrypt(
msg: String,
privateKey: ByteArray,
@@ -75,14 +58,7 @@ class Nip44(
}
}
class EncryptedInfoString(
val ciphertext: String,
val nonce: String,
val v: Int,
val mac: String?,
)
fun decryptNIP44FromJackson(
private fun decryptNIP44FromJackson(
json: String,
privateKey: ByteArray,
pubKey: ByteArray,
@@ -97,13 +73,13 @@ class Nip44(
}
return when (info.v) {
Nip04Encoder.V -> {
EncryptedInfo.V -> {
val encryptedInfo =
Nip04Encoder(
EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
)
nip04.decrypt(encryptedInfo, privateKey, pubKey)
Nip04.decrypt(encryptedInfo, privateKey, pubKey)
}
Nip44v1.EncryptedInfo.V -> {
@@ -129,7 +105,7 @@ class Nip44(
}
}
fun decryptNIP44FromBase64(
private fun decryptNIP44FromBase64(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
@@ -146,7 +122,7 @@ class Nip44(
}
return when (byteArray[0].toInt()) {
Nip04Encoder.V -> nip04.decrypt(payload, privateKey, pubKey)
EncryptedInfo.V -> Nip04.decrypt(payload, privateKey, pubKey)
Nip44v1.EncryptedInfo.V -> v1.decrypt(payload, privateKey, pubKey)
Nip44v2.EncryptedInfo.V -> v2.decrypt(payload, privateKey, pubKey)
else -> null
@@ -21,22 +21,14 @@
package com.vitorpamplona.quartz.nip44Encryption
import android.util.Log
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
import com.vitorpamplona.quartz.nip44Encryption.crypto.cryptoStreamXChaCha20Xor
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.sha256Hash
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
import com.vitorpamplona.quartz.utils.LibSodiumInstance
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.Secp256k1Instance
import com.vitorpamplona.quartz.utils.sha256
import java.util.Base64
class Nip44v1(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
class Nip44v1 {
private val sharedKeyCache = SharedKeyCache()
private val h02 = Hex.decode("02")
private val libSodium = SodiumAndroid()
fun clearCache() {
sharedKeyCache.clearCache()
@@ -55,15 +47,13 @@ class Nip44v1(
msg: String,
sharedSecret: ByteArray,
): EncryptedInfo {
val nonce = ByteArray(24)
random.nextBytes(nonce)
val nonce = RandomInstance.bytes(24)
val cipher =
cryptoStreamXChaCha20Xor(
libSodium = libSodium,
LibSodiumInstance.cryptoStreamXChaCha20Xor(
messageBytes = msg.toByteArray(),
nonce = nonce,
key = Key.fromBytes(sharedSecret),
key = sharedSecret,
)
return EncryptedInfo(
@@ -102,12 +92,12 @@ class Nip44v1(
encryptedInfo: EncryptedInfo,
sharedSecret: ByteArray,
): String? =
cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = encryptedInfo.ciphertext,
nonce = encryptedInfo.nonce,
key = Key.fromBytes(sharedSecret),
)?.decodeToString()
LibSodiumInstance
.cryptoStreamXChaCha20Xor(
messageBytes = encryptedInfo.ciphertext,
nonce = encryptedInfo.nonce,
key = sharedSecret,
)?.decodeToString()
fun getSharedSecret(
privateKey: ByteArray,
@@ -126,8 +116,8 @@ class Nip44v1(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray =
sha256Hash(
secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33),
sha256(
Secp256k1Instance.pubKeyTweakMulCompact(pubKey, privateKey),
)
class EncryptedInfo(
@@ -21,30 +21,21 @@
package com.vitorpamplona.quartz.nip44Encryption
import android.util.Log
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf
import com.vitorpamplona.quartz.utils.Hex
import fr.acinq.secp256k1.Secp256k1
import com.vitorpamplona.quartz.utils.LibSodiumInstance
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.Secp256k1Instance
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.SecureRandom
import java.util.Base64
import kotlin.math.floor
import kotlin.math.log2
class Nip44v2(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
class Nip44v2 {
private val sharedKeyCache = SharedKeyCache()
private val libSodium = SodiumAndroid()
private val lazySodium = LazySodiumAndroid(libSodium)
private val hkdf = Hkdf()
private val h02 = Hex.decode("02")
private val saltPrefix = "nip44-v2".toByteArray(Charsets.UTF_8)
private val hashLength = 32
@@ -65,8 +56,7 @@ class Nip44v2(
plaintext: String,
conversationKey: ByteArray,
): EncryptedInfo {
val nonce = ByteArray(hashLength)
random.nextBytes(nonce)
val nonce = RandomInstance.bytes(hashLength)
return encryptWithNonce(plaintext, conversationKey, nonce)
}
@@ -78,15 +68,12 @@ class Nip44v2(
val messageKeys = getMessageKeys(conversationKey, nonce)
val padded = pad(plaintext)
val ciphertext = ByteArray(padded.size)
lazySodium.cryptoStreamChaCha20IetfXor(
ciphertext,
padded,
padded.size.toLong(),
messageKeys.chachaNonce,
messageKeys.chachaKey,
)
val ciphertext =
LibSodiumInstance.cryptoStreamChaCha20IetfXor(
padded,
messageKeys.chachaNonce,
messageKeys.chachaKey,
)
val mac = hmacAad(messageKeys.hmacKey, ciphertext, nonce)
@@ -128,16 +115,12 @@ class Nip44v2(
"Invalid Mac: Calculated ${calculatedMac.toHexKey()}, decoded: ${decoded.mac.toHexKey()}"
}
val mLen = decoded.ciphertext.size.toLong()
val padded = ByteArray(decoded.ciphertext.size)
lazySodium.cryptoStreamChaCha20IetfXor(
padded,
decoded.ciphertext,
mLen,
messageKey.chachaNonce,
messageKey.chachaKey,
)
val padded =
LibSodiumInstance.cryptoStreamChaCha20IetfXor(
decoded.ciphertext,
messageKey.chachaNonce,
messageKey.chachaKey,
)
return unpad(padded)
}
@@ -241,7 +224,7 @@ class Nip44v2(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
val sharedX = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
val sharedX = Secp256k1Instance.pubKeyTweakMulCompact(pubKey, privateKey)
return hkdf.extract(sharedX, saltPrefix)
}
@@ -1,103 +0,0 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip44Encryption.crypto
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
/**
* I initially extended these methods from the Sodium and SodiumAndroid classes But JNI doesn't like
* it. There is some native method overriding bug when using Kotlin extensions
*/
fun cryptoStreamXchacha20XorIc(
libSodium: SodiumAndroid,
cipher: ByteArray,
message: ByteArray,
messageLen: Long,
nonce: ByteArray,
ic: Long,
key: ByteArray,
): Int {
/**
* C++ Code:
*
* unsigned char k2[crypto_core_hchacha20_OUTPUTBYTES]; crypto_core_hchacha20(k2, n, k, NULL);
* return crypto_stream_chacha20_xor_ic( c, m, mlen, n + crypto_core_hchacha20_INPUTBYTES, ic,
* k2);
*/
val k2 = ByteArray(32)
val nonceChaCha = nonce.drop(16).toByteArray()
assert(nonceChaCha.size == 8)
libSodium.crypto_core_hchacha20(k2, nonce, key, null)
return libSodium.crypto_stream_chacha20_xor_ic(
cipher,
message,
messageLen,
nonceChaCha,
ic,
k2,
)
}
fun cryptoStreamXchacha20Xor(
libSodium: SodiumAndroid,
cipher: ByteArray,
message: ByteArray,
messageLen: Long,
nonce: ByteArray,
key: ByteArray,
): Int = cryptoStreamXchacha20XorIc(libSodium, cipher, message, messageLen, nonce, 0, key)
fun cryptoStreamXChaCha20Xor(
libSodium: SodiumAndroid,
cipher: ByteArray,
message: ByteArray,
messageLen: Long,
nonce: ByteArray,
key: ByteArray,
): Boolean {
require(!(messageLen < 0 || messageLen > message.size)) {
"messageLen out of bounds: $messageLen"
}
return cryptoStreamXchacha20Xor(
libSodium,
cipher,
message,
messageLen,
nonce,
key,
) == 0
}
fun cryptoStreamXChaCha20Xor(
libSodium: SodiumAndroid,
messageBytes: ByteArray,
nonce: ByteArray,
key: Key,
): ByteArray? {
val mLen = messageBytes.size
val cipher = ByteArray(mLen)
val successful =
cryptoStreamXChaCha20Xor(libSodium, cipher, messageBytes, mLen.toLong(), nonce, key.asBytes)
return if (successful) cipher else null
}
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip49PrivKeyEnc
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
fun ByteArray.toNCryptSec() = Bech32.encodeBytes(hrp = "ncryptsec", this, Bech32.Encoding.Bech32)
@@ -21,24 +21,15 @@
package com.vitorpamplona.quartz.nip49PrivKeyEnc
import android.util.Log
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
import com.vitorpamplona.quartz.utils.LibSodiumInstance
import com.vitorpamplona.quartz.utils.RandomInstance
import java.text.Normalizer
class Nip49(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val libSodium = SodiumAndroid()
private val lazySodium = LazySodiumAndroid(libSodium)
class Nip49 {
fun decrypt(
nCryptSec: String,
password: String,
@@ -56,14 +47,11 @@ class Nip49(
val key = SCrypt.scrypt(normalizedPassword, encryptedInfo.salt, n, 8, 1, 32)
val m = ByteArray(32)
lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
LibSodiumInstance.cryptoAeadXChaCha20Poly1305IetfDecrypt(
m,
longArrayOf(32L),
key,
encryptedInfo.encryptedKey,
encryptedInfo.encryptedKey.size.toLong(),
byteArrayOf(encryptedInfo.keySecurity),
1,
encryptedInfo.nonce,
key,
)
@@ -87,11 +75,8 @@ class Nip49(
ksb: Byte,
): String {
check(secretKey.size == 32) { "invalid secret key" }
val salt = ByteArray(16)
random.nextBytes(salt)
val nonce = ByteArray(24)
random.nextBytes(nonce)
val salt = RandomInstance.bytes(16)
val nonce = RandomInstance.bytes(24)
val normalizedPassword = Normalizer.normalize(password, Normalizer.Form.NFKC).toByteArray(Charsets.UTF_8)
val n = Math.pow(2.0, logn.toDouble()).toInt()
@@ -102,13 +87,10 @@ class Nip49(
// byte[] m, long mLen,
// byte[] ad, long adLen,
// byte[] nSec, byte[] nPub, byte[] k
lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
LibSodiumInstance.cryptoAeadXChaCha20Poly1305IetfEncrypt(
ciphertext,
longArrayOf(48),
secretKey,
secretKey.size.toLong(),
byteArrayOf(ksb),
1,
key,
nonce,
key,
@@ -166,13 +148,8 @@ class Nip49(
// ln(n.toDouble()).toInt().toByte(),
fun encodePayload(): String =
Bech32.encodeBytes(
hrp = "ncryptsec",
byteArrayOf(
version,
logn,
) + salt + nonce + keySecurity + encryptedKey,
Bech32.Encoding.Bech32,
)
(
byteArrayOf(version, logn) + salt + nonce + keySecurity + encryptedKey
).toNCryptSec()
}
}
@@ -21,7 +21,6 @@
package com.vitorpamplona.quartz.nip55AndroidSigner
import android.util.Log
import com.goterl.lazysodium.BuildConfig
import com.vitorpamplona.quartz.EventFactory
import com.vitorpamplona.quartz.nip01Core.EventHasher
import com.vitorpamplona.quartz.nip01Core.HexKey
@@ -87,10 +86,6 @@ class NostrSignerExternal(
toPublicKey: HexKey,
onReady: (String) -> Unit,
) {
if (BuildConfig.DEBUG) {
Log.d("NostrExternalSigner", "Encrypt NIP04 Event: $decryptedContent")
}
launcher.encrypt(
decryptedContent,
toPublicKey,
@@ -104,10 +99,6 @@ class NostrSignerExternal(
fromPublicKey: HexKey,
onReady: (String) -> Unit,
) {
if (BuildConfig.DEBUG) {
Log.d("NostrExternalSigner", "Decrypt NIP04 Event: $encryptedContent")
}
launcher.decrypt(
encryptedContent,
fromPublicKey,
@@ -121,10 +112,6 @@ class NostrSignerExternal(
toPublicKey: HexKey,
onReady: (String) -> Unit,
) {
if (BuildConfig.DEBUG) {
Log.d("NostrExternalSigner", "Encrypt NIP44 Event: $decryptedContent")
}
launcher.encrypt(
decryptedContent,
toPublicKey,
@@ -138,10 +125,6 @@ class NostrSignerExternal(
fromPublicKey: HexKey,
onReady: (String) -> Unit,
) {
if (BuildConfig.DEBUG) {
Log.d("NostrExternalSigner", "Decrypt NIP44 Event: $encryptedContent")
}
launcher.decrypt(
encryptedContent,
fromPublicKey,
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip55AndroidSigner
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.sha256
fun signString(
message: String,
privKey: ByteArray,
nonce: ByteArray = RandomInstance.bytes(32),
): ByteArray = Nip01.sign(sha256(message.toByteArray()), privKey, nonce)
@@ -20,8 +20,9 @@
*/
package com.vitorpamplona.quartz.nip57Zaps
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
import com.vitorpamplona.quartz.utils.sha256
import java.nio.charset.Charset
import java.security.SecureRandom
import javax.crypto.BadPaddingException
@@ -38,7 +39,7 @@ class PrivateZapEncryption {
): ByteArray {
val str = privkey + id + createdAt.toString()
val strbyte = str.toByteArray(Charset.forName("utf-8"))
return CryptoUtils.sha256(strbyte)
return sha256(strbyte)
}
fun encryptPrivateZapMessage(
@@ -46,7 +47,7 @@ class PrivateZapEncryption {
privkey: ByteArray,
pubkey: ByteArray,
): String {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey)
val sharedSecret = Nip04.getSharedSecret(privkey, pubkey)
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
@@ -70,7 +71,7 @@ class PrivateZapEncryption {
privkey: ByteArray,
pubkey: ByteArray,
): String {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey)
val sharedSecret = Nip04.getSharedSecret(privkey, pubkey)
if (sharedSecret.size != 16 && sharedSecret.size != 32) {
throw IllegalArgumentException("Invalid shared secret size")
}
@@ -21,8 +21,8 @@
package com.vitorpamplona.quartz.nip57Zaps
import android.util.Log
import com.vitorpamplona.quartz.CryptoUtils
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip01Core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.toHexKey
@@ -105,7 +105,7 @@ class PrivateZapRequestBuilder {
try {
if (altPrivateKeyToUse != null && altPubkeyToUse != null) {
val altPubKeyFromPrivate = CryptoUtils.pubkeyCreate(altPrivateKeyToUse).toHexKey()
val altPubKeyFromPrivate = Nip01.pubKeyCreate(altPrivateKeyToUse).toHexKey()
if (altPubKeyFromPrivate == event.pubKey) {
val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse)
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip98HttpAuth.tags
import com.vitorpamplona.quartz.nip01Core.HexKey
import com.vitorpamplona.quartz.nip01Core.toHexKey
import com.vitorpamplona.quartz.utils.sha256Hash
import com.vitorpamplona.quartz.utils.sha256
class PayloadHashTag {
companion object {
@@ -39,6 +39,6 @@ class PayloadHashTag {
fun assemble(hash: HexKey) = arrayOf(TAG_NAME, hash)
@JvmStatic
fun assemble(payload: ByteArray) = assemble(sha256Hash(payload).toHexKey())
fun assemble(payload: ByteArray) = assemble(sha256(payload).toHexKey())
}
}
@@ -20,8 +20,6 @@
*/
package com.vitorpamplona.quartz.utils
import com.vitorpamplona.quartz.R
public fun arrayOfNotNull(vararg elements: String?) = removeTrailingNullsAndEmptyOthers(*elements)
public fun removeTrailingNullsAndEmptyOthers(vararg elements: String?): Array<String> {
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
fun hmac512(
key: ByteArray,
data: ByteArray,
): ByteArray {
val mac = Mac.getInstance("HmacSHA512")
mac.init(SecretKeySpec(key, "HmacSHA512"))
return mac.doFinal(data)
}
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
object LibSodiumInstance {
private val libSodium = SodiumAndroid()
private val lazySodium = LazySodiumAndroid(libSodium)
fun cryptoAeadXChaCha20Poly1305IetfDecrypt(
message: ByteArray,
nSec: ByteArray,
ciphertext: ByteArray,
ad: ByteArray,
nPub: ByteArray,
k: ByteArray,
): Boolean =
lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
message,
longArrayOf(message.size.toLong()),
nSec,
ciphertext,
ciphertext.size.toLong(),
ad,
ad.size.toLong(),
nPub,
k,
)
fun cryptoAeadXChaCha20Poly1305IetfEncrypt(
ciphertext: ByteArray,
message: ByteArray,
ad: ByteArray,
nSec: ByteArray,
nPub: ByteArray,
k: ByteArray,
): Boolean =
lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
ciphertext,
longArrayOf(ciphertext.size.toLong()),
message,
message.size.toLong(),
ad,
ad.size.toLong(),
nSec,
nPub,
k,
)
fun cryptoStreamChaCha20IetfXor(
message: ByteArray,
nonce: ByteArray?,
key: ByteArray?,
): ByteArray {
val ciphertext = ByteArray(message.size)
lazySodium.cryptoStreamChaCha20IetfXor(ciphertext, message, message.size.toLong(), nonce, key)
return ciphertext
}
// This function wasn't available in the bindings library. I had to move them here from C
fun cryptoStreamXChaCha20Xor(
messageBytes: ByteArray,
nonce: ByteArray,
key: ByteArray,
): ByteArray? {
val cipher = ByteArray(messageBytes.size)
val k2 = ByteArray(32)
val nonceChaCha = nonce.drop(16).toByteArray()
assert(nonceChaCha.size == 8)
libSodium.crypto_core_hchacha20(k2, nonce, key, null)
val resultCode =
libSodium.crypto_stream_chacha20_xor_ic(
cipher,
messageBytes,
messageBytes.size.toLong(),
nonceChaCha,
0,
k2,
)
return if (resultCode == 0) cipher else null
}
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
import java.security.SecureRandom
object RandomInstance {
private val randomizer = SecureRandom()
fun int(bound: Int) = randomizer.nextInt(bound)
fun bytes(size: Int) = ByteArray(size).also { randomizer.nextBytes(it) }
}
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
import fr.acinq.secp256k1.Secp256k1
object Secp256k1Instance {
private val h02 = Hex.decode("02")
private val secp256k1 = Secp256k1.get()
fun compressedPubKeyFor(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
fun isPrivateKeyValid(il: ByteArray): Boolean = secp256k1.secKeyVerify(il)
fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
nonce: ByteArray? = RandomInstance.bytes(32),
): ByteArray = secp256k1.signSchnorr(data, privKey, nonce)
fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
): ByteArray = secp256k1.signSchnorr(data, privKey, null)
fun verifySchnorr(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean = secp256k1.verifySchnorr(signature, hash, pubKey)
fun privateKeyAdd(
first: ByteArray,
second: ByteArray,
): ByteArray = secp256k1.privKeyTweakAdd(first, second)
fun pubKeyTweakMulCompact(
pubKey: ByteArray,
privateKey: ByteArray,
): ByteArray = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.utils
import java.security.MessageDigest
fun sha256Hash(data: ByteArray): ByteArray {
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
}
@@ -20,8 +20,6 @@
*/
package com.vitorpamplona.quartz.utils
import com.vitorpamplona.quartz.CryptoUtils
object TimeUtils {
const val ONE_MINUTE = 60
const val FIVE_MINUTES = 5 * ONE_MINUTE
@@ -57,5 +55,5 @@ object TimeUtils {
fun oneMonthAgo() = now() - ONE_MONTH
fun randomWithTwoDays() = now() - CryptoUtils.randomInt(twoDays())
fun randomWithTwoDays() = now() - RandomInstance.int(twoDays())
}