rearranges the crypto package into separate nips and reduces the amount of circular dependencies.

This commit is contained in:
Vitor Pamplona
2024-06-25 12:13:17 -04:00
parent 79ace7f18c
commit a8a2bda9af
34 changed files with 468 additions and 461 deletions
@@ -20,35 +20,32 @@
*/
package com.vitorpamplona.quartz.crypto
import android.util.Log
import com.vitorpamplona.quartz.crypto.nip01.Nip01
import com.vitorpamplona.quartz.crypto.nip04.Nip04
import com.vitorpamplona.quartz.crypto.nip06.Nip06
import com.vitorpamplona.quartz.crypto.nip44.Nip44
import com.vitorpamplona.quartz.crypto.nip44.Nip44v2
import com.vitorpamplona.quartz.crypto.nip49.Nip49
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.Event
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
object CryptoUtils {
private val secp256k1 = Secp256k1.get()
private val random = SecureRandom()
private val nip04 = Nip04(secp256k1, random)
private val nip44v1 = Nip44v1(secp256k1, random)
private val nip44v2 = Nip44v2(secp256k1, random)
private val nip49 = Nip49(secp256k1, random)
public val nip01 = Nip01(secp256k1, random)
public val nip06 = Nip06(secp256k1)
public val nip04 = Nip04(secp256k1, random)
public val nip44 = Nip44(secp256k1, random, nip04)
public val nip49 = Nip49(secp256k1, random)
fun clearCache() {
nip04.clearCache()
nip44v1.clearCache()
nip44v2.clearCache()
nip44.clearCache()
}
fun randomInt(bound: Int): Int {
return random.nextInt(bound)
}
/** Provides a 32B "private key" aka random number */
fun privkeyCreate() = random(32)
fun randomInt(bound: Int): Int = random.nextInt(bound)
fun random(size: Int): ByteArray {
val bytes = ByteArray(size)
@@ -56,39 +53,32 @@ object CryptoUtils {
return bytes
}
fun pubkeyCreateBitcoin(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
/** Provides a 32B "private key" aka random number */
fun privkeyCreate() = nip01.privkeyCreate()
fun pubkeyCreate(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)).copyOfRange(1, 33)
fun isPrivKeyValid(il: ByteArray): Boolean {
return secp256k1.secKeyVerify(il)
}
fun pubkeyCreate(privKey: ByteArray) = nip01.pubkeyCreate(privKey)
fun signString(
message: String,
privKey: ByteArray,
auxrand32: ByteArray = random(32),
): ByteArray {
return sign(sha256(message.toByteArray()), privKey, auxrand32)
}
): ByteArray = nip01.signString(message, privKey, auxrand32)
fun sign(
data: ByteArray,
privKey: ByteArray,
auxrand32: ByteArray? = null,
): ByteArray = secp256k1.signSchnorr(data, privKey, auxrand32)
): ByteArray = nip01.sign(data, privKey, auxrand32)
fun verifySignature(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean {
return secp256k1.verifySchnorr(signature, hash, pubKey)
}
): Boolean = nip01.verify(signature, hash, pubKey)
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
return nip01.sha256(data)
}
/** NIP 04 Utils */
@@ -96,189 +86,84 @@ object CryptoUtils {
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
return nip04.encrypt(msg, privateKey, pubKey)
}
): String = nip04.encrypt(msg, privateKey, pubKey)
fun encryptNIP04(
msg: String,
sharedSecret: ByteArray,
): Nip04.EncryptedInfo {
return nip04.encrypt(msg, sharedSecret)
}
): Nip04.EncryptedInfo = nip04.encrypt(msg, sharedSecret)
fun decryptNIP04(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
return nip04.decrypt(msg, privateKey, pubKey)
}
): String = nip04.decrypt(msg, privateKey, pubKey)
fun decryptNIP04(
encryptedInfo: Nip04.EncryptedInfo,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
return nip04.decrypt(encryptedInfo, privateKey, pubKey)
}
): String = nip04.decrypt(encryptedInfo, privateKey, pubKey)
fun decryptNIP04(
msg: String,
sharedSecret: ByteArray,
): String {
return nip04.decrypt(msg, sharedSecret)
}
): String = nip04.decrypt(msg, sharedSecret)
private fun decryptNIP04(
cipher: String,
nonce: String,
sharedSecret: ByteArray,
): String {
return nip04.decrypt(cipher, nonce, sharedSecret)
}
): String = nip04.decrypt(cipher, nonce, sharedSecret)
private fun decryptNIP04(
encryptedMsg: ByteArray,
iv: ByteArray,
sharedSecret: ByteArray,
): String {
return nip04.decrypt(encryptedMsg, iv, sharedSecret)
}
): String = nip04.decrypt(encryptedMsg, iv, sharedSecret)
fun getSharedSecretNIP04(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip04.getSharedSecret(privateKey, pubKey)
}
): ByteArray = nip04.getSharedSecret(privateKey, pubKey)
fun computeSharedSecretNIP04(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip04.computeSharedSecret(privateKey, pubKey)
}
): ByteArray = nip04.computeSharedSecret(privateKey, pubKey)
/** NIP 44v1 Utils */
fun encryptNIP44v1(
/** 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,
): Nip44v1.EncryptedInfo {
return nip44v1.encrypt(msg, privateKey, pubKey)
}
fun encryptNIP44v1(
msg: String,
sharedSecret: ByteArray,
): Nip44v1.EncryptedInfo {
return nip44v1.encrypt(msg, sharedSecret)
}
fun decryptNIP44v1(
encryptedInfo: Nip44v1.EncryptedInfo,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return nip44v1.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v1(
encryptedInfo: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return nip44v1.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v1(
encryptedInfo: Nip44v1.EncryptedInfo,
sharedSecret: ByteArray,
): String? {
return nip44v1.decrypt(encryptedInfo, sharedSecret)
}
fun getSharedSecretNIP44v1(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip44v1.getSharedSecret(privateKey, pubKey)
}
fun computeSharedSecretNIP44v1(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip44v1.computeSharedSecret(privateKey, pubKey)
}
/** NIP 44v2 Utils */
fun encryptNIP44v2(
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): Nip44v2.EncryptedInfo {
return nip44v2.encrypt(msg, privateKey, pubKey)
}
fun encryptNIP44v2(
msg: String,
sharedSecret: ByteArray,
): Nip44v2.EncryptedInfo {
return nip44v2.encrypt(msg, sharedSecret)
}
fun decryptNIP44v2(
encryptedInfo: Nip44v2.EncryptedInfo,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return nip44v2.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v2(
encryptedInfo: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return nip44v2.decrypt(encryptedInfo, privateKey, pubKey)
}
fun decryptNIP44v2(
encryptedInfo: Nip44v2.EncryptedInfo,
sharedSecret: ByteArray,
): String? {
return nip44v2.decrypt(encryptedInfo, sharedSecret)
}
fun getSharedSecretNIP44v2(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip44v2.getConversationKey(privateKey, pubKey)
}
fun computeSharedSecretNIP44v2(
privateKey: ByteArray,
pubKey: ByteArray,
): ByteArray {
return nip44v2.computeConversationKey(privateKey, pubKey)
}
): Nip44v2.EncryptedInfo = nip44.encrypt(msg, privateKey, pubKey)
fun decryptNIP44(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
if (payload.isEmpty()) return null
return if (payload[0] == '{') {
decryptNIP44FromJackson(payload, privateKey, pubKey)
} else {
decryptNIP44FromBase64(payload, privateKey, pubKey)
}
}
): String? = nip44.decrypt(payload, privateKey, pubKey)
/** NIP 49 Utils */
fun decryptNIP49(
payload: String,
password: String,
@@ -290,87 +175,5 @@ object CryptoUtils {
fun encryptNIP49(
key: HexKey,
password: String,
): String? {
return nip49.encrypt(key, password, 16, Nip49.EncryptedInfo.CLIENT_DOES_NOT_TRACK)
}
class EncryptedInfoString(
val ciphertext: String,
val nonce: String,
val v: Int,
val mac: String?,
)
fun decryptNIP44FromJackson(
json: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return try {
val info = Event.mapper.readValue(json, EncryptedInfoString::class.java)
when (info.v) {
Nip04.EncryptedInfo.V -> {
val encryptedInfo =
Nip04.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
)
decryptNIP04(encryptedInfo, privateKey, pubKey)
}
Nip44v1.EncryptedInfo.V -> {
val encryptedInfo =
Nip44v1.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
)
decryptNIP44v1(encryptedInfo, privateKey, pubKey)
}
Nip44v2.EncryptedInfo.V -> {
val encryptedInfo =
Nip44v2.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
mac = Base64.getDecoder().decode(info.mac),
)
decryptNIP44v2(encryptedInfo, privateKey, pubKey)
}
else -> null
}
} catch (e: Exception) {
Log.e("CryptoUtils", "Could not identify the version for NIP44 payload $json")
e.printStackTrace()
null
}
}
fun decryptNIP44FromBase64(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
if (payload.isEmpty()) return null
return try {
val byteArray = Base64.getDecoder().decode(payload)
when (byteArray[0].toInt()) {
Nip04.EncryptedInfo.V -> decryptNIP04(payload, privateKey, pubKey)
Nip44v1.EncryptedInfo.V -> decryptNIP44v1(payload, privateKey, pubKey)
Nip44v2.EncryptedInfo.V -> decryptNIP44v2(payload, privateKey, pubKey)
else -> null
}
} catch (e: Exception) {
Log.e("CryptoUtils", "Could not identify the version for NIP44 payload $payload")
e.printStackTrace()
null
}
}
fun sum(
first: ByteArray,
second: ByteArray,
): ByteArray {
return secp256k1.privKeyTweakAdd(first, second)
}
): String = nip49.encrypt(key, password)
}
@@ -0,0 +1,60 @@
/**
* 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.crypto.nip01
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.CryptoUtils.random
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
class Nip01(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
/** Provides a 32B "private key" aka random number */
fun privkeyCreate() = random(32)
fun pubkeyCreate(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)).copyOfRange(1, 33)
fun sign(
data: ByteArray,
privKey: ByteArray,
auxrand32: ByteArray? = null,
): ByteArray = secp256k1.signSchnorr(data, privKey, auxrand32)
fun verify(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean = secp256k1.verifySchnorr(signature, hash, pubKey)
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
}
fun signString(
message: String,
privKey: ByteArray,
auxrand32: ByteArray = random(32),
): ByteArray = sign(CryptoUtils.sha256(message.toByteArray()), privKey, auxrand32)
}
@@ -18,9 +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.crypto
package com.vitorpamplona.quartz.crypto.nip04
import android.util.Log
import com.vitorpamplona.quartz.crypto.SharedKeyCache
import com.vitorpamplona.quartz.crypto.nip44.Nip44v1
import com.vitorpamplona.quartz.encoders.Hex
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
@@ -29,7 +31,10 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) {
class Nip04(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val sharedKeyCache = SharedKeyCache()
private val h02 = Hex.decode("02")
@@ -41,9 +46,7 @@ class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) {
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String {
return encrypt(msg, getSharedSecret(privateKey, pubKey)).encodeToNIP04()
}
): String = encrypt(msg, getSharedSecret(privateKey, pubKey)).encodeToNIP04()
fun encrypt(
msg: String,
@@ -146,8 +149,8 @@ class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) {
}
}
fun decodeFromNIP04(payload: String): EncryptedInfo? {
return try {
fun decodeFromNIP04(payload: String): EncryptedInfo? =
try {
val parts = payload.split("?iv=")
EncryptedInfo(
ciphertext = Base64.getDecoder().decode(parts[0]),
@@ -157,15 +160,14 @@ class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) {
Log.w("NIP04", "Unable to Parse encrypted payload: $payload")
null
}
}
}
fun encodePayload(): String {
return Base64.getEncoder()
fun encodePayload(): String =
Base64
.getEncoder()
.encodeToString(
byteArrayOf(V.toByte()) + nonce + ciphertext,
)
}
fun encodeToNIP04(): String {
val nonce = Base64.getEncoder().encodeToString(nonce)
@@ -20,14 +20,16 @@
*/
package com.vitorpamplona.quartz.crypto.nip06
import com.vitorpamplona.quartz.crypto.CryptoUtils
import fr.acinq.secp256k1.Secp256k1
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
/*
Simplified from: https://github.com/ACINQ/bitcoin-kmp/
*/
object Bip32SeedDerivation {
class Bip32SeedDerivation(
val secp256k1: Secp256k1,
) {
class ExtendedPrivateKey(
val secretkeybytes: ByteArray,
val chaincode: ByteArray,
@@ -53,6 +55,15 @@ object Bip32SeedDerivation {
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,
@@ -62,17 +73,17 @@ object Bip32SeedDerivation {
val data = arrayOf(0.toByte()).toByteArray() + parent.secretkeybytes + writeInt32BE(index.toInt())
hmac512(parent.chaincode, data)
} else {
val data = CryptoUtils.pubkeyCreateBitcoin(parent.secretkeybytes) + writeInt32BE(index.toInt())
val data = pubkeyCreateBitcoin(parent.secretkeybytes) + writeInt32BE(index.toInt())
hmac512(parent.chaincode, data)
}
val il = i.take(32).toByteArray()
val ir = i.takeLast(32).toByteArray()
require(CryptoUtils.isPrivKeyValid(il)) { "cannot generate child private key: IL is invalid" }
require(isPrivKeyValid(il)) { "cannot generate child private key: IL is invalid" }
val key = CryptoUtils.sum(il, parent.secretkeybytes)
val key = sum(il, parent.secretkeybytes)
require(CryptoUtils.isPrivKeyValid(key)) { "cannot generate child private key: resulting private key is invalid" }
require(isPrivKeyValid(key)) { "cannot generate child private key: resulting private key is invalid" }
return ExtendedPrivateKey(key, ir)
}
@@ -94,7 +105,7 @@ object Bip32SeedDerivation {
fun derivePrivateKey(
parent: ExtendedPrivateKey,
chain: List<Long>,
): ExtendedPrivateKey = chain.fold(parent, Bip32SeedDerivation::derivePrivateKey)
): ExtendedPrivateKey = chain.fold(parent, this::derivePrivateKey)
fun derivePrivateKey(
parent: ExtendedPrivateKey,
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.quartz.crypto.nip06
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.PBKDF
import com.vitorpamplona.quartz.crypto.nip49.PBKDF
import java.security.MessageDigest
// CODE FROM: https://github.com/ACINQ/bitcoin-kmp/
@@ -37,6 +37,11 @@ object Bip39Mnemonics {
return zeroes + digits
}
fun sha256(data: ByteArray): ByteArray {
// Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data)
}
private fun toBinary(x: ByteArray): List<Boolean> = x.map(Bip39Mnemonics::toBinary).flatten()
private fun fromBinary(bin: List<Boolean>): Int = bin.fold(0) { acc, flag -> if (flag) 2 * acc + 1 else 2 * acc }
@@ -45,13 +50,12 @@ object Bip39Mnemonics {
items: List<Boolean>,
size: Int,
acc: List<List<Boolean>> = emptyList(),
): List<List<Boolean>> {
return when {
): List<List<Boolean>> =
when {
items.isEmpty() -> acc
items.size < size -> acc + listOf(items)
else -> group(items.drop(size), size, acc + listOf(items.take(size)))
}
}
/**
* @param mnemonics list of mnemonic words
@@ -80,20 +84,19 @@ 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(CryptoUtils.sha256(data)).take(data.size / 4)
val check = toBinary(sha256(data)).take(data.size / 4)
require(check == checksumbits) { "invalid checksum" }
}
fun validate(mnemonics: String): Unit = validate(mnemonics.split(" "))
fun isValid(mnemonics: String): Boolean {
return try {
fun isValid(mnemonics: String): Boolean =
try {
validate(mnemonics)
true
} catch (e: Exception) {
false
}
}
/**
* BIP39 entropy encoding
@@ -108,7 +111,7 @@ object Bip39Mnemonics {
wordlist: Array<String>,
): List<String> {
require(wordlist.size == 2048) { "invalid word list (size should be 2048)" }
val digits = toBinary(entropy) + toBinary(CryptoUtils.sha256(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,29 +20,31 @@
*/
package com.vitorpamplona.quartz.crypto.nip06
class Nip06 {
import fr.acinq.secp256k1.Secp256k1
class Nip06(
val secp256k1: Secp256k1,
) {
val derivation = Bip32SeedDerivation(secp256k1)
// m/44'/1237'/<account>'/0/0
private val nip6Base: KeyPath =
KeyPath("")
.derive(Hardener.hardened(44L))
.derive(Hardener.hardened(1237L))
private fun nip6Path(account: Long): KeyPath {
return nip6Base.derive(Hardener.hardened(account))
private fun nip6Path(account: Long): KeyPath =
nip6Base
.derive(Hardener.hardened(account))
.derive(0L)
.derive(0L)
}
fun isValidMnemonic(mnemonic: String): Boolean {
return Bip39Mnemonics.isValid(mnemonic)
}
fun isValidMnemonic(mnemonic: String): Boolean = Bip39Mnemonics.isValid(mnemonic)
fun privateKeyFromSeed(
seed: ByteArray,
account: Long = 0,
): ByteArray {
return Bip32SeedDerivation.derivePrivateKey(Bip32SeedDerivation.generate(seed), nip6Path(account))
}
): ByteArray = derivation.derivePrivateKey(derivation.generate(seed), nip6Path(account))
fun privateKeyFromMnemonic(
mnemonic: String,
@@ -18,13 +18,16 @@
* 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.crypto
package com.vitorpamplona.quartz.crypto.nip44
import java.nio.ByteBuffer
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class Hkdf(val algorithm: String = "HmacSHA256", val hashLen: Int = 32) {
class Hkdf(
val algorithm: String = "HmacSHA256",
val hashLen: Int = 32,
) {
fun extract(
key: ByteArray,
salt: ByteArray,
@@ -0,0 +1,148 @@
/**
* 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.crypto.nip44
import android.util.Log
import com.vitorpamplona.quartz.crypto.nip04.Nip04
import com.vitorpamplona.quartz.events.Event
import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
import java.util.Base64
class Nip44(
secp256k1: Secp256k1,
random: SecureRandom,
val nip04: Nip04,
) {
public val v1 = Nip44v1(secp256k1, random)
public val v2 = Nip44v2(secp256k1, random)
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,
pubKey: ByteArray,
): Nip44v2.EncryptedInfo {
// current version should be used.
return v2.encrypt(msg, privateKey, pubKey)
}
// Always decrypt from any version/any encoding
fun decrypt(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
if (payload.isEmpty()) return null
return if (payload[0] == '{') {
decryptNIP44FromJackson(payload, privateKey, pubKey)
} else {
decryptNIP44FromBase64(payload, privateKey, pubKey)
}
}
class EncryptedInfoString(
val ciphertext: String,
val nonce: String,
val v: Int,
val mac: String?,
)
fun decryptNIP44FromJackson(
json: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? =
try {
val info = Event.mapper.readValue(json, EncryptedInfoString::class.java)
when (info.v) {
Nip04.EncryptedInfo.V -> {
val encryptedInfo =
Nip04.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
)
nip04.decrypt(encryptedInfo, privateKey, pubKey)
}
Nip44v1.EncryptedInfo.V -> {
val encryptedInfo =
Nip44v1.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
)
v1.decrypt(encryptedInfo, privateKey, pubKey)
}
Nip44v2.EncryptedInfo.V -> {
val encryptedInfo =
Nip44v2.EncryptedInfo(
ciphertext = Base64.getDecoder().decode(info.ciphertext),
nonce = Base64.getDecoder().decode(info.nonce),
mac = Base64.getDecoder().decode(info.mac),
)
v2.decrypt(encryptedInfo, privateKey, pubKey)
}
else -> null
}
} catch (e: Exception) {
Log.e("CryptoUtils", "Could not identify the version for NIP44 payload $json")
e.printStackTrace()
null
}
fun decryptNIP44FromBase64(
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
if (payload.isEmpty()) return null
return try {
val byteArray = Base64.getDecoder().decode(payload)
when (byteArray[0].toInt()) {
Nip04.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
}
} catch (e: Exception) {
Log.e("CryptoUtils", "Could not identify the version for NIP44 payload $payload")
e.printStackTrace()
null
}
}
}
@@ -18,18 +18,22 @@
* 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.crypto
package com.vitorpamplona.quartz.crypto.nip44
import android.util.Log
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
import com.vitorpamplona.quartz.crypto.SharedKeyCache
import com.vitorpamplona.quartz.encoders.Hex
import fr.acinq.secp256k1.Secp256k1
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) {
class Nip44v1(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val sharedKeyCache = SharedKeyCache()
private val h02 = Hex.decode("02")
private val libSodium = SodiumAndroid()
@@ -97,15 +101,13 @@ class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) {
fun decrypt(
encryptedInfo: EncryptedInfo,
sharedSecret: ByteArray,
): String? {
return cryptoStreamXChaCha20Xor(
): String? =
cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = encryptedInfo.ciphertext,
nonce = encryptedInfo.nonce,
key = Key.fromBytes(sharedSecret),
)
?.decodeToString()
}
)?.decodeToString()
fun getSharedSecret(
privateKey: ByteArray,
@@ -155,11 +157,11 @@ class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) {
}
}
fun encodePayload(): String {
return Base64.getEncoder()
fun encodePayload(): String =
Base64
.getEncoder()
.encodeToString(
byteArrayOf(V.toByte()) + nonce + ciphertext,
)
}
}
}
@@ -18,11 +18,12 @@
* 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.crypto
package com.vitorpamplona.quartz.crypto.nip44
import android.util.Log
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.vitorpamplona.quartz.crypto.SharedKeyCache
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toHexKey
import fr.acinq.secp256k1.Secp256k1
@@ -33,7 +34,10 @@ import java.util.Base64
import kotlin.math.floor
import kotlin.math.log2
class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
class Nip44v2(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val sharedKeyCache = SharedKeyCache()
private val libSodium = SodiumAndroid()
@@ -55,9 +59,7 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
msg: String,
privateKey: ByteArray,
pubKey: ByteArray,
): EncryptedInfo {
return encrypt(msg, getConversationKey(privateKey, pubKey))
}
): EncryptedInfo = encrypt(msg, getConversationKey(privateKey, pubKey))
fun encrypt(
plaintext: String,
@@ -99,17 +101,13 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
payload: String,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return decrypt(payload, getConversationKey(privateKey, pubKey))
}
): String? = decrypt(payload, getConversationKey(privateKey, pubKey))
fun decrypt(
decoded: EncryptedInfo,
privateKey: ByteArray,
pubKey: ByteArray,
): String? {
return decrypt(decoded, getConversationKey(privateKey, pubKey))
}
): String? = decrypt(decoded, getConversationKey(privateKey, pubKey))
fun decrypt(
payload: String,
@@ -173,7 +171,11 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
check(unpaddedLen <= maxPlaintextSize) { "Message is too long ($unpaddedLen): $plaintext" }
val prefix =
ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort(unpaddedLen.toShort()).array()
ByteBuffer
.allocate(2)
.order(ByteOrder.BIG_ENDIAN)
.putShort(unpaddedLen.toShort())
.array()
val suffix = ByteArray(calcPaddedLen(unpaddedLen) - unpaddedLen)
return ByteBuffer.wrap(prefix + unpadded + suffix).array()
}
@@ -182,13 +184,12 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
byte1: Byte,
byte2: Byte,
bigEndian: Boolean,
): Int {
return if (bigEndian) {
): Int =
if (bigEndian) {
(byte1.toInt() and 0xFF shl 8 or (byte2.toInt() and 0xFF))
} else {
(byte2.toInt() and 0xFF shl 8 or (byte1.toInt() and 0xFF))
}
}
fun unpad(padded: ByteArray): String {
val unpaddedLen: Int = bytesToInt(padded[0], padded[1], true)
@@ -273,11 +274,11 @@ class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) {
}
}
fun encodePayload(): String {
return Base64.getEncoder()
fun encodePayload(): String =
Base64
.getEncoder()
.encodeToString(
byteArrayOf(V.toByte()) + nonce + ciphertext + mac,
)
}
}
}
@@ -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.crypto
package com.vitorpamplona.quartz.crypto.nip44
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
@@ -66,9 +66,7 @@ fun cryptoStreamXchacha20Xor(
messageLen: Long,
nonce: ByteArray,
key: ByteArray,
): Int {
return cryptoStreamXchacha20XorIc(libSodium, cipher, message, messageLen, nonce, 0, key)
}
): Int = cryptoStreamXchacha20XorIc(libSodium, cipher, message, messageLen, nonce, 0, key)
fun cryptoStreamXChaCha20Xor(
libSodium: SodiumAndroid,
@@ -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.crypto
package com.vitorpamplona.quartz.crypto.nip49
import android.util.Log
import com.goterl.lazysodium.LazySodiumAndroid
@@ -32,16 +32,17 @@ import fr.acinq.secp256k1.Secp256k1
import java.security.SecureRandom
import java.text.Normalizer
class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
class Nip49(
val secp256k1: Secp256k1,
val random: SecureRandom,
) {
private val libSodium = SodiumAndroid()
private val lazySodium = LazySodiumAndroid(libSodium)
fun decrypt(
nCryptSec: String,
password: String,
): HexKey {
return decrypt(EncryptedInfo.decodePayload(nCryptSec), password)
}
): HexKey = decrypt(EncryptedInfo.decodePayload(nCryptSec), password)
fun decrypt(
encryptedInfo: EncryptedInfo?,
@@ -75,11 +76,9 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
fun encrypt(
secretKeyHex: String,
password: String,
logn: Int,
ksb: Byte,
): String? {
return encrypt(secretKeyHex.hexToByteArray(), password, logn, ksb)
}
logn: Int = 16,
ksb: Byte = EncryptedInfo.CLIENT_DOES_NOT_TRACK,
): String = encrypt(secretKeyHex.hexToByteArray(), password, logn, ksb)
fun encrypt(
secretKey: ByteArray,
@@ -104,9 +103,12 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
// byte[] ad, long adLen,
// byte[] nSec, byte[] nPub, byte[] k
lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
ciphertext, longArrayOf(48),
secretKey, secretKey.size.toLong(),
byteArrayOf(ksb), 1,
ciphertext,
longArrayOf(48),
secretKey,
secretKey.size.toLong(),
byteArrayOf(ksb),
1,
key,
nonce,
key,
@@ -163,8 +165,8 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
}
// ln(n.toDouble()).toInt().toByte(),
fun encodePayload(): String {
return Bech32.encodeBytes(
fun encodePayload(): String =
Bech32.encodeBytes(
hrp = "ncryptsec",
byteArrayOf(
version,
@@ -172,6 +174,5 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
) + salt + nonce + keySecurity + encryptedKey,
Bech32.Encoding.Bech32,
)
}
}
}
@@ -1,6 +1,6 @@
// Copyright (C) 2011 - Will Glozer. All rights reserved.
package com.vitorpamplona.quartz.crypto;
package com.vitorpamplona.quartz.crypto.nip49;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@@ -1,6 +1,6 @@
// Copyright (C) 2011 - Will Glozer. All rights reserved.
package com.vitorpamplona.quartz.crypto;
package com.vitorpamplona.quartz.crypto.nip49;
import javax.crypto.Mac;
import java.security.GeneralSecurityException;
@@ -23,7 +23,7 @@
* questions.
*/
package com.vitorpamplona.quartz.crypto;
package com.vitorpamplona.quartz.crypto.nip49;
import java.security.MessageDigest;
import java.security.spec.KeySpec;
@@ -31,7 +31,9 @@ import com.vitorpamplona.quartz.events.EventFactory
import com.vitorpamplona.quartz.events.LnZapPrivateEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toHexKey()) {
class NostrSignerInternal(
val keyPair: KeyPair,
) : NostrSigner(keyPair.pubKey.toHexKey()) {
override fun <T : Event> sign(
createdAt: Long,
kind: Int,
@@ -52,10 +54,9 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH
fun isUnsignedPrivateEvent(
kind: Int,
tags: Array<Array<String>>,
): Boolean {
return kind == LnZapRequestEvent.KIND &&
): Boolean =
kind == LnZapRequestEvent.KIND &&
tags.any { t -> t.size > 1 && t[0] == "anon" && t[1].isBlank() }
}
fun <T : Event> signNormal(
createdAt: Long,
@@ -123,12 +124,12 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH
if (keyPair.privKey == null) return
onReady(
CryptoUtils.encryptNIP44v2(
decryptedContent,
keyPair.privKey,
toPublicKey.hexToByteArray(),
)
.encodePayload(),
CryptoUtils
.encryptNIP44(
decryptedContent,
keyPair.privKey,
toPublicKey.hexToByteArray(),
).encodePayload(),
)
}
@@ -139,12 +140,12 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH
) {
if (keyPair.privKey == null) return
CryptoUtils.decryptNIP44(
payload = encryptedContent,
privateKey = keyPair.privKey,
pubKey = fromPublicKey.hexToByteArray(),
)
?.let { onReady(it) }
CryptoUtils
.decryptNIP44(
payload = encryptedContent,
privateKey = keyPair.privKey,
pubKey = fromPublicKey.hexToByteArray(),
)?.let { onReady(it) }
}
private fun <T> signPrivateZap(