package no.zyon.shade import no.zyon.shade.crypto.CryptoProvider import no.zyon.shade.fingerprint.computeFingerprint import no.zyon.shade.protocol.createPreKeyBundle import no.zyon.shade.protocol.generateIdentityKeyPair import no.zyon.shade.protocol.generateOneTimePreKeys import no.zyon.shade.protocol.generateSignedPreKey import no.zyon.shade.protocol.initReceiverSession import no.zyon.shade.protocol.initSenderSession import no.zyon.shade.protocol.processPreKeyBundle import no.zyon.shade.protocol.processPreKeyMessage import no.zyon.shade.protocol.ratchetDecrypt import no.zyon.shade.protocol.ratchetEncrypt import no.zyon.shade.storage.StorageProvider import no.zyon.shade.types.OneTimePreKey import no.zyon.shade.types.PreKeyBundle import no.zyon.shade.types.PreKeyMessage import no.zyon.shade.types.RatchetMessage import no.zyon.shade.types.ShadeEnvelope import no.zyon.shade.types.SignedPreKey /** * High-level API mirroring @shade/core's ShadeSessionManager. * * Handles X3DH + Double Ratchet, persists state via StorageProvider. */ class ShadeSessionManager( private val crypto: CryptoProvider, private val storage: StorageProvider, ) { private var identity: no.zyon.shade.types.IdentityKeyPair? = null private var registrationId: Int = 0 private var currentSignedPreKeyId: Int = 0 // X3DH pending metadata (used for first message after bundle processing) private val pendingX3DH = mutableMapOf() private data class PendingX3DH( val ephemeralPublicKey: ByteArray, val signedPreKeyId: Int, val preKeyId: Int?, val identityDHKey: ByteArray, val registrationId: Int, ) suspend fun initialize() { identity = storage.getIdentityKeyPair() ?: run { val fresh = generateIdentityKeyPair(crypto) storage.saveIdentityKeyPair(fresh) fresh } registrationId = storage.getLocalRegistrationId() if (registrationId == 0) { var id = crypto.randomUint32() if (id == 0) id = 1 registrationId = id storage.saveLocalRegistrationId(id) } val spk = storage.getSignedPreKey(1) if (spk == null) { val fresh = generateSignedPreKey(crypto, identity!!, 1) storage.saveSignedPreKey(fresh) currentSignedPreKeyId = 1 } else { currentSignedPreKeyId = spk.keyId } } fun getPublicIdentity(): Pair { val id = identity ?: throw IllegalStateException("Not initialized") return id.signingPublicKey to id.dhPublicKey } suspend fun getIdentityFingerprint(): String { val id = identity ?: throw IllegalStateException("Not initialized") return computeFingerprint(crypto, id.signingPublicKey, id.dhPublicKey) } suspend fun createPreKeyBundle(): PreKeyBundle { val id = identity ?: throw IllegalStateException("Not initialized") val spk = storage.getSignedPreKey(currentSignedPreKeyId) ?: throw IllegalStateException("No signed prekey") return createPreKeyBundle(registrationId, id, spk) } suspend fun generateOneTimePreKeys(count: Int): List { val existing = storage.getOneTimePreKeyCount() val startId = existing + 1 val keys = generateOneTimePreKeys(crypto, startId, count) for (k in keys) storage.saveOneTimePreKey(k) return keys } suspend fun rotateSignedPreKey(): SignedPreKey { val id = identity ?: throw IllegalStateException("Not initialized") val newId = currentSignedPreKeyId + 1 val spk = generateSignedPreKey(crypto, id, newId) storage.saveSignedPreKey(spk) currentSignedPreKeyId = newId return spk } suspend fun initSessionFromBundle(address: String, bundle: PreKeyBundle) { val id = identity ?: throw IllegalStateException("Not initialized") val x3dhResult = processPreKeyBundle(crypto, id, bundle) val session = initSenderSession( crypto, x3dhResult.rootKey, x3dhResult.remoteIdentityKey, x3dhResult.remoteSignedPreKey, ) storage.saveSession(address, session) storage.saveTrustedIdentity(address, x3dhResult.remoteIdentityKey) pendingX3DH[address] = PendingX3DH( ephemeralPublicKey = x3dhResult.ephemeralPublicKey, signedPreKeyId = x3dhResult.signedPreKeyId, preKeyId = x3dhResult.preKeyId, identityDHKey = id.dhPublicKey, registrationId = registrationId, ) } suspend fun encrypt(address: String, plaintext: ByteArray): ShadeEnvelope { val session = storage.getSession(address) ?: throw IllegalStateException("No session for $address") val ratchetMsg = ratchetEncrypt(crypto, session, plaintext) val pending = pendingX3DH.remove(address) if (pending != null) { storage.saveSession(address, session) val preKeyMsg = PreKeyMessage( registrationId = pending.registrationId, preKeyId = pending.preKeyId, signedPreKeyId = pending.signedPreKeyId, ephemeralKey = pending.ephemeralPublicKey, identityDHKey = pending.identityDHKey, message = ratchetMsg, ) return ShadeEnvelope( type = ShadeEnvelope.EnvelopeType.PREKEY, content = preKeyMsg, timestamp = System.currentTimeMillis(), senderAddress = address, ) } storage.saveSession(address, session) return ShadeEnvelope( type = ShadeEnvelope.EnvelopeType.RATCHET, content = ratchetMsg, timestamp = System.currentTimeMillis(), senderAddress = address, ) } suspend fun decrypt(address: String, envelope: ShadeEnvelope): ByteArray { return when (envelope.type) { ShadeEnvelope.EnvelopeType.PREKEY -> decryptPreKeyMessage(address, envelope.content as PreKeyMessage) ShadeEnvelope.EnvelopeType.RATCHET -> decryptRatchetMessage(address, envelope.content as RatchetMessage) } } private suspend fun decryptPreKeyMessage(address: String, message: PreKeyMessage): ByteArray { val id = identity ?: throw IllegalStateException("Not initialized") val spk = storage.getSignedPreKey(message.signedPreKeyId) ?: throw IllegalStateException("Signed prekey ${message.signedPreKeyId} not found") val oneTimePrivate: ByteArray? = message.preKeyId?.let { keyId -> val otpk = storage.getOneTimePreKey(keyId) ?: throw IllegalStateException("One-time prekey $keyId not found") storage.removeOneTimePreKey(keyId) otpk.keyPair.privateKey } val x3dhResult = processPreKeyMessage( crypto, id, spk.keyPair.privateKey, oneTimePrivate, message, ) val session = initReceiverSession( rootKey = x3dhResult.rootKey, remoteIdentityKey = x3dhResult.remoteIdentityKey, localDHKeyPair = spk.keyPair, ) val plaintext = ratchetDecrypt(crypto, session, message.message) storage.saveSession(address, session) storage.saveTrustedIdentity(address, x3dhResult.remoteIdentityKey) return plaintext } private suspend fun decryptRatchetMessage(address: String, message: RatchetMessage): ByteArray { val session = storage.getSession(address) ?: throw IllegalStateException("No session for $address") val plaintext = ratchetDecrypt(crypto, session, message) storage.saveSession(address, session) return plaintext } }