mirror of
https://github.com/oxen-io/session-android.git
synced 2023-12-14 02:53:01 +01:00
253 lines
11 KiB
Kotlin
253 lines
11 KiB
Kotlin
package org.session.libsession.messaging.utilities
|
|
|
|
import com.goterl.lazysodium.LazySodiumAndroid
|
|
import com.goterl.lazysodium.SodiumAndroid
|
|
import com.goterl.lazysodium.interfaces.AEAD
|
|
import com.goterl.lazysodium.interfaces.GenericHash
|
|
import com.goterl.lazysodium.interfaces.Hash
|
|
import com.goterl.lazysodium.interfaces.Sign
|
|
import com.goterl.lazysodium.utils.Key
|
|
import com.goterl.lazysodium.utils.KeyPair
|
|
import org.session.libsignal.utilities.Hex
|
|
import org.session.libsignal.utilities.IdPrefix
|
|
import org.session.libsignal.utilities.SessionId
|
|
import org.whispersystems.curve25519.Curve25519
|
|
import kotlin.experimental.xor
|
|
|
|
object SodiumUtilities {
|
|
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
|
private val curve by lazy { Curve25519.getInstance(Curve25519.BEST) }
|
|
|
|
private const val SCALAR_LENGTH: Int = 32 // crypto_core_ed25519_scalarbytes
|
|
private const val NO_CLAMP_LENGTH: Int = 32 // crypto_scalarmult_ed25519_bytes
|
|
private const val SCALAR_MULT_LENGTH: Int = 32 // crypto_scalarmult_bytes
|
|
private const val PUBLIC_KEY_LENGTH: Int = 32 // crypto_scalarmult_bytes
|
|
private const val SECRET_KEY_LENGTH: Int = 64 //crypto_sign_secretkeybytes
|
|
|
|
/* 64-byte blake2b hash then reduce to get the blinding factor */
|
|
fun generateBlindingFactor(serverPublicKey: String): ByteArray? {
|
|
// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
|
|
val serverPubKeyData = Hex.fromStringCondensed(serverPublicKey)
|
|
if (serverPubKeyData.size != PUBLIC_KEY_LENGTH) return null
|
|
val serverPubKeyHash = ByteArray(GenericHash.BLAKE2B_BYTES_MAX)
|
|
if (!sodium.cryptoGenericHash(serverPubKeyHash, serverPubKeyHash.size, serverPubKeyData, serverPubKeyData.size.toLong())) {
|
|
return null
|
|
}
|
|
// Reduce the server public key into an ed25519 scalar (`k`)
|
|
val x25519PublicKey = ByteArray(SCALAR_LENGTH)
|
|
sodium.cryptoCoreEd25519ScalarReduce(x25519PublicKey, serverPubKeyHash)
|
|
return if (x25519PublicKey.any { it.toInt() != 0 }) {
|
|
x25519PublicKey
|
|
} else null
|
|
}
|
|
|
|
/*
|
|
Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
|
|
convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
|
|
same secret scalar secret (and so this is just the most convenient way to get 'a' out of
|
|
a sodium Ed25519 secret key)
|
|
*/
|
|
fun generatePrivateKeyScalar(secretKey: ByteArray): ByteArray? {
|
|
// a = s.to_curve25519_private_key().encode()
|
|
val aBytes = ByteArray(SCALAR_MULT_LENGTH)
|
|
return if (sodium.convertSecretKeyEd25519ToCurve25519(aBytes, secretKey)) {
|
|
aBytes
|
|
} else null
|
|
}
|
|
|
|
/* Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` */
|
|
@JvmStatic
|
|
fun blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair): KeyPair? {
|
|
if (edKeyPair.publicKey.asBytes.size != PUBLIC_KEY_LENGTH || edKeyPair.secretKey.asBytes.size != SECRET_KEY_LENGTH) return null
|
|
val kBytes = generateBlindingFactor(serverPublicKey) ?: return null
|
|
val aBytes = generatePrivateKeyScalar(edKeyPair.secretKey.asBytes) ?: return null
|
|
// Generate the blinded key pair `ka`, `kA`
|
|
val kaBytes = ByteArray(SECRET_KEY_LENGTH)
|
|
sodium.cryptoCoreEd25519ScalarMul(kaBytes, kBytes, aBytes)
|
|
if (kaBytes.all { it.toInt() == 0 }) return null
|
|
|
|
val kABytes = ByteArray(PUBLIC_KEY_LENGTH)
|
|
return if (sodium.cryptoScalarMultEd25519BaseNoClamp(kABytes, kaBytes)) {
|
|
KeyPair(Key.fromBytes(kABytes), Key.fromBytes(kaBytes))
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
|
|
/*
|
|
Constructs an Ed25519 signature from a root Ed25519 key and a blinded scalar/pubkey pair, with one tweak to the
|
|
construction: we add kA into the hashed value that yields r so that we have domain separation for different blinded
|
|
pubkeys (this doesn't affect verification at all)
|
|
*/
|
|
fun sogsSignature(
|
|
message: ByteArray,
|
|
secretKey: ByteArray,
|
|
blindedSecretKey: ByteArray, /*ka*/
|
|
blindedPublicKey: ByteArray /*kA*/
|
|
): ByteArray? {
|
|
// H_rh = sha512(s.encode()).digest()[32:]
|
|
val digest = ByteArray(Hash.SHA512_BYTES)
|
|
val h_rh = if (sodium.cryptoHashSha512(digest, secretKey, secretKey.size.toLong())) {
|
|
digest.takeLast(32).toByteArray()
|
|
} else return null
|
|
|
|
// r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts))
|
|
val rHash = sha512Multipart(listOf(h_rh, blindedPublicKey, message)) ?: return null
|
|
val r = ByteArray(SCALAR_LENGTH)
|
|
sodium.cryptoCoreEd25519ScalarReduce(r, rHash)
|
|
if (r.all { it.toInt() == 0 }) return null
|
|
|
|
// sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r)
|
|
val sig_R = ByteArray(NO_CLAMP_LENGTH)
|
|
if (!sodium.cryptoScalarMultEd25519BaseNoClamp(sig_R, r)) return null
|
|
|
|
// HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts))
|
|
val hRamHash = sha512Multipart(listOf(sig_R, blindedPublicKey, message)) ?: return null
|
|
val hRam = ByteArray(SCALAR_LENGTH)
|
|
sodium.cryptoCoreEd25519ScalarReduce(hRam, hRamHash)
|
|
if (hRam.all { it.toInt() == 0 }) return null
|
|
|
|
// sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
|
|
val sig_sMul = ByteArray(SCALAR_LENGTH)
|
|
val sig_s = ByteArray(SCALAR_LENGTH)
|
|
sodium.cryptoCoreEd25519ScalarMul(sig_sMul, hRam, blindedSecretKey)
|
|
if (sig_sMul.any { it.toInt() != 0 }) {
|
|
sodium.cryptoCoreEd25519ScalarAdd(sig_s, r, sig_sMul)
|
|
if (sig_s.all { it.toInt() == 0 }) return null
|
|
} else return null
|
|
|
|
return sig_R + sig_s
|
|
}
|
|
|
|
private fun sha512Multipart(parts: List<ByteArray>): ByteArray? {
|
|
val state = Hash.State512()
|
|
sodium.cryptoHashSha512Init(state)
|
|
parts.forEach {
|
|
sodium.cryptoHashSha512Update(state, it, it.size.toLong())
|
|
}
|
|
val finalHash = ByteArray(Hash.SHA512_BYTES)
|
|
return if (sodium.cryptoHashSha512Final(state, finalHash)) {
|
|
finalHash
|
|
} else null
|
|
}
|
|
|
|
/* Combines two keys (`kA`) */
|
|
fun combineKeys(lhsKey: ByteArray, rhsKey: ByteArray): ByteArray? {
|
|
val kA = ByteArray(NO_CLAMP_LENGTH)
|
|
return if (sodium.cryptoScalarMultEd25519NoClamp(kA, lhsKey, rhsKey)) {
|
|
kA
|
|
} else null
|
|
}
|
|
|
|
/*
|
|
Calculate a shared secret for a message from A to B:
|
|
BLAKE2b(a kB || kA || kB)
|
|
The receiver can calculate the same value via:
|
|
BLAKE2b(b kA || kA || kB)
|
|
*/
|
|
fun sharedBlindedEncryptionKey(
|
|
secretKey: ByteArray,
|
|
otherBlindedPublicKey: ByteArray,
|
|
kA: ByteArray, /*fromBlindedPublicKey*/
|
|
kB: ByteArray /*toBlindedPublicKey*/
|
|
): ByteArray? {
|
|
val aBytes = generatePrivateKeyScalar(secretKey) ?: return null
|
|
val combinedKeyBytes = combineKeys(aBytes, otherBlindedPublicKey) ?: return null
|
|
val outputHash = ByteArray(GenericHash.KEYBYTES)
|
|
val inputBytes = combinedKeyBytes + kA + kB
|
|
return if (sodium.cryptoGenericHash(outputHash, outputHash.size, inputBytes, inputBytes.size.toLong())) {
|
|
outputHash
|
|
} else null
|
|
}
|
|
|
|
/* This method should be used to check if a users standard sessionId matches a blinded one */
|
|
fun sessionId(
|
|
standardSessionId: String,
|
|
blindedSessionId: String,
|
|
serverPublicKey: String
|
|
): Boolean {
|
|
// Only support generating blinded keys for standard session ids
|
|
val sessionId = SessionId(standardSessionId)
|
|
if (sessionId.prefix != IdPrefix.STANDARD) return false
|
|
val blindedId = SessionId(blindedSessionId)
|
|
if (blindedId.prefix != IdPrefix.BLINDED) return false
|
|
val k = generateBlindingFactor(serverPublicKey) ?: return false
|
|
|
|
// From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys;
|
|
// the first is the positive (which is what Signal's XEd25519 conversion always uses)
|
|
val xEd25519Key = curve.convertToEd25519PublicKey(Key.fromHexString(sessionId.publicKey).asBytes)
|
|
|
|
// Blind the positive public key
|
|
val pk1 = combineKeys(k, xEd25519Key) ?: return false
|
|
|
|
// For the negative, what we're going to get out of the above is simply the negative of pk1, so flip the sign bit to get pk2
|
|
// pk2 = pk1[0:31] + bytes([pk1[31] ^ 0b1000_0000])
|
|
val pk2 = pk1.take(31).toByteArray() + listOf(pk1.last().xor(128.toByte())).toByteArray()
|
|
return SessionId(IdPrefix.BLINDED, pk1).publicKey == blindedId.publicKey ||
|
|
SessionId(IdPrefix.BLINDED, pk2).publicKey == blindedId.publicKey
|
|
}
|
|
|
|
fun encrypt(message: ByteArray, secretKey: ByteArray, nonce: ByteArray, additionalData: ByteArray? = null): ByteArray? {
|
|
val authenticatedCipherText = ByteArray(message.size + AEAD.CHACHA20POLY1305_ABYTES)
|
|
return if (sodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
|
authenticatedCipherText,
|
|
longArrayOf(0),
|
|
message,
|
|
message.size.toLong(),
|
|
additionalData,
|
|
(additionalData?.size ?: 0).toLong(),
|
|
null,
|
|
nonce,
|
|
secretKey
|
|
)
|
|
) {
|
|
authenticatedCipherText
|
|
} else null
|
|
}
|
|
|
|
fun decrypt(ciphertext: ByteArray, decryptionKey: ByteArray, nonce: ByteArray): ByteArray? {
|
|
val plaintextSize = ciphertext.size - AEAD.XCHACHA20POLY1305_IETF_ABYTES
|
|
val plaintext = ByteArray(plaintextSize)
|
|
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
|
plaintext,
|
|
longArrayOf(plaintextSize.toLong()),
|
|
null,
|
|
ciphertext,
|
|
ciphertext.size.toLong(),
|
|
null,
|
|
0L,
|
|
nonce,
|
|
decryptionKey
|
|
)
|
|
) {
|
|
plaintext
|
|
} else null
|
|
}
|
|
|
|
fun toX25519(ed25519PublicKey: ByteArray): ByteArray? {
|
|
val x25519PublicKey = ByteArray(PUBLIC_KEY_LENGTH)
|
|
return if (sodium.convertPublicKeyEd25519ToCurve25519(x25519PublicKey, ed25519PublicKey)) {
|
|
x25519PublicKey
|
|
} else null
|
|
}
|
|
|
|
/**
|
|
* Returns true only if the signature verified successfully
|
|
*/
|
|
fun verifySignature(signature: ByteArray, publicKey: ByteArray, messageToVerify: ByteArray): Boolean {
|
|
return sodium.cryptoSignVerifyDetached(signature, messageToVerify, messageToVerify.size, publicKey)
|
|
}
|
|
|
|
/**
|
|
* For signing
|
|
*/
|
|
fun sign(message: ByteArray, signingKey: ByteArray): ByteArray {
|
|
val signature = ByteArray(Sign.BYTES)
|
|
|
|
if (!sodium.cryptoSignDetached(signature, message, message.size.toLong(), signingKey)) {
|
|
throw SecurityException("Couldn't sign the message with the signing key")
|
|
}
|
|
return signature
|
|
}
|
|
}
|