From 316a893413e961d66a50489541677d4c0cadee6b Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Sun, 15 Feb 2026 07:37:45 -0800 Subject: [PATCH] chore(coverage): increase test coverage for 0% files --- .../connectbot/sshlib/StreamForwarderTest.kt | 40 ++++ .../sshlib/crypto/TinkX25519ProviderTest.kt | 94 +++++++++ .../crypto/ed25519/Ed25519KeyFactoryTest.kt | 182 +++++++++++++++++ .../ed25519/Ed25519KeyPairGeneratorTest.kt | 75 +++++++ .../crypto/ed25519/Ed25519PrivateKeyTest.kt | 188 ++++++++++++++++++ 5 files changed, 579 insertions(+) create mode 100644 sshlib/src/test/kotlin/org/connectbot/sshlib/StreamForwarderTest.kt create mode 100644 sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/TinkX25519ProviderTest.kt create mode 100644 sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519KeyFactoryTest.kt create mode 100644 sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519KeyPairGeneratorTest.kt create mode 100644 sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519PrivateKeyTest.kt diff --git a/sshlib/src/test/kotlin/org/connectbot/sshlib/StreamForwarderTest.kt b/sshlib/src/test/kotlin/org/connectbot/sshlib/StreamForwarderTest.kt new file mode 100644 index 0000000..7bf3777 --- /dev/null +++ b/sshlib/src/test/kotlin/org/connectbot/sshlib/StreamForwarderTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 Kenny Root + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.sshlib + +import org.junit.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class StreamForwarderTest { + + @Test + fun `close delegates to stop`() { + var stopCalled = false + val forwarder = object : StreamForwarder { + override val isActive: Boolean get() = !stopCalled + override suspend fun stop() { + stopCalled = true + } + } + + assertTrue(forwarder.isActive) + forwarder.close() + assertTrue(stopCalled) + assertFalse(forwarder.isActive) + } +} diff --git a/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/TinkX25519ProviderTest.kt b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/TinkX25519ProviderTest.kt new file mode 100644 index 0000000..3c6f1de --- /dev/null +++ b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/TinkX25519ProviderTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2025 Kenny Root + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.sshlib.crypto + +import org.junit.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class TinkX25519ProviderTest { + private val provider = TinkX25519Provider() + + // RFC 7748 Section 6.1 test vectors + private val alicePrivate = hexToBytes( + "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a" + ) + private val alicePublic = hexToBytes( + "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a" + ) + private val bobPrivate = hexToBytes( + "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb" + ) + private val bobPublic = hexToBytes( + "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f" + ) + private val expectedSharedSecret = hexToBytes( + "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742" + ) + + @Test + fun `generatePrivateKey returns 32 bytes`() { + val key = provider.generatePrivateKey() + assertEquals(32, key.size) + } + + @Test + fun `generated keys are unique`() { + val key1 = provider.generatePrivateKey() + val key2 = provider.generatePrivateKey() + assert(!key1.contentEquals(key2)) { "Two generated keys should differ" } + } + + @Test + fun `publicFromPrivate matches RFC 7748 Alice test vector`() { + val publicKey = provider.publicFromPrivate(alicePrivate) + assertContentEquals(alicePublic, publicKey) + } + + @Test + fun `publicFromPrivate matches RFC 7748 Bob test vector`() { + val publicKey = provider.publicFromPrivate(bobPrivate) + assertContentEquals(bobPublic, publicKey) + } + + @Test + fun `computeSharedSecret matches RFC 7748 test vector`() { + val secret = provider.computeSharedSecret(alicePrivate, bobPublic) + assertContentEquals(expectedSharedSecret, secret) + } + + @Test + fun `shared secret is symmetric`() { + val secretAlice = provider.computeSharedSecret(alicePrivate, bobPublic) + val secretBob = provider.computeSharedSecret(bobPrivate, alicePublic) + assertContentEquals(secretAlice, secretBob) + } + + @Test + fun `generatePrivateKey roundtrip through publicFromPrivate and computeSharedSecret`() { + val priv1 = provider.generatePrivateKey() + val pub1 = provider.publicFromPrivate(priv1) + val priv2 = provider.generatePrivateKey() + val pub2 = provider.publicFromPrivate(priv2) + + val secret1 = provider.computeSharedSecret(priv1, pub2) + val secret2 = provider.computeSharedSecret(priv2, pub1) + assertContentEquals(secret1, secret2) + } + + private fun hexToBytes(hex: String): ByteArray = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() +} diff --git a/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519KeyFactoryTest.kt b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519KeyFactoryTest.kt new file mode 100644 index 0000000..5fb9c95 --- /dev/null +++ b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519KeyFactoryTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2025 Kenny Root + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.sshlib.crypto.ed25519 + +import org.junit.Test +import java.security.InvalidKeyException +import java.security.Key +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.PublicKey +import java.security.spec.InvalidKeySpecException +import java.security.spec.KeySpec +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertSame + +class Ed25519KeyFactoryTest { + private val provider = Ed25519Provider() + private val factory = KeyFactory.getInstance(Ed25519Provider.KEY_ALGORITHM, provider) + private val testSeed = ByteArray(32) { it.toByte() } + + private fun generateKeyPair(): java.security.KeyPair = Ed25519KeyPairGenerator().generateKeyPair() + + @Test + fun `generatePublic with X509EncodedKeySpec`() { + val original = generateKeyPair().public as Ed25519PublicKey + val spec = X509EncodedKeySpec(original.encoded) + val restored = factory.generatePublic(spec) + assertIs(restored) + assertEquals(original, restored) + } + + @Test + fun `generatePublic rejects non-X509EncodedKeySpec`() { + val spec = PKCS8EncodedKeySpec(ByteArray(32)) + assertFailsWith { + factory.generatePublic(spec) + } + } + + @Test + fun `generatePrivate with PKCS8EncodedKeySpec`() { + val original = Ed25519PrivateKey(testSeed) + val spec = PKCS8EncodedKeySpec(original.encoded) + val restored = factory.generatePrivate(spec) + val restoredKey = assertIs(restored) + assertContentEquals(testSeed, restoredKey.getSeed()) + } + + @Test + fun `generatePrivate rejects non-PKCS8EncodedKeySpec`() { + val spec = X509EncodedKeySpec(ByteArray(32)) + assertFailsWith { + factory.generatePrivate(spec) + } + } + + @Test + fun `getKeySpec throws`() { + val key = Ed25519PrivateKey(testSeed) + assertFailsWith { + factory.getKeySpec(key, KeySpec::class.java) + } + } + + @Test + fun `translateKey returns same Ed25519PublicKey`() { + val key = generateKeyPair().public as Ed25519PublicKey + val result = factory.translateKey(key) + assertSame(key, result) + } + + @Test + fun `translateKey returns same Ed25519PrivateKey`() { + val key = Ed25519PrivateKey(testSeed) + val result = factory.translateKey(key) + assertSame(key, result) + } + + @Test + fun `translateKey converts foreign X509 public key`() { + val original = generateKeyPair().public as Ed25519PublicKey + val foreignKey = object : PublicKey { + override fun getAlgorithm() = "EdDSA" + override fun getFormat() = "X.509" + override fun getEncoded() = original.encoded + } + val result = factory.translateKey(foreignKey) + assertIs(result) + assertEquals(original, result) + } + + @Test + fun `translateKey converts foreign PKCS#8 private key`() { + val original = Ed25519PrivateKey(testSeed) + val foreignKey = object : PrivateKey { + override fun getAlgorithm() = "EdDSA" + override fun getFormat() = "PKCS#8" + override fun getEncoded() = original.encoded + } + val resultKey = assertIs(factory.translateKey(foreignKey)) + assertContentEquals(testSeed, resultKey.getSeed()) + } + + @Test + fun `translateKey rejects public key with wrong format`() { + val foreignKey = object : PublicKey { + override fun getAlgorithm() = "EdDSA" + override fun getFormat() = "RAW" + override fun getEncoded() = ByteArray(32) + } + assertFailsWith { + factory.translateKey(foreignKey) + } + } + + @Test + fun `translateKey rejects private key with wrong format`() { + val foreignKey = object : PrivateKey { + override fun getAlgorithm() = "EdDSA" + override fun getFormat() = "RAW" + override fun getEncoded() = ByteArray(32) + } + assertFailsWith { + factory.translateKey(foreignKey) + } + } + + @Test + fun `translateKey rejects unknown key type`() { + val foreignKey = object : Key { + override fun getAlgorithm() = "Unknown" + override fun getFormat() = "Unknown" + override fun getEncoded() = ByteArray(32) + } + assertFailsWith { + factory.translateKey(foreignKey) + } + } + + @Test + fun `translateKey wraps InvalidKeySpecException from bad X509 public key`() { + val foreignKey = object : PublicKey { + override fun getAlgorithm() = "EdDSA" + override fun getFormat() = "X.509" + override fun getEncoded() = ByteArray(64) { 0xFF.toByte() } + } + assertFailsWith { + factory.translateKey(foreignKey) + } + } + + @Test + fun `translateKey wraps InvalidKeySpecException from bad PKCS#8 private key`() { + val foreignKey = object : PrivateKey { + override fun getAlgorithm() = "EdDSA" + override fun getFormat() = "PKCS#8" + override fun getEncoded() = ByteArray(64) { 0xFF.toByte() } + } + assertFailsWith { + factory.translateKey(foreignKey) + } + } +} diff --git a/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519KeyPairGeneratorTest.kt b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519KeyPairGeneratorTest.kt new file mode 100644 index 0000000..a29610b --- /dev/null +++ b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519KeyPairGeneratorTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2025 Kenny Root + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.sshlib.crypto.ed25519 + +import org.junit.Test +import java.security.SecureRandom +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class Ed25519KeyPairGeneratorTest { + private val generator = Ed25519KeyPairGenerator() + + @Test + fun `generateKeyPair returns non-null key pair`() { + val keyPair = generator.generateKeyPair() + assertNotNull(keyPair) + assertNotNull(keyPair.public) + assertNotNull(keyPair.private) + } + + @Test + fun `public key is Ed25519PublicKey`() { + val keyPair = generator.generateKeyPair() + assertIs(keyPair.public) + } + + @Test + fun `private key is Ed25519PrivateKey`() { + val keyPair = generator.generateKeyPair() + assertIs(keyPair.private) + } + + @Test + fun `public key has 32 bytes`() { + val keyPair = generator.generateKeyPair() + val pubKey = keyPair.public as Ed25519PublicKey + assertEquals(32, pubKey.getAbyte().size) + } + + @Test + fun `private key seed has 32 bytes`() { + val keyPair = generator.generateKeyPair() + val privKey = keyPair.private as Ed25519PrivateKey + assertEquals(32, privKey.getSeed().size) + } + + @Test + fun `generated key pairs are unique`() { + val keyPair1 = generator.generateKeyPair() + val keyPair2 = generator.generateKeyPair() + val pub1 = (keyPair1.public as Ed25519PublicKey).getAbyte() + val pub2 = (keyPair2.public as Ed25519PublicKey).getAbyte() + assert(!pub1.contentEquals(pub2)) { "Two generated key pairs should differ" } + } + + @Test + fun `initialize does not throw`() { + generator.initialize(256, SecureRandom()) + } +} diff --git a/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519PrivateKeyTest.kt b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519PrivateKeyTest.kt new file mode 100644 index 0000000..db7b216 --- /dev/null +++ b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519PrivateKeyTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2025 Kenny Root + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.sshlib.crypto.ed25519 + +import org.connectbot.sshlib.crypto.DerReader +import org.junit.Test +import java.math.BigInteger +import java.security.spec.InvalidKeySpecException +import java.security.spec.PKCS8EncodedKeySpec +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class Ed25519PrivateKeyTest { + private val testSeed = ByteArray(32) { it.toByte() } + + @Test + fun `seed constructor stores seed`() { + val key = Ed25519PrivateKey(testSeed) + assertContentEquals(testSeed, key.getSeed()) + } + + @Test + fun `algorithm is EdDSA`() { + val key = Ed25519PrivateKey(testSeed) + assertEquals("EdDSA", key.algorithm) + } + + @Test + fun `format is PKCS#8`() { + val key = Ed25519PrivateKey(testSeed) + assertEquals("PKCS#8", key.format) + } + + @Test + fun `getEncoded produces valid PKCS#8 DER`() { + val key = Ed25519PrivateKey(testSeed) + val encoded = key.encoded + + val reader = DerReader(encoded) + reader.readSequence { seq -> + val version = seq.readInteger() + assertEquals(BigInteger.ZERO, version) + seq.readSequence { algId -> + val oid = algId.readObjectIdentifier() + assertContentEquals(byteArrayOf(0x2b, 0x65, 0x70), oid) + } + val privateKeyOctetString = seq.readOctetString() + val innerReader = DerReader(privateKeyOctetString) + val seed = innerReader.readOctetString() + assertContentEquals(testSeed, seed) + } + } + + @Test + fun `PKCS#8 roundtrip preserves seed`() { + val original = Ed25519PrivateKey(testSeed) + val encoded = original.encoded + val restored = Ed25519PrivateKey(PKCS8EncodedKeySpec(encoded)) + assertContentEquals(testSeed, restored.getSeed()) + } + + @Test + fun `PKCS#8 constructor accepts raw 32-byte seed`() { + val key = Ed25519PrivateKey(PKCS8EncodedKeySpec(testSeed)) + assertContentEquals(testSeed, key.getSeed()) + } + + @Test + fun `PKCS#8 constructor rejects wrong version`() { + val key = Ed25519PrivateKey(testSeed) + val encoded = key.encoded.clone() + // Version field is at offset: SEQUENCE tag(1) + length(1) + INTEGER tag(1) + length(1) + value + // Find the version integer value byte and change it + val reader = DerReader(encoded) + // Manually patch version: in PKCS#8, the version INTEGER 0 is encoded as 02 01 00 + // Search for the pattern and change 00 to 01 + val versionIndex = encoded.indexOfSequence(byteArrayOf(0x02, 0x01, 0x00)) + encoded[versionIndex + 2] = 0x01 + assertFailsWith { + Ed25519PrivateKey(PKCS8EncodedKeySpec(encoded)) + } + } + + @Test + fun `PKCS#8 constructor rejects wrong OID`() { + val key = Ed25519PrivateKey(testSeed) + val encoded = key.encoded.clone() + // Ed25519 OID is 2b 65 70, change last byte + val oidIndex = encoded.indexOfSequence(byteArrayOf(0x2b, 0x65, 0x70)) + encoded[oidIndex + 2] = 0x71 + assertFailsWith { + Ed25519PrivateKey(PKCS8EncodedKeySpec(encoded)) + } + } + + @Test + fun `PKCS#8 constructor rejects wrong seed length`() { + val shortSeed = ByteArray(8) { it.toByte() } + val encoded = org.connectbot.sshlib.crypto.encodeDer { + sequence { + integer(BigInteger.ZERO) + sequence { + objectIdentifier(byteArrayOf(0x2b, 0x65, 0x70)) + } + octetString(org.connectbot.sshlib.crypto.encodeDer { octetString(shortSeed) }) + } + } + assertFailsWith { + Ed25519PrivateKey(PKCS8EncodedKeySpec(encoded)) + } + } + + @Test + fun `PKCS#8 constructor rejects malformed DER`() { + val garbage = ByteArray(64) { 0xFF.toByte() } + assertFailsWith { + Ed25519PrivateKey(PKCS8EncodedKeySpec(garbage)) + } + } + + @Test + fun `equal keys have same hashCode`() { + val key1 = Ed25519PrivateKey(testSeed.clone()) + val key2 = Ed25519PrivateKey(testSeed.clone()) + assertEquals(key1.hashCode(), key2.hashCode()) + } + + @Test + fun `equal keys are equal`() { + val key1 = Ed25519PrivateKey(testSeed.clone()) + val key2 = Ed25519PrivateKey(testSeed.clone()) + assertEquals(key1, key2) + } + + @Test + fun `different keys are not equal`() { + val key1 = Ed25519PrivateKey(testSeed) + val otherSeed = ByteArray(32) { (it + 1).toByte() } + val key2 = Ed25519PrivateKey(otherSeed) + assertNotEquals(key1, key2) + } + + @Test + fun `not equal to non-Ed25519PrivateKey`() { + val key = Ed25519PrivateKey(testSeed) + assertFalse(key.equals("not a key")) + } + + @Test + fun `destroy zeros out seed`() { + val seed = testSeed.clone() + val key = Ed25519PrivateKey(seed) + assertFalse(key.isDestroyed) + + key.destroy() + + assertTrue(key.isDestroyed) + assertContentEquals(ByteArray(32), key.getSeed()) + } + + private fun ByteArray.indexOfSequence(seq: ByteArray): Int { + outer@ for (i in 0..this.size - seq.size) { + for (j in seq.indices) { + if (this[i + j] != seq[j]) continue@outer + } + return i + } + return -1 + } +}