feat: XK and XX handshake using noise lib
This commit is contained in:
parent
0badc8862c
commit
e62e85615a
@ -10,9 +10,9 @@ import javax.crypto.ShortBufferException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.Arrays
|
||||
|
||||
class NoiseHandler(
|
||||
private val localKeyBase64: String, // ED25519 private (initiator) or public (responder) key (Base64-encoded)
|
||||
private val remotePublicKeyBase64: String // Remote ED25519 public key (Base64-encoded)
|
||||
class NoiseHandlerXK(
|
||||
private val localKeyBase64: String, // ED25519 private (initiator) or private (responder) key (Base64-encoded)
|
||||
private val remotePublicKeyBase64: String? // Remote ED25519 public key (Base64-encoded, required for initiator only)
|
||||
) {
|
||||
private var handshakeState: HandshakeState? = null
|
||||
private var cipherStatePair: CipherStatePair? = null
|
||||
@ -25,7 +25,7 @@ class NoiseHandler(
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Noise handshake.
|
||||
* Initializes the Noise handshake with the XK pattern.
|
||||
* @param isInitiator True if this is the initiator, false if responder.
|
||||
* @return The initial handshake message.
|
||||
* @throws IllegalArgumentException If keys are invalid.
|
||||
@ -35,36 +35,38 @@ class NoiseHandler(
|
||||
var localKey: ByteArray? = null
|
||||
var remotePublicKey: ByteArray? = null
|
||||
try {
|
||||
val protocolName = "Noise_IK_25519_AESGCM_SHA256"
|
||||
val protocolName = "Noise_XK_25519_AESGCM_SHA256"
|
||||
handshakeState = HandshakeState(
|
||||
protocolName,
|
||||
if (isInitiator) HandshakeState.INITIATOR else HandshakeState.RESPONDER
|
||||
)
|
||||
|
||||
// Set local key (private for initiator, public for responder)
|
||||
// Set local key (private key for both initiator and responder in XK)
|
||||
localKey = Base64.decode(localKeyBase64, Base64.DEFAULT)
|
||||
if (localKey.size != 32) {
|
||||
throw IllegalArgumentException("Invalid local key size: ${localKey.size}")
|
||||
}
|
||||
if (isInitiator) {
|
||||
handshakeState?.localKeyPair?.setPrivateKey(localKey, 0)
|
||||
?: throw IllegalStateException("Local key pair not initialized")
|
||||
} else {
|
||||
handshakeState?.localKeyPair?.setPublicKey(localKey, 0)
|
||||
?: throw IllegalStateException("Local key pair not initialized")
|
||||
}
|
||||
handshakeState?.localKeyPair?.setPrivateKey(localKey, 0)
|
||||
?: throw IllegalStateException("Local key pair not initialized")
|
||||
|
||||
// Set remote public key
|
||||
remotePublicKey = Base64.decode(remotePublicKeyBase64, Base64.DEFAULT)
|
||||
if (remotePublicKey.size != 32) {
|
||||
throw IllegalArgumentException("Invalid remote public key size: ${remotePublicKey.size}")
|
||||
// Set remote public key (required for initiator only in XK)
|
||||
if (isInitiator) {
|
||||
if (remotePublicKeyBase64 == null) {
|
||||
throw IllegalArgumentException("Remote public key required for initiator")
|
||||
}
|
||||
remotePublicKey = Base64.decode(remotePublicKeyBase64, Base64.DEFAULT)
|
||||
if (remotePublicKey.size != 32) {
|
||||
throw IllegalArgumentException("Invalid remote public key size: ${remotePublicKey.size}")
|
||||
}
|
||||
handshakeState?.remotePublicKey?.setPublicKey(remotePublicKey, 0)
|
||||
?: throw IllegalStateException("Remote public key not initialized")
|
||||
}
|
||||
handshakeState?.remotePublicKey?.setPublicKey(remotePublicKey, 0)
|
||||
?: throw IllegalStateException("Remote public key not initialized")
|
||||
|
||||
// Start handshake and write initial message
|
||||
handshakeState?.start() ?: throw IllegalStateException("Handshake state not initialized")
|
||||
val messageBuffer = ByteArray(256) // Sufficient for IK initial message
|
||||
val keyLength = handshakeState?.localKeyPair?.getPublicKeyLength() ?: 32
|
||||
val macLength = 16 // AES-GCM MAC length
|
||||
val messageBuffer = ByteArray(keyLength + macLength + 128) // Conservative sizing
|
||||
val payload = ByteArray(0) // Empty payload
|
||||
val writtenLength: Int = handshakeState?.writeMessage(
|
||||
messageBuffer, 0, payload, 0, payload.size
|
||||
@ -90,7 +92,9 @@ class NoiseHandler(
|
||||
fun processHandshakeMessage(message: ByteArray): ByteArray? {
|
||||
try {
|
||||
val handshake = handshakeState ?: throw IllegalStateException("Handshake not initialized")
|
||||
val messageBuffer = ByteArray(256) // Sufficient for IK payload + MAC
|
||||
val keyLength = handshake.localKeyPair?.getPublicKeyLength() ?: 32
|
||||
val macLength = 16 // AES-GCM MAC length
|
||||
val messageBuffer = ByteArray(keyLength + macLength + 128) // Conservative sizing
|
||||
val writtenLength: Int = handshake.readMessage(
|
||||
message, 0, message.size, messageBuffer, 0
|
||||
)
|
||||
@ -102,7 +106,7 @@ class NoiseHandler(
|
||||
|
||||
// Write next message
|
||||
val payload = ByteArray(0) // Empty payload
|
||||
val nextMessage = ByteArray(256)
|
||||
val nextMessage = ByteArray(keyLength + macLength + 128)
|
||||
val nextWrittenLength: Int = handshake.writeMessage(
|
||||
nextMessage, 0, payload, 0, payload.size
|
||||
)
|
@ -0,0 +1,172 @@
|
||||
package com.icing.dialer
|
||||
|
||||
import android.util.Base64
|
||||
import com.southernstorm.noise.protocol.CipherStatePair
|
||||
import com.southernstorm.noise.protocol.HandshakeState
|
||||
import com.southernstorm.noise.protocol.Noise
|
||||
import java.util.Arrays
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.ShortBufferException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
class NoiseHandlerXX(
|
||||
private val localKeyBase64: String // ED25519 private key (Base64-encoded)
|
||||
) {
|
||||
private var handshakeState: HandshakeState? = null
|
||||
private var cipherStatePair: CipherStatePair? = null
|
||||
private var remotePublicKeyBase64: String? = null
|
||||
|
||||
/**
|
||||
* Wipes sensitive data by filling the byte array with zeros.
|
||||
*/
|
||||
private fun wipe(data: ByteArray?) {
|
||||
data?.let { Arrays.fill(it, 0.toByte()) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Noise XX handshake.
|
||||
* @param isInitiator True if this is the initiator, false if responder.
|
||||
* @return The initial handshake message.
|
||||
* @throws IllegalArgumentException If local key is invalid.
|
||||
* @throws IllegalStateException If handshake fails to start.
|
||||
*/
|
||||
fun initialize(isInitiator: Boolean): ByteArray {
|
||||
var localKey: ByteArray? = null
|
||||
try {
|
||||
val protocolName = "Noise_XX_25519_AESGCM_SHA256"
|
||||
handshakeState = HandshakeState(
|
||||
protocolName,
|
||||
if (isInitiator) HandshakeState.INITIATOR else HandshakeState.RESPONDER
|
||||
)
|
||||
|
||||
// Set local private key
|
||||
localKey = Base64.decode(localKeyBase64, Base64.DEFAULT)
|
||||
if (localKey.size != 32) {
|
||||
throw IllegalArgumentException("Invalid local key size: ${localKey.size}")
|
||||
}
|
||||
handshakeState?.localKeyPair?.setPrivateKey(localKey, 0)
|
||||
?: throw IllegalStateException("Local key pair not initialized")
|
||||
|
||||
// Start handshake and write initial message
|
||||
handshakeState?.start() ?: throw IllegalStateException("Handshake state not initialized")
|
||||
val keyLength = handshakeState?.localKeyPair?.getPublicKeyLength() ?: 32
|
||||
val macLength = 16 // AES-GCM MAC length
|
||||
val messageBuffer = ByteArray(keyLength + macLength + 128) // Conservative sizing
|
||||
val payload = ByteArray(0) // Empty payload
|
||||
val writtenLength: Int = handshakeState?.writeMessage(
|
||||
messageBuffer, 0, payload, 0, payload.size
|
||||
) ?: throw IllegalStateException("Failed to write handshake message")
|
||||
return messageBuffer.copyOf(writtenLength)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IllegalStateException("Unsupported algorithm: ${e.message}", e)
|
||||
} catch (e: ShortBufferException) {
|
||||
throw IllegalStateException("Buffer too small for handshake message", e)
|
||||
} finally {
|
||||
wipe(localKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a handshake message and returns the next message or null if complete.
|
||||
* @param message The received handshake message.
|
||||
* @return The next handshake message, null if handshake is complete, or the remote public key (Base64-encoded).
|
||||
* @throws IllegalStateException If handshake state is invalid.
|
||||
* @throws BadPaddingException If message decryption fails.
|
||||
*/
|
||||
fun processHandshakeMessage(message: ByteArray): Pair<ByteArray?, String?> {
|
||||
var remotePublicKey: ByteArray? = null
|
||||
try {
|
||||
val handshake = handshakeState ?: throw IllegalStateException("Handshake not initialized")
|
||||
val keyLength = handshake.localKeyPair?.getPublicKeyLength() ?: 32
|
||||
val macLength = 16 // AES-GCM MAC length
|
||||
val messageBuffer = ByteArray(keyLength + macLength + 128) // Conservative sizing
|
||||
val payload = ByteArray(0) // Empty payload
|
||||
val writtenLength: Int = handshake.readMessage(
|
||||
message, 0, message.size, messageBuffer, 0
|
||||
)
|
||||
|
||||
// Extract remote public key after reading message
|
||||
if (handshake.remotePublicKey.hasPublicKey()) {
|
||||
remotePublicKey = ByteArray(keyLength)
|
||||
handshake.remotePublicKey.getPublicKey(remotePublicKey, 0)
|
||||
remotePublicKeyBase64 = Base64.encodeToString(remotePublicKey, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
if (handshake.getAction() == HandshakeState.SPLIT) {
|
||||
cipherStatePair = handshake.split()
|
||||
return Pair(null, remotePublicKeyBase64) // Handshake complete
|
||||
}
|
||||
|
||||
// Write next message
|
||||
val nextMessage = ByteArray(keyLength + macLength + 128)
|
||||
val nextWrittenLength: Int = handshake.writeMessage(
|
||||
nextMessage, 0, payload, 0, payload.size
|
||||
)
|
||||
return Pair(nextMessage.copyOf(nextWrittenLength), remotePublicKeyBase64)
|
||||
} catch (e: ShortBufferException) {
|
||||
throw IllegalStateException("Buffer too small for handshake message", e)
|
||||
} catch (e: BadPaddingException) {
|
||||
throw IllegalStateException("Invalid handshake message: ${e.message}", e)
|
||||
} finally {
|
||||
wipe(remotePublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts data using the sender's cipher state.
|
||||
* @param data The data to encrypt.
|
||||
* @return The encrypted data.
|
||||
* @throws IllegalStateException If handshake is not completed.
|
||||
*/
|
||||
fun encryptData(data: ByteArray): ByteArray {
|
||||
val cipherState = cipherStatePair?.getSender()
|
||||
?: throw IllegalStateException("Handshake not completed")
|
||||
try {
|
||||
val outputBuffer = ByteArray(data.size + cipherState.getMACLength()) // Account for AES-GCM MAC
|
||||
val length: Int = cipherState.encryptWithAd(null, data, 0, outputBuffer, 0, data.size)
|
||||
return outputBuffer.copyOf(length)
|
||||
} catch (e: ShortBufferException) {
|
||||
throw IllegalStateException("Buffer too small for encryption: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts data using the receiver's cipher state.
|
||||
* @param data The encrypted data.
|
||||
* @return The decrypted data.
|
||||
* @throws IllegalStateException If handshake is not completed.
|
||||
* @throws BadPaddingException If decryption fails.
|
||||
*/
|
||||
fun decryptData(data: ByteArray): ByteArray {
|
||||
val cipherState = cipherStatePair?.getReceiver()
|
||||
?: throw IllegalStateException("Handshake not completed")
|
||||
try {
|
||||
val outputBuffer = ByteArray(data.size)
|
||||
val length: Int = cipherState.decryptWithAd(null, data, 0, outputBuffer, 0, data.size)
|
||||
return outputBuffer.copyOf(length)
|
||||
} catch (e: ShortBufferException) {
|
||||
throw IllegalStateException("Buffer too small for decryption: ${e.message}", e)
|
||||
} catch (e: BadPaddingException) {
|
||||
throw IllegalStateException("Invalid ciphertext: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remote public key received during the handshake.
|
||||
* @return The Base64-encoded remote public key, or null if not yet received.
|
||||
*/
|
||||
fun getRemotePublicKey(): String? {
|
||||
return remotePublicKeyBase64
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up sensitive cryptographic data.
|
||||
*/
|
||||
fun destroy() {
|
||||
handshakeState?.destroy()
|
||||
cipherStatePair?.destroy()
|
||||
handshakeState = null
|
||||
cipherStatePair = null
|
||||
remotePublicKeyBase64 = null
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user