import java.io.File
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.time.Instant
import java.util.Base64
data class AuditEntry(
val seq: Int,
val timestamp: String,
val action: String,
val agentDID: String,
val grantId: String,
val scopes: List<String>,
val result: String,
val prevHash: String,
val hash: String,
val signature: String,
)
class OfflineAuditLog(
private val signingKeyPem: String,
private val logFile: File,
) {
private var seq = 0
private var prevHash = "0000000000000000" // GENESIS_HASH
init {
// Resume from existing log
if (logFile.exists()) {
val lines = logFile.readLines().filter { it.isNotBlank() }
if (lines.isNotEmpty()) {
val last = Json.decodeFromString<AuditEntry>(lines.last())
seq = last.seq
prevHash = last.hash
}
}
}
fun append(
action: String,
agentDID: String,
grantId: String,
scopes: List<String>,
result: String,
): AuditEntry {
val nextSeq = ++seq
val timestamp = Instant.now().toString()
// Compute hash
val hashInput = "$nextSeq|$timestamp|$action|$agentDID|$grantId|${scopes.joinToString(",")}|$result||$prevHash"
val hash = sha256Hex(hashInput)
// Sign the hash with Ed25519
val signature = signEd25519(hash, signingKeyPem)
val entry = AuditEntry(
seq = nextSeq,
timestamp = timestamp,
action = action,
agentDID = agentDID,
grantId = grantId,
scopes = scopes,
result = result,
prevHash = prevHash,
hash = hash,
signature = signature,
)
logFile.appendText(Json.encodeToString(entry) + "\n")
prevHash = hash
return entry
}
private fun sha256Hex(input: String): String {
val digest = MessageDigest.getInstance("SHA-256")
return digest.digest(input.toByteArray()).joinToString("") {
"%02x".format(it)
}
}
private fun signEd25519(data: String, pem: String): String {
val keyBytes = Base64.getDecoder().decode(
pem.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("\\s".toRegex(), "")
)
val keySpec = PKCS8EncodedKeySpec(keyBytes)
val keyFactory = KeyFactory.getInstance("Ed25519")
val privateKey = keyFactory.generatePrivate(keySpec)
val sig = Signature.getInstance("Ed25519")
sig.initSign(privateKey)
sig.update(data.toByteArray())
return sig.sign().joinToString("") { "%02x".format(it) }
}
}