diff --git a/build.gradle.kts b/build.gradle.kts index dbd4976..22c3980 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { // Apply the java-library plugin for API and implementation separation. `java-library` - id("org.jlleitschuh.gradle.ktlint") version "11.6.1" + id("org.jlleitschuh.gradle.ktlint") version "12.1.1" jacoco @@ -104,9 +104,10 @@ tasks.withType().configureEach { kotlinOptions { jvmTarget = "1.8" allWarningsAsErrors = true - freeCompilerArgs = freeCompilerArgs + arrayOf( - "-opt-in=kotlin.RequiresOptIn" - ) + freeCompilerArgs = freeCompilerArgs + + arrayOf( + "-opt-in=kotlin.RequiresOptIn", + ) } } @@ -129,7 +130,7 @@ publishing { pom { name.set(rootProject.name) description.set( - "JVM implementation of file-based Private and Public Key Stores for Awala" + "JVM implementation of file-based Private and Public Key Stores for Awala", ) url.set("https://github.com/relaycorp/awala-keystore-file-jvm") developers { @@ -146,10 +147,10 @@ publishing { } scm { connection.set( - "scm:git:https://github.com/relaycorp/awala-keystore-file-jvm.git" + "scm:git:https://github.com/relaycorp/awala-keystore-file-jvm.git", ) developerConnection.set( - "scm:git:https://github.com/relaycorp/awala-keystore-file-jvm.git" + "scm:git:https://github.com/relaycorp/awala-keystore-file-jvm.git", ) url.set("https://github.com/relaycorp/awala-keystore-file-jvm") } @@ -171,7 +172,7 @@ nexusPublishing { sonatype { nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) snapshotRepositoryUrl.set( - uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"), ) username.set(System.getenv("MAVEN_USERNAME")) password.set(System.getenv("MAVEN_PASSWORD")) @@ -183,7 +184,7 @@ tasks.publish { } configure { - version.set("0.42.1") + version.set("1.3.1") } gradleEnterprise { diff --git a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileCertificateStore.kt b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileCertificateStore.kt index 423d1ae..33ae5d3 100644 --- a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileCertificateStore.kt +++ b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileCertificateStore.kt @@ -10,8 +10,9 @@ import java.time.ZoneId import java.time.ZonedDateTime import tech.relaycorp.relaynet.keystores.CertificateStore -public class FileCertificateStore(keystoreRoot: FileKeystoreRoot) : CertificateStore() { - +public class FileCertificateStore( + keystoreRoot: FileKeystoreRoot, +) : CertificateStore() { @Suppress("MemberVisibilityCanBePrivate") public val rootDirectory: File = keystoreRoot.directory.resolve("certificate") @@ -19,19 +20,20 @@ public class FileCertificateStore(keystoreRoot: FileKeystoreRoot) : CertificateS subjectId: String, leafCertificateExpiryDate: ZonedDateTime, certificationPathData: ByteArray, - issuerId: String + issuerId: String, ) { val expirationTimestamp = leafCertificateExpiryDate.toTimestamp() val dataDigest = certificationPathData.toDigest() - val certFile = getNodeSubdirectory(subjectId, issuerId).resolve( - "$expirationTimestamp-$dataDigest" - ) + val certFile = + getNodeSubdirectory(subjectId, issuerId).resolve( + "$expirationTimestamp-$dataDigest", + ) saveCertificationFile(certFile, certificationPathData) } override suspend fun retrieveData( subjectId: String, - issuerId: String + issuerId: String, ): List { val certificateFiles = getNodeSubdirectory(subjectId, issuerId).listFiles() @@ -57,22 +59,28 @@ public class FileCertificateStore(keystoreRoot: FileKeystoreRoot) : CertificateS } @Throws(FileKeystoreException::class) - override fun delete(subjectId: String, issuerId: String) { + override fun delete( + subjectId: String, + issuerId: String, + ) { val deletionSucceeded = getNodeSubdirectory(subjectId, issuerId).deleteRecursively() if (!deletionSucceeded) { throw FileKeystoreException( - "Failed to delete node directory for $subjectId" + "Failed to delete node directory for $subjectId", ) } } - private fun saveCertificationFile(certFile: File, serialization: ByteArray) { + private fun saveCertificationFile( + certFile: File, + serialization: ByteArray, + ) { val parentDirectory = certFile.parentFile val wereDirectoriesCreated = parentDirectory.mkdirs() if (!wereDirectoriesCreated && !parentDirectory.exists()) { throw FileKeystoreException( - "Failed to create address directory for certification files" + "Failed to create address directory for certification files", ) } try { @@ -85,22 +93,21 @@ public class FileCertificateStore(keystoreRoot: FileKeystoreRoot) : CertificateS } } - private fun retrieveData(file: File): ByteArray { - return try { + private fun retrieveData(file: File): ByteArray = + try { FileInputStream(file).use { it.readBytes() } } catch (exc: IOException) { throw FileKeystoreException("Failed to read certification file", exc) } - } - private fun ZonedDateTime.toTimestamp() = - toInstant().toEpochMilli() + private fun ZonedDateTime.toTimestamp() = toInstant().toEpochMilli() private fun Long.toZonedDateTime() = ZonedDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.of("UTC")) private fun File.getExpiryDateFromName(): ZonedDateTime = - name.split("-") + name + .split("-") .first() .toLongOrNull() ?.toZonedDateTime() @@ -108,12 +115,12 @@ public class FileCertificateStore(keystoreRoot: FileKeystoreRoot) : CertificateS private fun getNodeSubdirectory( subjectId: String, - issuerId: String - ) = - rootDirectory.resolve(issuerId).resolve(subjectId) + issuerId: String, + ) = rootDirectory.resolve(issuerId).resolve(subjectId) } internal fun ByteArray.toDigest() = - MessageDigest.getInstance("SHA-256") + MessageDigest + .getInstance("SHA-256") .digest(this) .joinToString("") { "%02x".format(it) } diff --git a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreException.kt b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreException.kt index 0fa022a..d531d0d 100644 --- a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreException.kt +++ b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreException.kt @@ -2,5 +2,7 @@ package tech.relaycorp.awala.keystores.file import tech.relaycorp.relaynet.keystores.KeyStoreBackendException -public class FileKeystoreException(message: String, cause: Throwable? = null) : - KeyStoreBackendException(message, cause) +public class FileKeystoreException( + message: String, + cause: Throwable? = null, +) : KeyStoreBackendException(message, cause) diff --git a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreRoot.kt b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreRoot.kt index e45c902..2f679cd 100644 --- a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreRoot.kt +++ b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreRoot.kt @@ -2,22 +2,24 @@ package tech.relaycorp.awala.keystores.file import java.io.File -public class FileKeystoreRoot @Throws(FileKeystoreException::class) constructor( - internal val directory: File -) { - init { - if (directory.exists()) { - if (!directory.isDirectory) { - throw FileKeystoreException("Root '${directory.path}' isn't a directory") - } +public class FileKeystoreRoot + @Throws(FileKeystoreException::class) + constructor( + internal val directory: File, + ) { + init { + if (directory.exists()) { + if (!directory.isDirectory) { + throw FileKeystoreException("Root '${directory.path}' isn't a directory") + } - // Check permissions (read and write operations are always allowed on Windows) - if (!directory.canRead()) { - throw FileKeystoreException("Root '${directory.path}' isn't readable") - } - if (!directory.canWrite()) { - throw FileKeystoreException("Root '${directory.path}' isn't writable") + // Check permissions (read and write operations are always allowed on Windows) + if (!directory.canRead()) { + throw FileKeystoreException("Root '${directory.path}' isn't readable") + } + if (!directory.canWrite()) { + throw FileKeystoreException("Root '${directory.path}' isn't writable") + } } } } -} diff --git a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FilePrivateKeyStore.kt b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FilePrivateKeyStore.kt index 3d8301a..152a5b6 100644 --- a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FilePrivateKeyStore.kt +++ b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FilePrivateKeyStore.kt @@ -7,14 +7,16 @@ import java.io.OutputStream import tech.relaycorp.relaynet.keystores.PrivateKeyData import tech.relaycorp.relaynet.keystores.PrivateKeyStore -public abstract class FilePrivateKeyStore(keystoreRoot: FileKeystoreRoot) : PrivateKeyStore() { +public abstract class FilePrivateKeyStore( + keystoreRoot: FileKeystoreRoot, +) : PrivateKeyStore() { @Suppress("MemberVisibilityCanBePrivate") public val rootDirectory: File = keystoreRoot.directory.resolve("private") @Suppress("BlockingMethodInNonBlockingContext") override suspend fun saveIdentityKeyData( nodeId: String, - keyData: PrivateKeyData + keyData: PrivateKeyData, ) { val keyFile = getNodeSubdirectory(nodeId).resolve("identity") saveKeyFile(keyFile, keyData.privateKeyDer) @@ -51,7 +53,10 @@ public abstract class FilePrivateKeyStore(keystoreRoot: FileKeystoreRoot) : Priv return retrieveKeyData(boundKeyPath) ?: retrieveKeyData(unboundKeyPath) } - private fun saveKeyFile(keyFile: File, serialization: ByteArray) { + private fun saveKeyFile( + keyFile: File, + serialization: ByteArray, + ) { val parentDirectory = keyFile.parentFile val wereDirectoriesCreated = parentDirectory.mkdirs() if (!wereDirectoriesCreated && !parentDirectory.exists()) { @@ -82,18 +87,19 @@ public abstract class FilePrivateKeyStore(keystoreRoot: FileKeystoreRoot) : Priv private fun resolveSessionKeyFile( nodeId: String, keyId: String, - peerId: String? + peerId: String?, ): File { val nodeSubdirectory = getNodeSubdirectory(nodeId).resolve("session") - val parentDirectory = if (peerId != null) - nodeSubdirectory.resolve(peerId) - else - nodeSubdirectory + val parentDirectory = + if (peerId != null) { + nodeSubdirectory.resolve(peerId) + } else { + nodeSubdirectory + } return parentDirectory.resolve(keyId) } - private fun getNodeSubdirectory(nodeId: String) = - rootDirectory.resolve(nodeId) + private fun getNodeSubdirectory(nodeId: String) = rootDirectory.resolve(nodeId) private fun getNodeDirectories() = rootDirectory.listFiles()?.filter(File::isDirectory) @@ -116,14 +122,18 @@ public abstract class FilePrivateKeyStore(keystoreRoot: FileKeystoreRoot) : Priv * Delete all the private keys associated with [peerId]. */ @Throws(FileKeystoreException::class) - override suspend fun deleteBoundSessionKeys(nodeId: String, peerId: String) { - val deletionSucceeded = getNodeSubdirectory(nodeId) - .resolve("session") - .resolve(peerId) - .deleteRecursively() + override suspend fun deleteBoundSessionKeys( + nodeId: String, + peerId: String, + ) { + val deletionSucceeded = + getNodeSubdirectory(nodeId) + .resolve("session") + .resolve(peerId) + .deleteRecursively() if (!deletionSucceeded) { throw FileKeystoreException( - "Failed to delete session keys for node $nodeId and peer $peerId" + "Failed to delete session keys for node $nodeId and peer $peerId", ) } } diff --git a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileSessionPublicKeystore.kt b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileSessionPublicKeystore.kt index d673057..8f9a44b 100644 --- a/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileSessionPublicKeystore.kt +++ b/src/main/kotlin/tech/relaycorp/awala/keystores/file/FileSessionPublicKeystore.kt @@ -12,7 +12,7 @@ import tech.relaycorp.relaynet.keystores.SessionPublicKeyData import tech.relaycorp.relaynet.keystores.SessionPublicKeyStore public class FileSessionPublicKeystore( - keystoreRoot: FileKeystoreRoot + keystoreRoot: FileKeystoreRoot, ) : SessionPublicKeyStore() { @Suppress("MemberVisibilityCanBePrivate") public val rootDirectory: File = keystoreRoot.directory.resolve("public") @@ -20,7 +20,7 @@ public class FileSessionPublicKeystore( override suspend fun saveKeyData( keyData: SessionPublicKeyData, nodeId: String, - peerId: String + peerId: String, ) { val wasDirectoryCreated = rootDirectory.mkdirs() if (!wasDirectoryCreated && !rootDirectory.exists()) { @@ -28,16 +28,17 @@ public class FileSessionPublicKeystore( } val keyDataFile = getKeyDataFile(nodeId, peerId) - val bsonSerialization = BasicOutputBuffer().use { buffer -> - BsonBinaryWriter(buffer).use { - it.writeStartDocument() - it.writeBinaryData("key_id", BsonBinary(keyData.keyId)) - it.writeBinaryData("key_der", BsonBinary(keyData.keyDer)) - it.writeInt32("creation_timestamp", keyData.creationTimestamp.toInt()) - it.writeEndDocument() + val bsonSerialization = + BasicOutputBuffer().use { buffer -> + BsonBinaryWriter(buffer).use { + it.writeStartDocument() + it.writeBinaryData("key_id", BsonBinary(keyData.keyId)) + it.writeBinaryData("key_der", BsonBinary(keyData.keyDer)) + it.writeInt32("creation_timestamp", keyData.creationTimestamp.toInt()) + it.writeEndDocument() + } + buffer.toByteArray() } - buffer.toByteArray() - } try { keyDataFile.writeBytes(bsonSerialization) } catch (exc: IOException) { @@ -45,36 +46,46 @@ public class FileSessionPublicKeystore( } } - override suspend fun retrieveKeyData(nodeId: String, peerId: String): SessionPublicKeyData? { + override suspend fun retrieveKeyData( + nodeId: String, + peerId: String, + ): SessionPublicKeyData? { val keyDataFile = getKeyDataFile(nodeId, peerId) - val serialization = try { - keyDataFile.readBytes() - } catch (exc: IOException) { - if (keyDataFile.exists()) { - throw FileKeystoreException("Failed to read key file", exc) + val serialization = + try { + keyDataFile.readBytes() + } catch (exc: IOException) { + if (keyDataFile.exists()) { + throw FileKeystoreException("Failed to read key file", exc) + } + return null } - return null - } - val data = try { - BsonBinaryReader(ByteBuffer.wrap(serialization)).use { - it.readStartDocument() - SessionPublicKeyData( - it.readBinaryData("key_id").data, - it.readBinaryData("key_der").data, - it.readInt32("creation_timestamp").toLong() - ) + val data = + try { + BsonBinaryReader(ByteBuffer.wrap(serialization)).use { + it.readStartDocument() + SessionPublicKeyData( + it.readBinaryData("key_id").data, + it.readBinaryData("key_der").data, + it.readInt32("creation_timestamp").toLong(), + ) + } + } catch (exc: BSONException) { + throw FileKeystoreException("Key file is malformed", exc) } - } catch (exc: BSONException) { - throw FileKeystoreException("Key file is malformed", exc) - } return data } - override suspend fun delete(nodeId: String, peerId: String) { + override suspend fun delete( + nodeId: String, + peerId: String, + ) { val keyDataFile = getKeyDataFile(nodeId, peerId) keyDataFile.delete() } - private fun getKeyDataFile(nodeId: String, peerId: String) = - rootDirectory.resolve("$nodeId-$peerId") + private fun getKeyDataFile( + nodeId: String, + peerId: String, + ) = rootDirectory.resolve("$nodeId-$peerId") } diff --git a/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileCertificateStoreTest.kt b/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileCertificateStoreTest.kt index de63219..ea47d8c 100644 --- a/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileCertificateStoreTest.kt +++ b/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileCertificateStoreTest.kt @@ -20,33 +20,35 @@ import tech.relaycorp.relaynet.testing.pki.PDACertPath @ExperimentalCoroutinesApi @Suppress("BlockingMethodInNonBlockingContext") internal class FileCertificateStoreTest : KeystoreTestCase() { - private val storeRootFile = keystoreRoot.directory.resolve("certificate") - private val certificate = issueGatewayCertificate( - KeyPairSet.PRIVATE_GW.public, - KeyPairSet.INTERNET_GW.private, - validityEndDate = ZonedDateTime.now().plusHours(1), - validityStartDate = ZonedDateTime.now().minusSeconds(1) - ) + private val certificate = + issueGatewayCertificate( + KeyPairSet.PRIVATE_GW.public, + KeyPairSet.INTERNET_GW.private, + validityEndDate = ZonedDateTime.now().plusHours(1), + validityStartDate = ZonedDateTime.now().minusSeconds(1), + ) private val chain = listOf(PDACertPath.INTERNET_GW) private val issuerId = PDACertPath.INTERNET_GW.subjectId private val subjectId = certificate.subjectId private val issuerFolder = storeRootFile.resolve(issuerId) private val subjectFolder = issuerFolder.resolve(subjectId) - private val longerCertificate = issueGatewayCertificate( - KeyPairSet.PRIVATE_GW.public, - KeyPairSet.INTERNET_GW.private, - validityEndDate = ZonedDateTime.now().plusHours(10), - validityStartDate = ZonedDateTime.now().minusSeconds(1) - ) - private val aboutToExpireCertificate = issueGatewayCertificate( - KeyPairSet.PRIVATE_GW.public, - KeyPairSet.INTERNET_GW.private, - validityEndDate = ZonedDateTime.now().plusNanos(100_000_000), - validityStartDate = ZonedDateTime.now().minusSeconds(1) - ) + private val longerCertificate = + issueGatewayCertificate( + KeyPairSet.PRIVATE_GW.public, + KeyPairSet.INTERNET_GW.private, + validityEndDate = ZonedDateTime.now().plusHours(10), + validityStartDate = ZonedDateTime.now().minusSeconds(1), + ) + private val aboutToExpireCertificate = + issueGatewayCertificate( + KeyPairSet.PRIVATE_GW.public, + KeyPairSet.INTERNET_GW.private, + validityEndDate = ZonedDateTime.now().plusNanos(100_000_000), + validityStartDate = ZonedDateTime.now().minusSeconds(1), + ) private val unrelatedCertificate = PDACertPath.PRIVATE_ENDPOINT @Test @@ -59,168 +61,193 @@ internal class FileCertificateStoreTest : KeystoreTestCase() { @Nested inner class SaveData { @Test - fun `Certificate should be stored and retrieved`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) - - keystore.save(CertificationPath(certificate, chain), issuerId) - - val result = keystore.retrieveAll(subjectId, issuerId) - assertEquals(1, result.size) - assertEquals( - certificate.serialize().asList(), - result.first().leafCertificate.serialize().asList() - ) - assertEquals( - chain.map { it.serialize().asList() }, - result.first().certificateAuthorities.map { it.serialize().asList() } - ) - - val addressFiles = subjectFolder.listFiles()!! - assertEquals(1, addressFiles.size) - with(addressFiles.first()) { - assertTrue(exists()) - assertTrue( - name.startsWith("${certificate.expiryDate.toInstant().toEpochMilli()}-") + fun `Certificate should be stored and retrieved`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) + + keystore.save(CertificationPath(certificate, chain), issuerId) + + val result = keystore.retrieveAll(subjectId, issuerId) + assertEquals(1, result.size) + assertEquals( + certificate.serialize().asList(), + result + .first() + .leafCertificate + .serialize() + .asList(), ) + assertEquals( + chain.map { it.serialize().asList() }, + result.first().certificateAuthorities.map { it.serialize().asList() }, + ) + + val addressFiles = subjectFolder.listFiles()!! + assertEquals(1, addressFiles.size) + with(addressFiles.first()) { + assertTrue(exists()) + assertTrue( + name.startsWith("${certificate.expiryDate.toInstant().toEpochMilli()}-"), + ) + } } - } @Test - fun `Certificate stored multiple times should override`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `Certificate stored multiple times should override`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - repeat(3) { - keystore.save(CertificationPath(certificate, chain), issuerId) - } + repeat(3) { + keystore.save(CertificationPath(certificate, chain), issuerId) + } - val result = keystore.retrieveAll(subjectId, issuerId) - assertEquals(1, result.size) - assertEquals( - certificate.serialize().asList(), - result.first().leafCertificate.serialize().asList() - ) - assertEquals( - chain.map { it.serialize().asList() }, - result.first().certificateAuthorities.map { it.serialize().asList() } - ) - - val addressFiles = subjectFolder.listFiles()!! - assertEquals(1, addressFiles.size) - } + val result = keystore.retrieveAll(subjectId, issuerId) + assertEquals(1, result.size) + assertEquals( + certificate.serialize().asList(), + result + .first() + .leafCertificate + .serialize() + .asList(), + ) + assertEquals( + chain.map { it.serialize().asList() }, + result.first().certificateAuthorities.map { it.serialize().asList() }, + ) - @Test - internal fun `Certificates by different issuers should not override`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) - - keystore.save(CertificationPath(certificate, chain), issuerId) - keystore.save(CertificationPath(certificate, emptyList()), issuerId + "diff") - - val result = keystore.retrieveAll(subjectId, issuerId) - assertEquals(1, result.size) - assertEquals( - certificate.serialize().asList(), - result.first().leafCertificate.serialize().asList() - ) - assertEquals( - chain.map { it.serialize().asList() }, - result.first().certificateAuthorities.map { it.serialize().asList() } - ) - - val resultDiff = keystore.retrieveAll(subjectId, issuerId + "diff") - assertEquals(1, resultDiff.size) - assertEquals( - certificate.serialize().asList(), - resultDiff.first().leafCertificate.serialize().asList() - ) - assertTrue( - resultDiff.first().certificateAuthorities.isEmpty() - ) - } + val addressFiles = subjectFolder.listFiles()!! + assertEquals(1, addressFiles.size) + } @Test - @DisabledOnOs(OS.WINDOWS) - fun `Errors creating address subdirectory should be wrapped`() = runTest { - keystoreRoot.directory.setExecutable(false) - keystoreRoot.directory.setWritable(false) - val keystore = FileCertificateStore(keystoreRoot) + internal fun `Certificates by different issuers should not override`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - val exception = assertThrows { keystore.save(CertificationPath(certificate, chain), issuerId) - } + keystore.save(CertificationPath(certificate, emptyList()), issuerId + "diff") - assertEquals( - "Failed to create address directory for certification files", - exception.message - ) - } + val result = keystore.retrieveAll(subjectId, issuerId) + assertEquals(1, result.size) + assertEquals( + certificate.serialize().asList(), + result + .first() + .leafCertificate + .serialize() + .asList(), + ) + assertEquals( + chain.map { it.serialize().asList() }, + result.first().certificateAuthorities.map { it.serialize().asList() }, + ) + + val resultDiff = keystore.retrieveAll(subjectId, issuerId + "diff") + assertEquals(1, resultDiff.size) + assertEquals( + certificate.serialize().asList(), + resultDiff + .first() + .leafCertificate + .serialize() + .asList(), + ) + assertTrue( + resultDiff.first().certificateAuthorities.isEmpty(), + ) + } @Test @DisabledOnOs(OS.WINDOWS) - fun `Errors creating or updating file should be wrapped`() = runTest { - subjectFolder.mkdirs() - subjectFolder.setWritable(false) - val keystore = FileCertificateStore(keystoreRoot) + fun `Errors creating address subdirectory should be wrapped`() = + runTest { + keystoreRoot.directory.setExecutable(false) + keystoreRoot.directory.setWritable(false) + val keystore = FileCertificateStore(keystoreRoot) - val exception = assertThrows { - keystore.save(CertificationPath(certificate, chain), issuerId) + val exception = + assertThrows { + keystore.save(CertificationPath(certificate, chain), issuerId) + } + + assertEquals( + "Failed to create address directory for certification files", + exception.message, + ) } - assertEquals("Failed to save certification file", exception.message) - assertTrue(exception.cause is IOException) - } + @Test + @DisabledOnOs(OS.WINDOWS) + fun `Errors creating or updating file should be wrapped`() = + runTest { + subjectFolder.mkdirs() + subjectFolder.setWritable(false) + val keystore = FileCertificateStore(keystoreRoot) + + val exception = + assertThrows { + keystore.save(CertificationPath(certificate, chain), issuerId) + } + + assertEquals("Failed to save certification file", exception.message) + assertTrue(exception.cause is IOException) + } } @Nested inner class RetrieveData { - @Test - fun `All valid certificates should be retrieved`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) - - keystore.save(CertificationPath(certificate, chain), issuerId) - keystore.save(CertificationPath(longerCertificate, chain), issuerId) - keystore.save(CertificationPath(aboutToExpireCertificate, chain), issuerId) - - Thread.sleep(300) // wait for aboutToExpireCertificate to expire - - val result = keystore.retrieveAll(subjectId, issuerId) - assertEquals(2, result.size) - assertTrue( - result.any { certPath -> - certificate.serialize().asList() == - certPath.leafCertificate.serialize().asList() && - chain.map { it.serialize().asList() } == - certPath.certificateAuthorities.map { it.serialize().asList() } - } - ) - - assertTrue( - result.any { certPath -> - longerCertificate.serialize().asList() == - certPath.leafCertificate.serialize().asList() && - chain.map { it.serialize().asList() } == - certPath.certificateAuthorities.map { it.serialize().asList() } - } - ) - } + fun `All valid certificates should be retrieved`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) + + keystore.save(CertificationPath(certificate, chain), issuerId) + keystore.save(CertificationPath(longerCertificate, chain), issuerId) + keystore.save(CertificationPath(aboutToExpireCertificate, chain), issuerId) + + Thread.sleep(300) // wait for aboutToExpireCertificate to expire + + val result = keystore.retrieveAll(subjectId, issuerId) + assertEquals(2, result.size) + assertTrue( + result.any { certPath -> + certificate.serialize().asList() == + certPath.leafCertificate.serialize().asList() && + chain.map { it.serialize().asList() } == + certPath.certificateAuthorities.map { it.serialize().asList() } + }, + ) + + assertTrue( + result.any { certPath -> + longerCertificate.serialize().asList() == + certPath.leafCertificate.serialize().asList() && + chain.map { it.serialize().asList() } == + certPath.certificateAuthorities.map { it.serialize().asList() } + }, + ) + } @Test - fun `Certificates should not be retrieved with wrong issuer`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `Certificates should not be retrieved with wrong issuer`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - keystore.save(CertificationPath(certificate, chain), issuerId) + keystore.save(CertificationPath(certificate, chain), issuerId) - val result = keystore.retrieveAll(subjectId, "wrong") - assertTrue(result.isEmpty()) - } + val result = keystore.retrieveAll(subjectId, "wrong") + assertTrue(result.isEmpty()) + } @Test - fun `If there are no certificates return empty list`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `If there are no certificates return empty list`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - val result = keystore.retrieveAll(subjectId, issuerId) - assertTrue(result.isEmpty()) - } + val result = keystore.retrieveAll(subjectId, issuerId) + assertTrue(result.isEmpty()) + } @Test @DisabledOnOs(OS.WINDOWS) // Windows can't tell apart between not-readable and non-existing @@ -234,12 +261,13 @@ internal class FileCertificateStoreTest : KeystoreTestCase() { file.createNewFile() file.setReadable(false) - val exception = assertThrows { - keystore.retrieveAll(subjectId, issuerId) - } + val exception = + assertThrows { + keystore.retrieveAll(subjectId, issuerId) + } assertEquals( "Failed to read certification file", - exception.message + exception.message, ) } @@ -251,12 +279,13 @@ internal class FileCertificateStoreTest : KeystoreTestCase() { subjectFolder.mkdirs() subjectFolder.resolve("INVALID").createNewFile() - val exception = assertThrows { - keystore.retrieveAll(subjectId, issuerId) - } + val exception = + assertThrows { + keystore.retrieveAll(subjectId, issuerId) + } assertEquals( "Invalid certificate file name: INVALID", - exception.message + exception.message, ) } @@ -268,138 +297,145 @@ internal class FileCertificateStoreTest : KeystoreTestCase() { subjectFolder.mkdirs() subjectFolder.resolve("AAA-AAA").createNewFile() - val exception = assertThrows { - keystore.retrieveAll(subjectId, issuerId) - } + val exception = + assertThrows { + keystore.retrieveAll(subjectId, issuerId) + } assertEquals( "Invalid certificate file name: AAA-AAA", - exception.message + exception.message, ) } } @Nested inner class DeleteExpired { - @Test - fun `Certificates that are expired are deleted`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `Certificates that are expired are deleted`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - keystore.save(CertificationPath(certificate, chain), issuerId) - // create empty expired cert file - subjectFolder.resolve("0-12345").createNewFile() + keystore.save(CertificationPath(certificate, chain), issuerId) + // create empty expired cert file + subjectFolder.resolve("0-12345").createNewFile() - assertEquals(2, subjectFolder.listFiles()!!.size) + assertEquals(2, subjectFolder.listFiles()!!.size) - keystore.deleteExpired() + keystore.deleteExpired() - assertEquals(1, subjectFolder.listFiles()!!.size) - } + assertEquals(1, subjectFolder.listFiles()!!.size) + } @Test - fun `Skip if root folder couldn't be read`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `Skip if root folder couldn't be read`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - storeRootFile.setReadable(false) + storeRootFile.setReadable(false) - keystore.deleteExpired() - storeRootFile.setReadable(true) - } + keystore.deleteExpired() + storeRootFile.setReadable(true) + } @Test - fun `Skip if issuer folder couldn't be read`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `Skip if issuer folder couldn't be read`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - issuerFolder.mkdirs() - issuerFolder.setReadable(false) + issuerFolder.mkdirs() + issuerFolder.setReadable(false) - keystore.deleteExpired() - issuerFolder.setReadable(true) - } + keystore.deleteExpired() + issuerFolder.setReadable(true) + } @Test - fun `Skip if address folder couldn't be read`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `Skip if address folder couldn't be read`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - subjectFolder.mkdirs() - subjectFolder.setReadable(false) + subjectFolder.mkdirs() + subjectFolder.setReadable(false) - keystore.deleteExpired() - subjectFolder.setReadable(true) - } + keystore.deleteExpired() + subjectFolder.setReadable(true) + } @Test - fun `Skip files inside root folder`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `Skip files inside root folder`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - storeRootFile.mkdirs() - storeRootFile.resolve("file").createNewFile() + storeRootFile.mkdirs() + storeRootFile.resolve("file").createNewFile() - keystore.deleteExpired() - } + keystore.deleteExpired() + } @Test - fun `Skip files inside issuer folder`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `Skip files inside issuer folder`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - issuerFolder.mkdirs() - issuerFolder.resolve("file").createNewFile() + issuerFolder.mkdirs() + issuerFolder.resolve("file").createNewFile() - keystore.deleteExpired() - } + keystore.deleteExpired() + } @Test - fun `Skip if expired certificate couldn't be deleted`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) + fun `Skip if expired certificate couldn't be deleted`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) - subjectFolder.mkdirs() - val file = subjectFolder.resolve("0-12345") - file.createNewFile() - storeRootFile.setWritable(false) + subjectFolder.mkdirs() + val file = subjectFolder.resolve("0-12345") + file.createNewFile() + storeRootFile.setWritable(false) - keystore.deleteExpired() - } + keystore.deleteExpired() + } } @Nested inner class Delete { - @Test - fun `Certificates of given subject and issuer addresses are deleted`() = runTest { - val keystore = FileCertificateStore(keystoreRoot) - - keystore.save(CertificationPath(certificate, chain), issuerId) - keystore.save(CertificationPath(unrelatedCertificate, chain), issuerId) - keystore.save(CertificationPath(certificate, chain), issuerId + "diff") - - assertEquals( - 1, - keystore.retrieveAll(certificate.subjectId, issuerId).size - ) - assertEquals( - 1, - keystore.retrieveAll(unrelatedCertificate.subjectId, issuerId).size - ) - assertEquals( - 1, - keystore.retrieveAll(certificate.subjectId, issuerId + "diff").size - ) - - keystore.delete(certificate.subjectId, issuerId) - - assertEquals( - 0, - keystore.retrieveAll(certificate.subjectId, issuerId).size - ) - assertEquals( - 1, - keystore.retrieveAll(unrelatedCertificate.subjectId, issuerId).size - ) - assertEquals( - 1, - keystore.retrieveAll(certificate.subjectId, issuerId + "diff").size - ) - } + fun `Certificates of given subject and issuer addresses are deleted`() = + runTest { + val keystore = FileCertificateStore(keystoreRoot) + + keystore.save(CertificationPath(certificate, chain), issuerId) + keystore.save(CertificationPath(unrelatedCertificate, chain), issuerId) + keystore.save(CertificationPath(certificate, chain), issuerId + "diff") + + assertEquals( + 1, + keystore.retrieveAll(certificate.subjectId, issuerId).size, + ) + assertEquals( + 1, + keystore.retrieveAll(unrelatedCertificate.subjectId, issuerId).size, + ) + assertEquals( + 1, + keystore.retrieveAll(certificate.subjectId, issuerId + "diff").size, + ) + + keystore.delete(certificate.subjectId, issuerId) + + assertEquals( + 0, + keystore.retrieveAll(certificate.subjectId, issuerId).size, + ) + assertEquals( + 1, + keystore.retrieveAll(unrelatedCertificate.subjectId, issuerId).size, + ) + assertEquals( + 1, + keystore.retrieveAll(certificate.subjectId, issuerId + "diff").size, + ) + } @Test @DisabledOnOs(OS.WINDOWS) // Windows can't tell apart between not-writable and non-existing @@ -410,12 +446,13 @@ internal class FileCertificateStoreTest : KeystoreTestCase() { subjectFolder.mkdirs() issuerFolder.setWritable(false) - val exception = assertThrows { - keystore.delete(subjectId, issuerId) - } + val exception = + assertThrows { + keystore.delete(subjectId, issuerId) + } assertEquals( "Failed to delete node directory for $subjectId", - exception.message + exception.message, ) } @@ -429,7 +466,7 @@ internal class FileCertificateStoreTest : KeystoreTestCase() { assertEquals( 1, - keystore.retrieveAll(certificate.subjectId, issuerId).size + keystore.retrieveAll(certificate.subjectId, issuerId).size, ) } } diff --git a/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreRootTest.kt b/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreRootTest.kt index 04633dc..a7471e7 100644 --- a/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreRootTest.kt +++ b/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileKeystoreRootTest.kt @@ -33,7 +33,8 @@ class FileKeystoreRootTest { if (rootDirectoryPath.exists()) { rootDirectoryPath.toFile().setReadable(true) - Files.walk(rootDirectoryPath) + Files + .walk(rootDirectoryPath) .sorted(Comparator.reverseOrder()) .map(Path::toFile) .forEach(File::delete) @@ -52,13 +53,14 @@ class FileKeystoreRootTest { val file = rootDirectoryFile.resolve("file.txt") file.createNewFile() - val exception = assertThrows { - FileKeystoreRoot(file) - } + val exception = + assertThrows { + FileKeystoreRoot(file) + } assertEquals( "Root '${file.path}' isn't a directory", - exception.message + exception.message, ) } @@ -88,13 +90,14 @@ class FileKeystoreRootTest { fun `Root directory should be refused if it isn't readable`() { rootDirectoryPath.toFile().setReadable(false) - val exception = assertThrows { - FileKeystoreRoot(rootDirectoryFile) - } + val exception = + assertThrows { + FileKeystoreRoot(rootDirectoryFile) + } assertEquals( "Root '${rootDirectoryPath.pathString}' isn't readable", - exception.message + exception.message, ) } @@ -103,13 +106,14 @@ class FileKeystoreRootTest { fun `Root directory should be refused if it isn't writable`() { rootDirectoryPath.toFile().setWritable(false) - val exception = assertThrows { - FileKeystoreRoot(rootDirectoryFile) - } + val exception = + assertThrows { + FileKeystoreRoot(rootDirectoryFile) + } assertEquals( "Root '${rootDirectoryPath.pathString}' isn't writable", - exception.message + exception.message, ) } } diff --git a/src/test/kotlin/tech/relaycorp/awala/keystores/file/FilePrivateKeyStoreTest.kt b/src/test/kotlin/tech/relaycorp/awala/keystores/file/FilePrivateKeyStoreTest.kt index 923ac84..d33e789 100644 --- a/src/test/kotlin/tech/relaycorp/awala/keystores/file/FilePrivateKeyStoreTest.kt +++ b/src/test/kotlin/tech/relaycorp/awala/keystores/file/FilePrivateKeyStoreTest.kt @@ -35,11 +35,12 @@ class FilePrivateKeyStoreTest : KeystoreTestCase() { private val identityKeyFilePath = nodeDirectoryPath.resolve("identity") private val boundSessionKeyFilePath = nodeDirectoryPath.resolve("session").resolve(peerId).resolve( - byteArrayToHex(sessionKeypair.sessionKey.keyId) + byteArrayToHex(sessionKeypair.sessionKey.keyId), + ) + private val unboundSessionKeyFilePath = + nodeDirectoryPath.resolve("session").resolve( + byteArrayToHex(sessionKeypair.sessionKey.keyId), ) - private val unboundSessionKeyFilePath = nodeDirectoryPath.resolve("session").resolve( - byteArrayToHex(sessionKeypair.sessionKey.keyId) - ) @Test fun `Root directory should be exposed`() { @@ -49,176 +50,186 @@ class FilePrivateKeyStoreTest : KeystoreTestCase() { } @Nested - inner class SaveIdentity : PrivateKeyStoreSavingTestCase( - keystoreRoot, - identityKeyFilePath, - { saveIdentityKey(privateKey) } - ) { - + inner class SaveIdentity : + PrivateKeyStoreSavingTestCase( + keystoreRoot, + identityKeyFilePath, + { saveIdentityKey(privateKey) }, + ) { @Test - override fun `Private key should be stored`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) + override fun `Private key should be stored`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveIdentityKey(privateKey) + keystore.saveIdentityKey(privateKey) - val savedKeyData = readKeyData(identityKeyFilePath) - assertEquals( - privateKey.encoded.asList(), - savedKeyData.asList() - ) - } + val savedKeyData = readKeyData(identityKeyFilePath) + assertEquals( + privateKey.encoded.asList(), + savedKeyData.asList(), + ) + } - private fun readKeyData(path: Path) = - MockFilePrivateKeyStore.readFile(path.toFile()) + private fun readKeyData(path: Path) = MockFilePrivateKeyStore.readFile(path.toFile()) } @Nested - inner class RetrieveIdentity : PrivateKeyStoreRetrievalTestCase( - keystoreRoot, - identityKeyFilePath, - { retrieveIdentityKey(privateId) } - ) { - + inner class RetrieveIdentity : + PrivateKeyStoreRetrievalTestCase( + keystoreRoot, + identityKeyFilePath, + { retrieveIdentityKey(privateId) }, + ) { @Test - fun `Exception should be thrown if private key does not exist`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `Exception should be thrown if private key does not exist`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) - val exception = assertThrows { - keystore.retrieveIdentityKey(privateId) - } + val exception = + assertThrows { + keystore.retrieveIdentityKey(privateId) + } - assertEquals("There is no identity key for $privateId", exception.message) - } + assertEquals("There is no identity key for $privateId", exception.message) + } @Test - override fun `Private key should be returned if file exists`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveIdentityKey(privateKey) + override fun `Private key should be returned if file exists`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveIdentityKey(privateKey) - val key = keystore.retrieveIdentityKey(privateId) + val key = keystore.retrieveIdentityKey(privateId) - assertEquals(privateKey, key) - } + assertEquals(privateKey, key) + } } @Nested inner class AllIdentityKeys { @Test - fun `Nothing should be returned if store is empty`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `Nothing should be returned if store is empty`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) - val allIdentityKeys = keystore.retrieveAllIdentityKeys() + val allIdentityKeys = keystore.retrieveAllIdentityKeys() - assertEquals(0, allIdentityKeys.size) - } + assertEquals(0, allIdentityKeys.size) + } @Test - fun `All identity key pairs should be returned`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveIdentityKey(privateKey) - val extraPrivateKey = KeyPairSet.PDA_GRANTEE.private - keystore.saveIdentityKey(extraPrivateKey) + fun `All identity key pairs should be returned`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveIdentityKey(privateKey) + val extraPrivateKey = KeyPairSet.PDA_GRANTEE.private + keystore.saveIdentityKey(extraPrivateKey) - val allIdentityKeys = keystore.retrieveAllIdentityKeys() + val allIdentityKeys = keystore.retrieveAllIdentityKeys() - assertEquals(2, allIdentityKeys.size) - assertContains(allIdentityKeys, privateKey) - assertContains(allIdentityKeys, extraPrivateKey) - } + assertEquals(2, allIdentityKeys.size) + assertContains(allIdentityKeys, privateKey) + assertContains(allIdentityKeys, extraPrivateKey) + } @Test - fun `Irrelevant subdirectories should be ignored`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveIdentityKey(privateKey) - privateKeystoreRootFile.resolve("invalid").toPath().createDirectories() + fun `Irrelevant subdirectories should be ignored`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveIdentityKey(privateKey) + privateKeystoreRootFile.resolve("invalid").toPath().createDirectories() - val allIdentityKeys = keystore.retrieveAllIdentityKeys() + val allIdentityKeys = keystore.retrieveAllIdentityKeys() - assertEquals(1, allIdentityKeys.size) - assertContains(allIdentityKeys, privateKey) - } + assertEquals(1, allIdentityKeys.size) + assertContains(allIdentityKeys, privateKey) + } @Test - fun `Irrelevant files should be ignored`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveIdentityKey(privateKey) - privateKeystoreRootFile.resolve("invalid").createNewFile() + fun `Irrelevant files should be ignored`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveIdentityKey(privateKey) + privateKeystoreRootFile.resolve("invalid").createNewFile() - val allIdentityKeys = keystore.retrieveAllIdentityKeys() + val allIdentityKeys = keystore.retrieveAllIdentityKeys() - assertEquals(1, allIdentityKeys.size) - assertContains(allIdentityKeys, privateKey) - } + assertEquals(1, allIdentityKeys.size) + assertContains(allIdentityKeys, privateKey) + } } @Nested - inner class SaveSession : PrivateKeyStoreSavingTestCase( - keystoreRoot, - unboundSessionKeyFilePath, - { - saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId - ) - } - ) { - + inner class SaveSession : + PrivateKeyStoreSavingTestCase( + keystoreRoot, + unboundSessionKeyFilePath, + { + saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + ) + }, + ) { @Test - override fun `Private key should be stored`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) + override fun `Private key should be stored`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId - ) + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + ) - assertEquals( - sessionKeypair.privateKey.encoded.asList(), - readKeyData(unboundSessionKeyFilePath).asList() - ) - } + assertEquals( + sessionKeypair.privateKey.encoded.asList(), + readKeyData(unboundSessionKeyFilePath).asList(), + ) + } @Test - fun `Existing file should be updated if key already existed`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId - ) - - // Replace the private key - val differentSessionKeyPair = SessionKeyPair.generate() - keystore.saveSessionKey( - differentSessionKeyPair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId - ) - - assertEquals( - differentSessionKeyPair.privateKey.encoded.asList(), - readKeyData(unboundSessionKeyFilePath).asList() - ) - } + fun `Existing file should be updated if key already existed`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + ) + + // Replace the private key + val differentSessionKeyPair = SessionKeyPair.generate() + keystore.saveSessionKey( + differentSessionKeyPair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + ) + + assertEquals( + differentSessionKeyPair.privateKey.encoded.asList(), + readKeyData(unboundSessionKeyFilePath).asList(), + ) + } @Test - fun `File should be stored under peer subdirectory if key is bound`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId, - peerId, - ) - - assertEquals( - sessionKeypair.privateKey.encoded.asList(), - readKeyData(boundSessionKeyFilePath).asList() - ) - } + fun `File should be stored under peer subdirectory if key is bound`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) + + assertEquals( + sessionKeypair.privateKey.encoded.asList(), + readKeyData(boundSessionKeyFilePath).asList(), + ) + } @Test fun `File should not be stored under a peer subdirectory if key is unbound`() = @@ -233,7 +244,7 @@ class FilePrivateKeyStoreTest : KeystoreTestCase() { assertEquals( sessionKeypair.privateKey.encoded.asList(), - readKeyData(unboundSessionKeyFilePath).asList() + readKeyData(unboundSessionKeyFilePath).asList(), ) } @@ -241,243 +252,268 @@ class FilePrivateKeyStoreTest : KeystoreTestCase() { } @Nested - inner class RetrieveSession : PrivateKeyStoreRetrievalTestCase( - keystoreRoot, - unboundSessionKeyFilePath, - { - retrieveSessionKey( - sessionKeypair.sessionKey.keyId, - privateId, - peerId - ) - } - ) { - override fun `Private key should be returned if file exists`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId - ) - - val sessionPrivateKey = keystore.retrieveSessionKey( - sessionKeypair.sessionKey.keyId, - privateId, - peerId - ) - - assertEquals(sessionKeypair.privateKey, sessionPrivateKey) - } + inner class RetrieveSession : + PrivateKeyStoreRetrievalTestCase( + keystoreRoot, + unboundSessionKeyFilePath, + { + retrieveSessionKey( + sessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) + }, + ) { + override fun `Private key should be returned if file exists`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + ) + + val sessionPrivateKey = + keystore.retrieveSessionKey( + sessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) + + assertEquals(sessionKeypair.privateKey, sessionPrivateKey) + } @Test - fun `Bound keys should be retrieved`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId, - peerId - ) - - val privateKey = keystore.retrieveSessionKey( - sessionKeypair.sessionKey.keyId, - privateId, - peerId - ) - - assertEquals(sessionKeypair.privateKey.encoded.asList(), privateKey.encoded.asList()) - } + fun `Bound keys should be retrieved`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) + + val privateKey = + keystore.retrieveSessionKey( + sessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) + + assertEquals( + sessionKeypair.privateKey.encoded.asList(), + privateKey.encoded.asList(), + ) + } @Test - fun `Unbound keys should be retrieved`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId, - ) - - val privateKey = keystore.retrieveSessionKey( - sessionKeypair.sessionKey.keyId, - privateId, - peerId - ) - - assertEquals(sessionKeypair.privateKey.encoded.asList(), privateKey.encoded.asList()) - } + fun `Unbound keys should be retrieved`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + ) + + val privateKey = + keystore.retrieveSessionKey( + sessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) + + assertEquals( + sessionKeypair.privateKey.encoded.asList(), + privateKey.encoded.asList(), + ) + } } @Nested inner class DeleteKeys { @Test - fun `Node directory should be deleted even if it contains keys`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveIdentityKey(privateKey) - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId, - ) + fun `Node directory should be deleted even if it contains keys`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveIdentityKey(privateKey) + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + ) - keystore.deleteKeys(privateId) + keystore.deleteKeys(privateId) - assertFalse(nodeDirectoryPath.exists()) - } + assertFalse(nodeDirectoryPath.exists()) + } @Test - fun `Other node directories shouldn't be deleted`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveIdentityKey(privateKey) - val node2Directory = nodeDirectoryPath.resolveSibling("node2") - node2Directory.createDirectories() - val node3Directory = nodeDirectoryPath.resolveSibling("node3") - node3Directory.createDirectories() + fun `Other node directories shouldn't be deleted`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveIdentityKey(privateKey) + val node2Directory = nodeDirectoryPath.resolveSibling("node2") + node2Directory.createDirectories() + val node3Directory = nodeDirectoryPath.resolveSibling("node3") + node3Directory.createDirectories() - keystore.deleteKeys(privateId) + keystore.deleteKeys(privateId) - assertTrue(node2Directory.exists()) - assertTrue(node3Directory.exists()) - } + assertTrue(node2Directory.exists()) + assertTrue(node3Directory.exists()) + } @Test - fun `Nothing should happen if the node directory doesn't exist`() = runTest { - assertFalse(nodeDirectoryPath.exists()) - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `Nothing should happen if the node directory doesn't exist`() = + runTest { + assertFalse(nodeDirectoryPath.exists()) + val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.deleteKeys(privateId) + keystore.deleteKeys(privateId) - assertFalse(nodeDirectoryPath.exists()) - } + assertFalse(nodeDirectoryPath.exists()) + } @Test @DisabledOnOs(OS.WINDOWS) - fun `Exception should be thrown if node directory couldn't be deleted`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveIdentityKey(privateKey) - nodeDirectoryPath.toFile().setWritable(false) - - val exception = - assertThrows { keystore.deleteKeys(privateId) } - - assertEquals( - "Failed to delete node directory for $privateId", - exception.message - ) - } + fun `Exception should be thrown if node directory couldn't be deleted`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveIdentityKey(privateKey) + nodeDirectoryPath.toFile().setWritable(false) + + val exception = + assertThrows { keystore.deleteKeys(privateId) } + + assertEquals( + "Failed to delete node directory for $privateId", + exception.message, + ) + } } @Nested inner class DeleteBoundSessionKeys { @Test - fun `Keys linked to peer should not be deleted from other nodes`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId, - peerId, - ) - val node2PrivateAddress = "AnotherPrivateAddress" - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - node2PrivateAddress, - peerId, - ) - val boundSessionKey2FilePath = privateKeystoreRootFile.resolve(node2PrivateAddress) - .resolve("session") - .resolve(peerId) - .resolve(byteArrayToHex(sessionKeypair.sessionKey.keyId)) - .toPath() - assertTrue(boundSessionKey2FilePath.exists()) - - keystore.deleteBoundSessionKeys(privateId, peerId) - - assertFalse(boundSessionKeyFilePath.parent.exists()) - assertTrue(boundSessionKey2FilePath.parent.exists()) - } + fun `Keys linked to peer should not be deleted from other nodes`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) + val node2PrivateAddress = "AnotherPrivateAddress" + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + node2PrivateAddress, + peerId, + ) + val boundSessionKey2FilePath = + privateKeystoreRootFile + .resolve(node2PrivateAddress) + .resolve("session") + .resolve(peerId) + .resolve(byteArrayToHex(sessionKeypair.sessionKey.keyId)) + .toPath() + assertTrue(boundSessionKey2FilePath.exists()) - @Test - fun `Keys linked to other peers should not be deleted`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - val peer2PrivateAddress = "Peer2Address" - val peer2SessionKeypair = SessionKeyPair.generate() - keystore.saveSessionKey( - peer2SessionKeypair.privateKey, - peer2SessionKeypair.sessionKey.keyId, - privateId, - peer2PrivateAddress, - ) - - keystore.deleteBoundSessionKeys(privateId, peerId) - - keystore.retrieveSessionKey( - peer2SessionKeypair.sessionKey.keyId, - privateId, - peer2PrivateAddress, - ) - } + keystore.deleteBoundSessionKeys(privateId, peerId) + + assertFalse(boundSessionKeyFilePath.parent.exists()) + assertTrue(boundSessionKey2FilePath.parent.exists()) + } @Test - fun `Unbound keys should not be deleted`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId, - peerId, - ) - val unboundSessionKeypair = SessionKeyPair.generate() - keystore.saveSessionKey( - unboundSessionKeypair.privateKey, - unboundSessionKeypair.sessionKey.keyId, - privateId, - ) - - keystore.deleteBoundSessionKeys(privateId, peerId) - - assertThrows { + fun `Keys linked to other peers should not be deleted`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + val peer2PrivateAddress = "Peer2Address" + val peer2SessionKeypair = SessionKeyPair.generate() + keystore.saveSessionKey( + peer2SessionKeypair.privateKey, + peer2SessionKeypair.sessionKey.keyId, + privateId, + peer2PrivateAddress, + ) + + keystore.deleteBoundSessionKeys(privateId, peerId) + keystore.retrieveSessionKey( + peer2SessionKeypair.sessionKey.keyId, + privateId, + peer2PrivateAddress, + ) + } + + @Test + fun `Unbound keys should not be deleted`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveSessionKey( + sessionKeypair.privateKey, sessionKeypair.sessionKey.keyId, privateId, peerId, ) + val unboundSessionKeypair = SessionKeyPair.generate() + keystore.saveSessionKey( + unboundSessionKeypair.privateKey, + unboundSessionKeypair.sessionKey.keyId, + privateId, + ) + + keystore.deleteBoundSessionKeys(privateId, peerId) + + assertThrows { + keystore.retrieveSessionKey( + sessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) + } + keystore.retrieveSessionKey( + unboundSessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) } - keystore.retrieveSessionKey( - unboundSessionKeypair.sessionKey.keyId, - privateId, - peerId, - ) - } @Test - fun `Nothing should happen if the root directory doesn't exist`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `Nothing should happen if the root directory doesn't exist`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.deleteBoundSessionKeys(privateId, peerId) - } + keystore.deleteBoundSessionKeys(privateId, peerId) + } @Test @DisabledOnOs(OS.WINDOWS) - fun `Exception should be thrown if a directory couldn't be deleted`() = runTest { - val keystore = MockFilePrivateKeyStore(keystoreRoot) - keystore.saveSessionKey( - sessionKeypair.privateKey, - sessionKeypair.sessionKey.keyId, - privateId, - peerId, - ) - boundSessionKeyFilePath.parent.toFile().setWritable(false) - - val exception = assertThrows { - keystore.deleteBoundSessionKeys(privateId, peerId) - } + fun `Exception should be thrown if a directory couldn't be deleted`() = + runTest { + val keystore = MockFilePrivateKeyStore(keystoreRoot) + keystore.saveSessionKey( + sessionKeypair.privateKey, + sessionKeypair.sessionKey.keyId, + privateId, + peerId, + ) + boundSessionKeyFilePath.parent.toFile().setWritable(false) + + val exception = + assertThrows { + keystore.deleteBoundSessionKeys(privateId, peerId) + } - assertEquals( - "Failed to delete session keys for node $privateId and peer $peerId", - exception.message - ) - } + assertEquals( + "Failed to delete session keys for node $privateId and peer $peerId", + exception.message, + ) + } } private fun byteArrayToHex(byteArray: ByteArray) = diff --git a/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileSessionPublicKeystoreTest.kt b/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileSessionPublicKeystoreTest.kt index fa34837..7c7dac3 100644 --- a/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileSessionPublicKeystoreTest.kt +++ b/src/test/kotlin/tech/relaycorp/awala/keystores/file/FileSessionPublicKeystoreTest.kt @@ -41,121 +41,131 @@ class FileSessionPublicKeystoreTest : KeystoreTestCase() { @Nested inner class Save { @Test - fun `Keystore directory should be reused if it already exists`() = runTest { - publicKeystoreRootPath.createDirectory() - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Keystore directory should be reused if it already exists`() = + runTest { + publicKeystoreRootPath.createDirectory() + val keystore = FileSessionPublicKeystore(keystoreRoot) - keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) + keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) - readKeyData() - } + readKeyData() + } @Test - fun `Keystore directory should be created if it doesn't already exist`() = runTest { - assertFalse(publicKeystoreRootPath.exists()) - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Keystore directory should be created if it doesn't already exist`() = + runTest { + assertFalse(publicKeystoreRootPath.exists()) + val keystore = FileSessionPublicKeystore(keystoreRoot) - keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) + keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) - readKeyData() - } + readKeyData() + } @Test - fun `Root directory should be created if it doesn't already exist`() = runTest { - keystoreRoot.directory.delete() - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Root directory should be created if it doesn't already exist`() = + runTest { + keystoreRoot.directory.delete() + val keystore = FileSessionPublicKeystore(keystoreRoot) - keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) + keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) - readKeyData() - } + readKeyData() + } @Test @DisabledOnOs(OS.WINDOWS) - fun `Errors creating parent directory should be wrapped`() = runTest { - keystoreRoot.directory.setExecutable(false) - keystoreRoot.directory.setWritable(false) - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Errors creating parent directory should be wrapped`() = + runTest { + keystoreRoot.directory.setExecutable(false) + keystoreRoot.directory.setWritable(false) + val keystore = FileSessionPublicKeystore(keystoreRoot) - val exception = assertThrows { - keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) - } + val exception = + assertThrows { + keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) + } - assertEquals( - "Failed to create root directory for public keys", - exception.message - ) - } + assertEquals( + "Failed to create root directory for public keys", + exception.message, + ) + } @Test - fun `New file should be created if there is no prior key for peer`() = runTest { - assertFalse(keyDataFilePath.exists()) - val creationTime = ZonedDateTime.now() - val keystore = FileSessionPublicKeystore(keystoreRoot) - - keystore.save(sessionKeyPair.sessionKey, nodeId, peerId, creationTime) - - val savedKeyData = readKeyData() - assertEquals( - sessionKeyPair.sessionKey.keyId.asList(), - savedKeyData.readBinaryData("key_id").data.asList() - ) - assertEquals( - sessionKeyPair.sessionKey.publicKey.encoded.asList(), - savedKeyData.readBinaryData("key_der").data.asList() - ) - assertEquals( - creationTime.toEpochSecond(), - savedKeyData.readInt32("creation_timestamp").toLong() - ) - } + fun `New file should be created if there is no prior key for peer`() = + runTest { + assertFalse(keyDataFilePath.exists()) + val creationTime = ZonedDateTime.now() + val keystore = FileSessionPublicKeystore(keystoreRoot) - @Test - fun `Existing file should be updated if there is a prior key for peer`() = runTest { - val now = ZonedDateTime.now() - val keystore = FileSessionPublicKeystore(keystoreRoot) - keystore.save(sessionKeyPair.sessionKey, nodeId, peerId, now.minusSeconds(1)) - val (newSessionKey) = SessionKeyPair.generate() - - keystore.save(newSessionKey, nodeId, peerId, now) - - val savedKeyData = readKeyData() - assertEquals( - newSessionKey.keyId.asList(), - savedKeyData.readBinaryData("key_id").data.asList() - ) - assertEquals( - newSessionKey.publicKey.encoded.asList(), - savedKeyData.readBinaryData("key_der").data.asList() - ) - assertEquals( - now.toEpochSecond(), - savedKeyData.readInt32("creation_timestamp").toLong() - ) - } + keystore.save(sessionKeyPair.sessionKey, nodeId, peerId, creationTime) + + val savedKeyData = readKeyData() + assertEquals( + sessionKeyPair.sessionKey.keyId.asList(), + savedKeyData.readBinaryData("key_id").data.asList(), + ) + assertEquals( + sessionKeyPair.sessionKey.publicKey.encoded + .asList(), + savedKeyData.readBinaryData("key_der").data.asList(), + ) + assertEquals( + creationTime.toEpochSecond(), + savedKeyData.readInt32("creation_timestamp").toLong(), + ) + } @Test - fun `Errors creating or updating file should be wrapped`() = runTest { - val keystore = FileSessionPublicKeystore(keystoreRoot) - // Make the read operation work but the subsequent write operation fail - keystore.save( - sessionKeyPair.sessionKey, - nodeId, - peerId, - ZonedDateTime.now().minusDays(1) - ) - keyDataFilePath.toFile().setWritable(false) - - val exception = assertThrows { - keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) + fun `Existing file should be updated if there is a prior key for peer`() = + runTest { + val now = ZonedDateTime.now() + val keystore = FileSessionPublicKeystore(keystoreRoot) + keystore.save(sessionKeyPair.sessionKey, nodeId, peerId, now.minusSeconds(1)) + val (newSessionKey) = SessionKeyPair.generate() + + keystore.save(newSessionKey, nodeId, peerId, now) + + val savedKeyData = readKeyData() + assertEquals( + newSessionKey.keyId.asList(), + savedKeyData.readBinaryData("key_id").data.asList(), + ) + assertEquals( + newSessionKey.publicKey.encoded.asList(), + savedKeyData.readBinaryData("key_der").data.asList(), + ) + assertEquals( + now.toEpochSecond(), + savedKeyData.readInt32("creation_timestamp").toLong(), + ) } - assertEquals( - "Failed to save key data to file", - exception.message - ) - assertTrue(exception.cause is IOException) - } + @Test + fun `Errors creating or updating file should be wrapped`() = + runTest { + val keystore = FileSessionPublicKeystore(keystoreRoot) + // Make the read operation work but the subsequent write operation fail + keystore.save( + sessionKeyPair.sessionKey, + nodeId, + peerId, + ZonedDateTime.now().minusDays(1), + ) + keyDataFilePath.toFile().setWritable(false) + + val exception = + assertThrows { + keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) + } + + assertEquals( + "Failed to save key data to file", + exception.message, + ) + assertTrue(exception.cause is IOException) + } private fun readKeyData() = BsonBinaryReader(ByteBuffer.wrap(keyDataFilePath.toFile().readBytes())).also { @@ -182,109 +192,128 @@ class FileSessionPublicKeystoreTest : KeystoreTestCase() { } @Test - fun `Key should be reported as missing if the file doesn't exist`() = runTest { - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Key should be reported as missing if the file doesn't exist`() = + runTest { + val keystore = FileSessionPublicKeystore(keystoreRoot) - assertThrows { keystore.retrieve(nodeId, peerId) } - } + assertThrows { keystore.retrieve(nodeId, peerId) } + } @Test @DisabledOnOs(OS.WINDOWS) // Windows can't tell apart between not-readable and non-existing - fun `Exception should be thrown if file isn't readable`() = runTest { - keyDataFilePath.toFile().createNewFile() - keyDataFilePath.toFile().setReadable(false) - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Exception should be thrown if file isn't readable`() = + runTest { + keyDataFilePath.toFile().createNewFile() + keyDataFilePath.toFile().setReadable(false) + val keystore = FileSessionPublicKeystore(keystoreRoot) - val exception = assertThrows { - keystore.retrieve(nodeId, peerId) - } + val exception = + assertThrows { + keystore.retrieve(nodeId, peerId) + } - assertEquals("Failed to read key file", exception.message) - assertTrue(exception.cause is IOException) - } + assertEquals("Failed to read key file", exception.message) + assertTrue(exception.cause is IOException) + } @Test - fun `Exception should be thrown if file is not BSON-serialized`() = runTest { - saveKeyData("not BSON".toByteArray()) - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Exception should be thrown if file is not BSON-serialized`() = + runTest { + saveKeyData("not BSON".toByteArray()) + val keystore = FileSessionPublicKeystore(keystoreRoot) - val exception = assertThrows { - keystore.retrieve(nodeId, peerId) - } + val exception = + assertThrows { + keystore.retrieve(nodeId, peerId) + } - assertEquals("Key file is malformed", exception.message) - assertTrue(exception.cause is BSONException) - } + assertEquals("Key file is malformed", exception.message) + assertTrue(exception.cause is BSONException) + } @Test - fun `Exception should be thrown if key id is missing`() = runTest { - saveKeyData { - writeBinaryData("key_der", BsonBinary(sessionKeyPair.sessionKey.publicKey.encoded)) - writeInt32("creation_timestamp", creationTimestamp) - } - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Exception should be thrown if key id is missing`() = + runTest { + saveKeyData { + writeBinaryData( + "key_der", + BsonBinary(sessionKeyPair.sessionKey.publicKey.encoded), + ) + writeInt32("creation_timestamp", creationTimestamp) + } + val keystore = FileSessionPublicKeystore(keystoreRoot) - val exception = assertThrows { - keystore.retrieve(nodeId, peerId) - } + val exception = + assertThrows { + keystore.retrieve(nodeId, peerId) + } - assertEquals("Key file is malformed", exception.message) - assertTrue(exception.cause is BSONException) - } + assertEquals("Key file is malformed", exception.message) + assertTrue(exception.cause is BSONException) + } @Test - fun `Exception should be thrown if public key is missing`() = runTest { - saveKeyData { - writeBinaryData("key_id", BsonBinary(sessionKeyPair.sessionKey.keyId)) - writeInt32("creation_timestamp", creationTimestamp) - } - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Exception should be thrown if public key is missing`() = + runTest { + saveKeyData { + writeBinaryData("key_id", BsonBinary(sessionKeyPair.sessionKey.keyId)) + writeInt32("creation_timestamp", creationTimestamp) + } + val keystore = FileSessionPublicKeystore(keystoreRoot) - val exception = assertThrows { - keystore.retrieve(nodeId, peerId) - } + val exception = + assertThrows { + keystore.retrieve(nodeId, peerId) + } - assertEquals("Key file is malformed", exception.message) - assertTrue(exception.cause is BSONException) - } + assertEquals("Key file is malformed", exception.message) + assertTrue(exception.cause is BSONException) + } @Test - fun `Exception should be thrown if creation timestamp is missing`() = runTest { - saveKeyData { - writeBinaryData("key_id", BsonBinary(sessionKeyPair.sessionKey.keyId)) - writeBinaryData("key_der", BsonBinary(sessionKeyPair.sessionKey.publicKey.encoded)) - } - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Exception should be thrown if creation timestamp is missing`() = + runTest { + saveKeyData { + writeBinaryData("key_id", BsonBinary(sessionKeyPair.sessionKey.keyId)) + writeBinaryData( + "key_der", + BsonBinary(sessionKeyPair.sessionKey.publicKey.encoded), + ) + } + val keystore = FileSessionPublicKeystore(keystoreRoot) - val exception = assertThrows { - keystore.retrieve(nodeId, peerId) - } + val exception = + assertThrows { + keystore.retrieve(nodeId, peerId) + } - assertEquals("Key file is malformed", exception.message) - assertTrue(exception.cause is BsonInvalidOperationException) - } + assertEquals("Key file is malformed", exception.message) + assertTrue(exception.cause is BsonInvalidOperationException) + } @Test - fun `Data should be returned if file exists and is valid`() = runTest { - val keystore = FileSessionPublicKeystore(keystoreRoot) - keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) + fun `Data should be returned if file exists and is valid`() = + runTest { + val keystore = FileSessionPublicKeystore(keystoreRoot) + keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) - val key = keystore.retrieve(nodeId, peerId) + val key = keystore.retrieve(nodeId, peerId) - assertEquals(sessionKeyPair.sessionKey, key) - } + assertEquals(sessionKeyPair.sessionKey, key) + } private fun saveKeyData(data: ByteArray) = keyDataFilePath.toFile().writeBytes(data) private fun saveKeyData(writeBsonFields: BsonBinaryWriter.() -> Unit) { - val bsonSerialization = BasicOutputBuffer().use { buffer -> - BsonBinaryWriter(buffer).use { - it.writeStartDocument() - writeBsonFields(it) - it.writeEndDocument() + val bsonSerialization = + BasicOutputBuffer().use { buffer -> + BsonBinaryWriter(buffer).use { + it.writeStartDocument() + writeBsonFields(it) + it.writeEndDocument() + } + buffer.toByteArray() } - buffer.toByteArray() - } saveKeyData(bsonSerialization) } } @@ -297,28 +326,31 @@ class FileSessionPublicKeystoreTest : KeystoreTestCase() { } @Test - fun `Deletion should be skipped if the root directory doesn't exist`() = runTest { - publicKeystoreRootPath.deleteExisting() - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Deletion should be skipped if the root directory doesn't exist`() = + runTest { + publicKeystoreRootPath.deleteExisting() + val keystore = FileSessionPublicKeystore(keystoreRoot) - keystore.delete(nodeId, peerId) - } + keystore.delete(nodeId, peerId) + } @Test - fun `Deletion should be skipped if the file doesn't exist`() = runTest { - val keystore = FileSessionPublicKeystore(keystoreRoot) + fun `Deletion should be skipped if the file doesn't exist`() = + runTest { + val keystore = FileSessionPublicKeystore(keystoreRoot) - keystore.delete(nodeId, peerId) - } + keystore.delete(nodeId, peerId) + } @Test - fun `File should be deleted if it exists`() = runTest { - val keystore = FileSessionPublicKeystore(keystoreRoot) - keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) + fun `File should be deleted if it exists`() = + runTest { + val keystore = FileSessionPublicKeystore(keystoreRoot) + keystore.save(sessionKeyPair.sessionKey, nodeId, peerId) - keystore.delete(nodeId, peerId) + keystore.delete(nodeId, peerId) - assertThrows { keystore.retrieve(nodeId, peerId) } - } + assertThrows { keystore.retrieve(nodeId, peerId) } + } } } diff --git a/src/test/kotlin/tech/relaycorp/awala/keystores/file/MockFilePrivateKeyStore.kt b/src/test/kotlin/tech/relaycorp/awala/keystores/file/MockFilePrivateKeyStore.kt index f27915d..f4aae92 100644 --- a/src/test/kotlin/tech/relaycorp/awala/keystores/file/MockFilePrivateKeyStore.kt +++ b/src/test/kotlin/tech/relaycorp/awala/keystores/file/MockFilePrivateKeyStore.kt @@ -11,7 +11,9 @@ import kotlin.test.assertEquals * * But it doesn't actually encrypt anything. */ -class MockFilePrivateKeyStore(keystoreRoot: FileKeystoreRoot) : FilePrivateKeyStore(keystoreRoot) { +class MockFilePrivateKeyStore( + keystoreRoot: FileKeystoreRoot, +) : FilePrivateKeyStore(keystoreRoot) { override fun makeEncryptedOutputStream(file: File): OutputStream { val stream = file.outputStream() stream.write(header) @@ -32,7 +34,7 @@ class MockFilePrivateKeyStore(keystoreRoot: FileKeystoreRoot) : FilePrivateKeySt val fileContents = file.readBytes() assertEquals( header.toString(charset), - fileContents.slice(header.indices).toByteArray().toString(charset) + fileContents.slice(header.indices).toByteArray().toString(charset), ) return fileContents.slice(header.size until fileContents.size).toByteArray() } diff --git a/src/test/kotlin/tech/relaycorp/awala/keystores/file/PrivateKeyStoreRetrievalTestCase.kt b/src/test/kotlin/tech/relaycorp/awala/keystores/file/PrivateKeyStoreRetrievalTestCase.kt index 278115e..25f7adf 100644 --- a/src/test/kotlin/tech/relaycorp/awala/keystores/file/PrivateKeyStoreRetrievalTestCase.kt +++ b/src/test/kotlin/tech/relaycorp/awala/keystores/file/PrivateKeyStoreRetrievalTestCase.kt @@ -21,37 +21,40 @@ import tech.relaycorp.relaynet.keystores.MissingKeyException abstract class PrivateKeyStoreRetrievalTestCase( private val keystoreRoot: FileKeystoreRoot, private val keyFilePath: Path, - private val retrieveMethod: suspend FilePrivateKeyStore.() -> Unit + private val retrieveMethod: suspend FilePrivateKeyStore.() -> Unit, ) { @Test - fun `Key should be reported as missing if parent directory doesn't exist`() = runTest { - assertFalse(keyFilePath.parent.exists()) - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `Key should be reported as missing if parent directory doesn't exist`() = + runTest { + assertFalse(keyFilePath.parent.exists()) + val keystore = MockFilePrivateKeyStore(keystoreRoot) - assertThrows { retrieveMethod(keystore) } - } + assertThrows { retrieveMethod(keystore) } + } @Test - fun `Key should be reported as missing if the file doesn't exist`() = runTest { - keyFilePath.parent.createDirectories() - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `Key should be reported as missing if the file doesn't exist`() = + runTest { + keyFilePath.parent.createDirectories() + val keystore = MockFilePrivateKeyStore(keystoreRoot) - assertThrows { retrieveMethod(keystore) } - } + assertThrows { retrieveMethod(keystore) } + } @Test @DisabledOnOs(OS.WINDOWS) // Windows can't tell apart between not-readable and non-existing - fun `Exception should be thrown if file isn't readable`() = runTest { - keyFilePath.parent.createDirectories() - keyFilePath.createFile() - keyFilePath.toFile().setReadable(false) - val keystore = MockFilePrivateKeyStore(keystoreRoot) - - val exception = assertThrows { retrieveMethod(keystore) } - - assertEquals("Failed to read key file", exception.message) - assertTrue(exception.cause is IOException) - } + fun `Exception should be thrown if file isn't readable`() = + runTest { + keyFilePath.parent.createDirectories() + keyFilePath.createFile() + keyFilePath.toFile().setReadable(false) + val keystore = MockFilePrivateKeyStore(keystoreRoot) + + val exception = assertThrows { retrieveMethod(keystore) } + + assertEquals("Failed to read key file", exception.message) + assertTrue(exception.cause is IOException) + } abstract fun `Private key should be returned if file exists`() } diff --git a/src/test/kotlin/tech/relaycorp/awala/keystores/file/PrivateKeyStoreSavingTestCase.kt b/src/test/kotlin/tech/relaycorp/awala/keystores/file/PrivateKeyStoreSavingTestCase.kt index 362fbd7..2ab0ea4 100644 --- a/src/test/kotlin/tech/relaycorp/awala/keystores/file/PrivateKeyStoreSavingTestCase.kt +++ b/src/test/kotlin/tech/relaycorp/awala/keystores/file/PrivateKeyStoreSavingTestCase.kt @@ -19,80 +19,88 @@ import org.junit.jupiter.api.condition.OS abstract class PrivateKeyStoreSavingTestCase( private val keystoreRoot: FileKeystoreRoot, private val keyFilePath: Path, - private val saveMethod: suspend FilePrivateKeyStore.() -> Unit + private val saveMethod: suspend FilePrivateKeyStore.() -> Unit, ) { @Test - fun `Parent subdirectory should be reused if it exists`() = runTest { - keyFilePath.parent.createDirectories() - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `Parent subdirectory should be reused if it exists`() = + runTest { + keyFilePath.parent.createDirectories() + val keystore = MockFilePrivateKeyStore(keystoreRoot) - saveMethod(keystore) + saveMethod(keystore) - assertTrue(keyFilePath.exists()) - } + assertTrue(keyFilePath.exists()) + } @Test - fun `Parent directory should be created if it doesn't exist`() = runTest { - assertFalse(keyFilePath.parent.exists()) - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `Parent directory should be created if it doesn't exist`() = + runTest { + assertFalse(keyFilePath.parent.exists()) + val keystore = MockFilePrivateKeyStore(keystoreRoot) - saveMethod(keystore) + saveMethod(keystore) - assertTrue(keyFilePath.exists()) - } + assertTrue(keyFilePath.exists()) + } @Test - fun `Root directory should be created if it doesn't exist`() = runTest { - keystoreRoot.directory.delete() - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `Root directory should be created if it doesn't exist`() = + runTest { + keystoreRoot.directory.delete() + val keystore = MockFilePrivateKeyStore(keystoreRoot) - saveMethod(keystore) + saveMethod(keystore) - assertTrue(keyFilePath.exists()) - } + assertTrue(keyFilePath.exists()) + } @Test @DisabledOnOs(OS.WINDOWS) - fun `Errors creating node subdirectory should be wrapped`() = runTest { - keystoreRoot.directory.setExecutable(false) - keystoreRoot.directory.setWritable(false) - val keystore = MockFilePrivateKeyStore(keystoreRoot) - - val exception = assertThrows { - saveMethod(keystore) + fun `Errors creating node subdirectory should be wrapped`() = + runTest { + keystoreRoot.directory.setExecutable(false) + keystoreRoot.directory.setWritable(false) + val keystore = MockFilePrivateKeyStore(keystoreRoot) + + val exception = + assertThrows { + saveMethod(keystore) + } + + assertEquals( + "Failed to create root directory for private keys", + exception.message, + ) } - assertEquals( - "Failed to create root directory for private keys", - exception.message - ) - } - @Test @DisabledOnOs(OS.WINDOWS) - fun `Errors creating or updating file should be wrapped`() = runTest { - keyFilePath.parent.createDirectories() - keyFilePath.toFile().createNewFile() - keyFilePath.toFile().setWritable(false) - val keystore = MockFilePrivateKeyStore(keystoreRoot) - - val exception = assertThrows { - saveMethod(keystore) + fun `Errors creating or updating file should be wrapped`() = + runTest { + keyFilePath.parent.createDirectories() + keyFilePath.toFile().createNewFile() + keyFilePath.toFile().setWritable(false) + val keystore = MockFilePrivateKeyStore(keystoreRoot) + + val exception = + assertThrows { + saveMethod(keystore) + } + + assertEquals("Failed to save key file", exception.message) + assertTrue(exception.cause is IOException) } - assertEquals("Failed to save key file", exception.message) - assertTrue(exception.cause is IOException) - } - @Test - fun `New file should be created if key is new`() = runTest { - assertFalse(keyFilePath.exists()) - val keystore = MockFilePrivateKeyStore(keystoreRoot) + fun `New file should be created if key is new`() = + runTest { + assertFalse(keyFilePath.exists()) + val keystore = MockFilePrivateKeyStore(keystoreRoot) - saveMethod(keystore) + saveMethod(keystore) - assertTrue(keyFilePath.exists()) - } + assertTrue(keyFilePath.exists()) + } @Test abstract fun `Private key should be stored`()