diff --git a/library/runtime-api/api/runtime-api.api b/library/runtime-api/api/runtime-api.api index e69de29bb..ab5df3784 100644 --- a/library/runtime-api/api/runtime-api.api +++ b/library/runtime-api/api/runtime-api.api @@ -0,0 +1,328 @@ +public abstract interface class io/matthewnelson/kmp/tor/runtime/api/Destroyable { + public abstract fun destroy ()V + public abstract fun isDestroyed ()Z +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/address/Address : java/lang/Comparable { + public final field value Ljava/lang/String; + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public abstract fun canonicalHostname ()Ljava/lang/String; + public final fun compareTo (Lio/matthewnelson/kmp/tor/runtime/api/address/Address;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public final fun equals (Ljava/lang/Object;)Z + public final fun hashCode ()I + public final fun toString ()Ljava/lang/String; +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/address/IPAddress : io/matthewnelson/kmp/tor/runtime/api/address/Address { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$Companion; + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/IPAddress$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V4 : io/matthewnelson/kmp/tor/runtime/api/address/IPAddress { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V4$Companion; + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun canonicalHostname ()Ljava/lang/String; + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V4; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V4; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V4$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V4; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V4; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V6 : io/matthewnelson/kmp/tor/runtime/api/address/IPAddress { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V6$Companion; + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun canonicalHostname ()Ljava/lang/String; + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V6; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V6; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V6$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V6; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress$V6; +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/address/OnionAddress : io/matthewnelson/kmp/tor/runtime/api/address/Address { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$Companion; + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public abstract fun asPublicKey ()Lio/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public; + public final fun canonicalHostname ()Ljava/lang/String; + public abstract fun decode ()[B + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; + public static final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; + public static final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; + public final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; + public final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3 : io/matthewnelson/kmp/tor/runtime/api/address/OnionAddress { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3$Companion; + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun asPublicKey ()Lio/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public; + public fun asPublicKey ()Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey; + public fun decode ()[B + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3; + public static final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3; + public static final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3; + public final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3; + public final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3; +} + +public class io/matthewnelson/kmp/tor/runtime/api/address/Port : java/lang/Comparable { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Companion; + public static final field MAX I + public static final field MIN I + public final field value I + public synthetic fun (ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun compareTo (Lio/matthewnelson/kmp/tor/runtime/api/address/Port;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public final fun equals (Ljava/lang/Object;)Z + public static final fun get (I)Lio/matthewnelson/kmp/tor/runtime/api/address/Port; + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/Port; + public static final fun getOrNull (I)Lio/matthewnelson/kmp/tor/runtime/api/address/Port; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/Port; + public final fun hashCode ()I + public final fun toString ()Ljava/lang/String; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/Port$Companion { + public final fun get (I)Lio/matthewnelson/kmp/tor/runtime/api/address/Port; + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/Port; + public final fun getOrNull (I)Lio/matthewnelson/kmp/tor/runtime/api/address/Port; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/Port; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy : io/matthewnelson/kmp/tor/runtime/api/address/Port { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy$Companion; + public static final field MAX I + public static final field MIN I + public synthetic fun (ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun get (I)Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy; + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy; + public static final fun getOrNull (I)Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy$Companion { + public final fun get (I)Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy; + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy; + public final fun getOrNull (I)Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/Port$Proxy; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress : io/matthewnelson/kmp/tor/runtime/api/address/Address { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress$Companion; + public final field address Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress; + public final field port Lio/matthewnelson/kmp/tor/runtime/api/address/Port; + public fun (Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress;Lio/matthewnelson/kmp/tor/runtime/api/address/Port;)V + public fun canonicalHostname ()Ljava/lang/String; + public final fun component1 ()Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress; + public final fun component2 ()Lio/matthewnelson/kmp/tor/runtime/api/address/Port; + public final fun copy (Lio/matthewnelson/kmp/tor/runtime/api/address/IPAddress;)Lio/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress; + public final fun copy (Lio/matthewnelson/kmp/tor/runtime/api/address/Port;)Lio/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress; + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/AddressKey { +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Private : io/matthewnelson/kmp/tor/runtime/api/key/Key$Private { + public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public : io/matthewnelson/kmp/tor/runtime/api/key/Key$Public, java/lang/Comparable { + public synthetic fun (Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun address ()Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; + public final fun base16 ()Ljava/lang/String; + public final fun base32 ()Ljava/lang/String; + public final fun base64 ()Ljava/lang/String; + public final fun compareTo (Lio/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public final fun encoded ()[B + public final fun equals (Ljava/lang/Object;)Z + public final fun hashCode ()I +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/AuthKey { +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/AuthKey$Private : io/matthewnelson/kmp/tor/runtime/api/key/Key$Private { + public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun descriptorBase32 (Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress;)Ljava/lang/String; + public final fun descriptorBase32 (Lio/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public;)Ljava/lang/String; + public final fun descriptorBase32OrNull (Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress;)Ljava/lang/String; + public final fun descriptorBase32OrNull (Lio/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public;)Ljava/lang/String; + public final fun descriptorBase64 (Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress;)Ljava/lang/String; + public final fun descriptorBase64 (Lio/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public;)Ljava/lang/String; + public final fun descriptorBase64OrNull (Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress;)Ljava/lang/String; + public final fun descriptorBase64OrNull (Lio/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public;)Ljava/lang/String; + protected abstract fun isCompatible (Lio/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public;)Z +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/AuthKey$Public : io/matthewnelson/kmp/tor/runtime/api/key/Key$Public { + public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun base16 ()Ljava/lang/String; + public final fun base32 ()Ljava/lang/String; + public final fun base64 ()Ljava/lang/String; + public final fun descriptorBase32 ()Ljava/lang/String; + public final fun descriptorBase64 ()Ljava/lang/String; + public final fun encoded ()[B + public final fun equals (Ljava/lang/Object;)Z + public final fun hashCode ()I +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3 : io/matthewnelson/kmp/tor/runtime/api/key/KeyType$Address { + public static final field INSTANCE Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3; + public fun algorithm ()Ljava/lang/String; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey : io/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Private { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey$Companion; + public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun algorithm ()Ljava/lang/String; + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey; + public static final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey; + public static final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey; + public final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey; + public final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PrivateKey; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey : io/matthewnelson/kmp/tor/runtime/api/key/AddressKey$Public { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey$Companion; + public fun (Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3;)V + public fun address ()Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress$V3; + public synthetic fun address ()Lio/matthewnelson/kmp/tor/runtime/api/address/OnionAddress; + public fun algorithm ()Ljava/lang/String; + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey; + public static final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey; + public static final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey; + public final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey; + public final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3$PublicKey; +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/Key : java/security/Key { + protected static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/key/Key$Companion; + public abstract fun algorithm ()Ljava/lang/String; + public abstract fun encoded ()[B + public final fun getAlgorithm ()Ljava/lang/String; + public final fun getEncoded ()[B + public final fun getFormat ()Ljava/lang/String; +} + +protected final class io/matthewnelson/kmp/tor/runtime/api/key/Key$Companion { +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/Key$Private : io/matthewnelson/kmp/tor/runtime/api/key/Key, io/matthewnelson/kmp/tor/runtime/api/Destroyable, java/security/PrivateKey { + public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun base16 ()Ljava/lang/String; + public final fun base16OrNull ()Ljava/lang/String; + public final fun base32 ()Ljava/lang/String; + public final fun base32OrNull ()Ljava/lang/String; + public final fun base64 ()Ljava/lang/String; + public final fun base64OrNull ()Ljava/lang/String; + public final fun destroy ()V + public final fun encoded ()[B + public final fun encodedOrThrow ()[B + public final fun isDestroyed ()Z + public final fun toString ()Ljava/lang/String; + protected final fun withKeyOrNull (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/Key$Public : io/matthewnelson/kmp/tor/runtime/api/key/Key, java/security/PublicKey { + public abstract fun base16 ()Ljava/lang/String; + public abstract fun base32 ()Ljava/lang/String; + public abstract fun base64 ()Ljava/lang/String; + public abstract fun encoded ()[B + public final fun toString ()Ljava/lang/String; +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/KeyType { + public abstract fun algorithm ()Ljava/lang/String; + public final fun toString ()Ljava/lang/String; +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/KeyType$Address : io/matthewnelson/kmp/tor/runtime/api/key/KeyType { +} + +public abstract class io/matthewnelson/kmp/tor/runtime/api/key/KeyType$Auth : io/matthewnelson/kmp/tor/runtime/api/key/KeyType { +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/X25519 : io/matthewnelson/kmp/tor/runtime/api/key/KeyType$Auth { + public static final field INSTANCE Lio/matthewnelson/kmp/tor/runtime/api/key/X25519; + public fun algorithm ()Ljava/lang/String; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey : io/matthewnelson/kmp/tor/runtime/api/key/AuthKey$Private { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey$Companion; + public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun algorithm ()Ljava/lang/String; + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey; + public static final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey; + public static final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey; + public final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey; + public final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PrivateKey; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey : io/matthewnelson/kmp/tor/runtime/api/key/AuthKey$Public { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey$Companion; + public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun algorithm ()Ljava/lang/String; + public static final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey; + public static final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey; + public static final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey; + public static final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey; +} + +public final class io/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey$Companion { + public final fun get (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey; + public final fun get ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey; + public final fun getOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey; + public final fun getOrNull ([B)Lio/matthewnelson/kmp/tor/runtime/api/key/X25519$PublicKey; +} + diff --git a/library/runtime-api/build.gradle.kts b/library/runtime-api/build.gradle.kts index 6dcd3740f..69c890e0c 100644 --- a/library/runtime-api/build.gradle.kts +++ b/library/runtime-api/build.gradle.kts @@ -24,6 +24,9 @@ kmpConfiguration { dependencies { api(libs.kmp.tor.core.api) implementation(libs.kmp.tor.core.resource) + implementation(libs.encoding.base16) + implementation(libs.encoding.base32) + implementation(libs.encoding.base64) } } } diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/-Stub.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/Destroyable.kt similarity index 80% rename from library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/-Stub.kt rename to library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/Destroyable.kt index 3b0adae96..0954e8f03 100644 --- a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/-Stub.kt +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/Destroyable.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://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, @@ -15,4 +15,7 @@ **/ package io.matthewnelson.kmp.tor.runtime.api -internal fun stub() { /* no-op */ } +public interface Destroyable { + public fun destroy() + public fun isDestroyed(): Boolean +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/Address.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/Address.kt new file mode 100644 index 000000000..d75dee785 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/Address.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import kotlin.jvm.JvmField + +/** + * Base abstraction for all address types + * + * @see [IPAddress] + * @see [OnionAddress] + * @see [ProxyAddress] + * */ +public sealed class Address( + @JvmField + public val value: String, +): Comparable
{ + + /** + * Returns the [value] in it's canonicalized hostname form + * + * e.g. + * + * println("127.0.0.1" + * .toIPAddressV4() + * .canonicalHostname() + * ) + * // 127.0.0.1 + * + * println("::1" + * .toIPAddressV6() + * .canonicalHostname() + * ) + * // [::1] + * + * println("http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion" + * .toOnionAddressV3() + * .canonicalHostname() + * ) + * // 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion + * + * println("http://127.0.0.1:8081/path" + * .toProxyAddress() + * .canonicalHostName() + * ) + * // 127.0.0.1 + * */ + public abstract fun canonicalHostname(): String + + public final override fun compareTo(other: Address): Int = value.compareTo(other.value) + + public final override fun equals(other: Any?): Boolean { + if (other === this) return true + if (other !is Address) return false + if (other::class != this::class) return false + return other.value == value + } + + public final override fun hashCode(): Int { + var result = 17 + result = result * 31 + this::class.hashCode() + result = result * 31 + value.hashCode() + return result + } + + public final override fun toString(): String = value +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddress.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddress.kt new file mode 100644 index 000000000..735ee6903 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddress.kt @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.kmp.tor.runtime.api.address.IPAddress.V4.Companion.toIPAddressV4OrNull +import io.matthewnelson.kmp.tor.runtime.api.address.IPAddress.V6.Companion.toIPAddressV6OrNull +import io.matthewnelson.kmp.tor.runtime.api.internal.findHostnameAndPortFromURL +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic + +/** + * Base abstraction for denoting a String value as an ip address + * */ +public sealed class IPAddress private constructor(value: String): Address(value) { + + public companion object { + + /** + * Parses a String for its IPv4 or IPv6 address. + * + * String can be either a URL containing the IP address, or the + * IPv4/IPv6 address itself. + * + * @return [IPAddress] + * @throws [IllegalArgumentException] if no IP address is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toIPAddress(): IPAddress { + return toIPAddressOrNull() + ?: throw IllegalArgumentException("$this does not contain an IP address") + } + + /** + * Parses a String for its IPv4 or IPv6 address. + * + * String can be either a URL containing the IP address, or the + * IPv4/IPv6 address itself. + * + * @return [IPAddress] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toIPAddressOrNull(): IPAddress? { + return toIPAddressV4OrNull() + ?: toIPAddressV6OrNull() + } + } + + /** + * Holder for an IPv4 address + * */ + public class V4 private constructor(value: String): IPAddress(value) { + + public override fun canonicalHostname(): String = value + + public companion object { + + /** + * Parses a String for its IPv4 address. + * + * String can be either a URL containing the IPv4 address, or the + * IPv4 address itself. + * + * @return [IPAddress.V4] + * @throws [IllegalArgumentException] if no IPv4 address is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toIPAddressV4(): V4 { + return toIPAddressV4OrNull() + ?: throw IllegalArgumentException("$this does not contain an IPv4 address") + } + + /** + * Parses a String for its IPv4 address. + * + * String can be either a URL containing the IPv4 address, or the + * IPv4 address itself. + * + * @return [IPAddress.V4] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toIPAddressV4OrNull(): V4? { + val stripped = findHostnameAndPortFromURL() + .substringBeforeLast(':') + + if (!stripped.matches(REGEX)) return null + return V4(stripped) + } + + // https://ihateregex.io/expr/ip/ + @Suppress("RegExpSimplifiable") + private val REGEX: Regex = Regex(pattern = + "(" + + "\\b25[0-5]|" + + "\\b2[0-4][0-9]|" + + "\\b[01]?[0-9][0-9]?)(\\.(25[0-5]|" + + "2[0-4][0-9]|" + + "[01]?[0-9][0-9]?)" + + "){3}" + ) + } + } + + /** + * Holder for an IPv6 address + * */ + public class V6 private constructor(value: String): IPAddress(value) { + + public override fun canonicalHostname(): String = "[$value]" + + public companion object { + + /** + * Parses a String for its IPv6 address. + * + * String can be either a URL containing the IPv6 address, or the + * IPv6 address itself. + * + * @return [IPAddress.V6] + * @throws [IllegalArgumentException] if no IPv6 address is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toIPAddressV6(): V6 { + return toIPAddressV6OrNull() + ?: throw IllegalArgumentException("$this does not contain an IPv6 address") + } + + /** + * Parses a String for its IPv6 address. + * + * String can be either a URL containing the IPv6 address, or the + * IPv6 address itself. + * + * @return [IPAddress.V6] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toIPAddressV6OrNull(): V6? { + var stripped = findHostnameAndPortFromURL() + + // is canonical with port >> [::1]:8080 + if (stripped.startsWith('[') && stripped.contains("]:")) { + stripped = stripped.substringBeforeLast(':') + } + + if (stripped.startsWith('[') && stripped.endsWith(']')) { + stripped = stripped.drop(1).dropLast(1) + } + + if (!stripped.matches(REGEX)) return null + return V6(stripped) + } + + // https://ihateregex.io/expr/ipv6/ + @Suppress("RegExpSimplifiable") + private val REGEX: Regex = Regex(pattern = + "(" + + "([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" + + "([0-9a-fA-F]{1,4}:){1,7}:|" + + "([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" + + "([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" + + "([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" + + "([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" + + "([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" + + "[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" + + ":((:[0-9a-fA-F]{1,4}){1,7}|:)|" + + "fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" + + "::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|" + + "(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + + "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|" + + "([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|" + + "(2[0-4]|" + + "1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|" + + "(2[0-4]|1{0,1}[0-9]){0,1}[0-9])" + + ")" + ) + } + } +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/OnionAddress.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/OnionAddress.kt new file mode 100644 index 000000000..587520632 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/OnionAddress.kt @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.encoding.base32.Base32 +import io.matthewnelson.encoding.base32.Base32Default +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddress.V3.Companion.toOnionAddressV3OrNull +import io.matthewnelson.kmp.tor.runtime.api.internal.findHostnameAndPortFromURL +import io.matthewnelson.kmp.tor.runtime.api.internal.stripBaseEncoding +import io.matthewnelson.kmp.tor.runtime.api.key.AddressKey +import io.matthewnelson.kmp.tor.runtime.api.key.ED25519_V3 +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic + +/** + * Base abstraction for denoting a String value as a `.onion` address + * */ +public sealed class OnionAddress private constructor(value: String): Address(value) { + + public final override fun canonicalHostname(): String = "$value.onion" + public abstract fun decode(): ByteArray + public abstract fun asPublicKey(): AddressKey.Public + + public companion object { + + /** + * Parses a String for any `.onion` address. + * + * String can be either a URL containing the `.onion` address, or the + * `.onion` address itself. + * + * Currently, only v3 `.onion` addresses are supported. + * + * @return [OnionAddress] + * @throws [IllegalArgumentException] if no `.onion` address is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toOnionAddress(): OnionAddress { + return toOnionAddressOrNull() + ?: throw IllegalArgumentException("$this does not contain an onion address") + } + + /** + * Transforms provided bytes into a `.onion` address. + * + * Currently, only v3 `.onion` addresses are supported. + * + * @return [OnionAddress] + * @throws [IllegalArgumentException] if byte array size is inappropriate + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun ByteArray.toOnionAddress(): OnionAddress { + return toOnionAddressOrNull() + ?: throw IllegalArgumentException("bytes are not an onion address") + } + + /** + * Parses a String for any `.onion` address. + * + * String can be either a URL containing the `.onion` address, or the + * `.onion` address itself. + * + * Currently, only v3 `.onion` addresses are supported. + * + * @return [OnionAddress] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toOnionAddressOrNull(): OnionAddress? { + return toOnionAddressV3OrNull() + } + + /** + * Transforms provided bytes into a `.onion` address. + * + * Currently, only v3 `.onion` addresses are supported. + * + * @return [OnionAddress] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun ByteArray.toOnionAddressOrNull(): OnionAddress? { + return toOnionAddressV3OrNull() + } + } + + /** + * Holder for a 56 character v3 onion address without the scheme or `.onion` + * appended. + * + * This is only a preliminary check for character and length correctness. + * Public key validity is not checked. + * */ + public class V3 private constructor(value: String): OnionAddress(value) { + + public override fun decode(): ByteArray = value.decodeToByteArray(Base32.Default) + public override fun asPublicKey(): ED25519_V3.PublicKey = ED25519_V3.PublicKey(this) + + public companion object { + + /** + * Parses a String for a v3 `.onion` address. + * + * String can be either a URL containing the v3 `.onion` address, or the + * v3 `.onion` address itself. + * + * @return [OnionAddress.V3] + * @throws [IllegalArgumentException] if no v3 `.onion` address is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toOnionAddressV3(): V3 { + return toOnionAddressV3OrNull() + ?: throw IllegalArgumentException("$this does not contain a v3 onion address") + } + + /** + * Transforms provided bytes into a v3 `.onion` address. + * + * @return [OnionAddress.V3] + * @throws [IllegalArgumentException] if byte array size is inappropriate + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun ByteArray.toOnionAddressV3(): V3 { + return toOnionAddressV3OrNull() + ?: throw IllegalArgumentException("bytes are not a v3 onion address") + } + + /** + * Parses a String for a v3 `.onion` address. + * + * String can be either a URL containing the v3 `.onion` address, or the + * v3 `.onion` address itself. + * + * @return [OnionAddress.V3] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toOnionAddressV3OrNull(): V3? { + val stripped = findOnionAddressFromURL() + .lowercase() + .stripBaseEncoding() + + if (!stripped.matches(REGEX)) return null + return V3(stripped) + } + + /** + * Transforms provided bytes into a v3 `.onion` address. + * + * @return [OnionAddress.V3] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun ByteArray.toOnionAddressV3OrNull(): V3? { + if (size != BYTE_SIZE) return null + + val encoded = encodeToString(Base32Default { + encodeToLowercase = true + padEncoded = false + }) + + return V3(encoded) + } + + internal const val BYTE_SIZE: Int = 35 + private val REGEX: Regex = "[${Base32.Default.CHARS_LOWER}]{56}".toRegex() + } + } +} + +@Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress") +private inline fun String.findOnionAddressFromURL(): String { + return findHostnameAndPortFromURL() + .substringBefore(':') // port + .substringBeforeLast(".onion") + .substringAfterLast('.') // subdomains +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/Port.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/Port.kt new file mode 100644 index 000000000..140055e04 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/Port.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.kmp.tor.runtime.api.address.Port.Proxy.Companion.toPortProxyOrNull +import io.matthewnelson.kmp.tor.runtime.api.internal.findHostnameAndPortFromURL +import kotlin.jvm.JvmField +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic + +/** + * Holder for a port between 0 and 65535 + * */ +public open class Port private constructor( + @JvmField + public val value: Int, +): Comparable { + + public final override fun compareTo(other: Port): Int = value.compareTo(other.value) + + public companion object { + + public const val MIN: Int = 0 + public const val MAX: Int = 65535 + + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun Int.toPort(): Port { + return toPortOrNull() + ?: throw IllegalArgumentException("$this is not a valid port") + } + + /** + * Parses a String for a port between 0 and 65535. + * + * String can be either a URL containing the port, or the + * port itself. + * + * @return [Port] + * @throws [IllegalArgumentException] if no port is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toPort(): Port { + return toPortOrNull() + ?: throw IllegalArgumentException("$this does not contain a port") + } + + @JvmStatic + @JvmName("getOrNull") + public fun Int.toPortOrNull(): Port? { + // Try Port.Proxy first (more constrained) + toPortProxyOrNull()?.let { return it } + if (this !in MIN..MAX) return null + return Port(this) + } + + /** + * Parses a String for a port between 0 and 65535. + * + * String can be either a URL containing the port, or the + * port itself. + * + * @return [Port] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toPortOrNull(): Port? { + // Try quick win first + toIntOrNull()?.let { port -> return port.toPortOrNull() } + + // Try parsing as URL + return findHostnameAndPortFromURL() + .substringAfterLast(':') + .toIntOrNull() + ?.toPortOrNull() + } + } + + /** + * A [Port] with a more constrained range of 1024 and 65535 + * */ + public class Proxy private constructor(value: Int): Port(value) { + + public companion object { + + public const val MIN: Int = 1024 + public const val MAX: Int = Port.MAX + + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun Int.toPortProxy(): Proxy { + return toPortProxyOrNull() + ?: throw IllegalArgumentException("$this is not a valid proxy port") + } + + /** + * Parses a String for a port between 1024 and 65535. + * + * String can be either a URL containing the port, or the + * port itself. + * + * @return [Port.Proxy] + * @throws [IllegalArgumentException] if no port is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toPortProxy(): Proxy { + return toPortProxyOrNull() + ?: throw IllegalArgumentException("$this does not contain a valid proxy port") + } + + @JvmStatic + @JvmName("getOrNull") + public fun Int.toPortProxyOrNull(): Proxy? { + if (this !in MIN..MAX) return null + return Proxy(this) + } + + /** + * Parses a String for a port between 1024 and 65535. + * + * String can be either a URL containing the port, or the + * port itself. + * + * @return [Port.Proxy] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toPortProxyOrNull(): Proxy? { + val port = toPortOrNull() + if (port is Proxy) return port + return null + } + } + } + + public final override fun equals(other: Any?): Boolean = other is Port && other.value == value + public final override fun hashCode(): Int = 17 * 31 + value.hashCode() + public final override fun toString(): String = value.toString() +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress.kt new file mode 100644 index 000000000..7a8aaf257 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/ProxyAddress.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.kmp.tor.runtime.api.address.IPAddress.Companion.toIPAddressOrNull +import io.matthewnelson.kmp.tor.runtime.api.address.Port.Companion.toPortOrNull +import kotlin.jvm.JvmField +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic + +/** + * Holder for a single proxy address' parts + * */ +public class ProxyAddress( + @JvmField + public val address: IPAddress, + @JvmField + public val port: Port, +): Address(address.canonicalHostname() + ':' + port) { + + public operator fun component1(): IPAddress = address + public operator fun component2(): Port = port + + public override fun canonicalHostname(): String = address.canonicalHostname() + + public fun copy(address: IPAddress): ProxyAddress = ProxyAddress(address, port) + public fun copy(port: Port): ProxyAddress = ProxyAddress(address, port) + + public companion object { + + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toProxyAddress(): ProxyAddress { + return toProxyAddressOrNull() + ?: throw IllegalArgumentException("$this is not a valid IP address & port") + } + + @JvmStatic + @JvmName("getOrNull") + public fun String.toProxyAddressOrNull(): ProxyAddress? { + val address = toIPAddressOrNull() ?: return null + val port = toPortOrNull() ?: return null + return ProxyAddress(address, port) + } + } +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/internal/-StringExt.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/internal/-StringExt.kt new file mode 100644 index 000000000..11a738559 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/internal/-StringExt.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Matthew Nelson + * + * 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 + * + * https://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. + **/ +@file:Suppress("KotlinRedundantDiagnosticSuppress") + +package io.matthewnelson.kmp.tor.runtime.api.internal + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.base32.Base32 +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.core.Decoder +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull + +@Suppress("NOTHING_TO_INLINE") +internal inline fun String.stripBaseEncoding(): String { + var limit = length + + // Disregard padding and/or whitespace from end of string + while (limit > 0) { + val c = this[limit - 1] + if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') { + break + } + limit-- + } + + return this.substring(0, limit).trimStart() +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun String.findHostnameAndPortFromURL(): String { + return substringAfter("://") // scheme + .substringAfter('@') // username:password + .substringBefore('/') // path +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun String.tryDecodeOrNull( + expectedSize: Int, + decoders: List> = listOf( + Base16, + Base32.Default, + Base64.Default, + ) +): ByteArray? { + decoders.forEach { decoder -> + val bytes = decodeToByteArrayOrNull(decoder) ?: return@forEach + if (bytes.size == expectedSize) { + return bytes + } else { + bytes.fill(0) + } + } + + return null +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AddressKey.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AddressKey.kt new file mode 100644 index 000000000..435329db7 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AddressKey.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddress + +/** + * Type definition of [Key.Public] and [Key.Private] specific to + * [OnionAddress]. + * + * @see [OnionAddress.asPublicKey] + * @see [ED25519_V3] + * */ +public class AddressKey private constructor() { + + /** + * Wrapper to extend [OnionAddress] capabilities for public key usage + * */ + public sealed class Public(private val onionAddress: OnionAddress): Key.Public(), Comparable { + + public open fun address(): OnionAddress = onionAddress + public final override fun encoded(): ByteArray = onionAddress.decode() + + public final override fun base16(): String = encoded().encodeToString(BASE_16) + public final override fun base32(): String = when (onionAddress) { + is OnionAddress.V3 -> onionAddress.value.uppercase() + } + public final override fun base64(): String = encoded().encodeToString(BASE_64) + + public final override fun compareTo(other: AddressKey.Public): Int = onionAddress.compareTo(other.onionAddress) + + public final override fun equals(other: Any?): Boolean = other is AddressKey.Public && other.onionAddress == onionAddress + public final override fun hashCode(): Int = 17 * 31 + onionAddress.hashCode() + } + + /** + * Holder for an [OnionAddress] private key + * */ + public sealed class Private(key: ByteArray): Key.Private(key) + + init { + throw IllegalStateException("AddressKey cannot be instantiated") + } +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AuthKey.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AuthKey.kt new file mode 100644 index 000000000..d4b393193 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AuthKey.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddress + +/** + * Type definition of [Key.Public] and [Key.Private] specific to + * client authentication. + * + * @see [X25519] + * */ +public class AuthKey private constructor() { + + public sealed class Public( + private val key: ByteArray + ): Key.Public() { + + // TODO: writeDescriptorToFile + + public final override fun encoded(): ByteArray = key.copyOf() + + public final override fun base16(): String = key.encodeToString(BASE_16) + public final override fun base32(): String = key.encodeToString(BASE_32) + public final override fun base64(): String = key.encodeToString(BASE_64) + + public fun descriptorBase32(): String = toDescriptor("descriptor", base32()) + public fun descriptorBase64(): String = toDescriptor("descriptor", base64()) + + public final override fun equals(other: Any?): Boolean { + if (other !is AuthKey.Public) return false + if (other::class != this::class) return false + if (other.algorithm() != algorithm()) return false + if (other.key.size != key.size) return false + + var isEqual = true + for (i in key.indices) { + if (other.key[i] != key[i]) { + isEqual = false + } + } + + return isEqual + } + + public final override fun hashCode(): Int = 17 * 31 + key.toList().hashCode() + } + + public sealed class Private(key: ByteArray): Key.Private(key) { + + // TODO: writeDescriptorToFile + + @Throws(IllegalArgumentException::class, IllegalStateException::class) + public fun descriptorBase32( + address: OnionAddress, + ): String = descriptorBase32(address.asPublicKey()) + + @Throws(IllegalArgumentException::class, IllegalStateException::class) + public fun descriptorBase32( + publicKey: AddressKey.Public, + ): String { + val result = descriptorBase32OrNull(publicKey) + if (result != null) return result + + if (publicKey.isCompatible()) { + throw IllegalStateException("isDestroyed[${isDestroyed()}]") + } + + throw IllegalArgumentException("${publicKey.algorithm()}.PublicKey is not compatible with ${algorithm()}.PrivateKey") + } + + public fun descriptorBase32OrNull( + address: OnionAddress, + ): String? = descriptorBase32OrNull(address.asPublicKey()) + + public fun descriptorBase32OrNull( + publicKey: AddressKey.Public, + ): String? { + if (!publicKey.isCompatible()) return null + + val encoded = base32OrNull() ?: return null + return toDescriptor(publicKey.address().value, encoded) + } + + @Throws(IllegalArgumentException::class, IllegalStateException::class) + public fun descriptorBase64( + address: OnionAddress, + ): String = descriptorBase64(address.asPublicKey()) + + @Throws(IllegalArgumentException::class, IllegalStateException::class) + public fun descriptorBase64( + publicKey: AddressKey.Public, + ): String { + val result = descriptorBase64OrNull(publicKey) + if (result != null) return result + + if (publicKey.isCompatible()) { + throw IllegalStateException("isDestroyed[${isDestroyed()}]") + } + + throw IllegalArgumentException("${publicKey.algorithm()}.PublicKey is not compatible with ${algorithm()}.PrivateKey") + } + + public fun descriptorBase64OrNull( + address: OnionAddress, + ): String? = descriptorBase64OrNull(address.asPublicKey()) + + public fun descriptorBase64OrNull( + publicKey: AddressKey.Public, + ): String? { + if (!publicKey.isCompatible()) return null + + val encoded = base64OrNull() ?: return null + return toDescriptor(publicKey.address().value, encoded) + } + + protected abstract fun AddressKey.Public.isCompatible(): Boolean + } + + init { + throw IllegalStateException("AuthKey cannot be instantiated") + } +} + +@Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress") +private inline fun Key.toDescriptor( + descriptor: String, + encoded: String +): String { + val sb = StringBuilder() + sb.append(descriptor) + sb.append(':') + sb.append(algorithm()) + sb.append(':') + sb.append(encoded) + + val result = sb.toString() + + if (this is Key.Private) { + // blank it (sb.clear only resets the internal index) + val size = sb.length + sb.clear() + repeat(size) { sb.append(' ') } + } + + return result +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3.kt new file mode 100644 index 000000000..25ed61a95 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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. + **/ +@file:Suppress("ClassName", "FunctionName") + +package io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddress +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddress.V3.Companion.toOnionAddressV3OrNull +import io.matthewnelson.kmp.tor.runtime.api.internal.tryDecodeOrNull +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic + +public object ED25519_V3: KeyType.Address() { + + public override fun algorithm(): String = "ED25519-V3" + + public class PublicKey(address: OnionAddress.V3): AddressKey.Public(address) { + + public override fun algorithm(): String = ED25519_V3.algorithm() + public override fun address(): OnionAddress.V3 = super.address() as OnionAddress.V3 + + public companion object { + + /** + * Parses a String for an ED25519-V3 public key. + * + * String can be a URL containing a v3 `.onion` address, the v3 `.onion` + * address itself, or Base 16/32/64 encoded raw value. + * + * @return [ED25519_V3.PublicKey] + * @throws [IllegalArgumentException] if no key is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toED25519_V3PublicKey(): PublicKey { + return toED25519_V3PublicKeyOrNull() + ?: throw IllegalArgumentException("$this is not an ${algorithm()} public key") + } + + /** + * Transforms provided bytes into a ED25519-V3 public key. + * + * @return [ED25519_V3.PublicKey] + * @throws [IllegalArgumentException] if byte array size is inappropriate + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun ByteArray.toED25519_V3PublicKey(): PublicKey { + return toED25519_V3PublicKeyOrNull() + ?: throw IllegalArgumentException("bytes are not an ${algorithm()} public key") + } + + /** + * Parses a String for an ED25519-V3 public key. + * + * String can be a URL containing a v3 `.onion` address, the v3 `.onion` + * address itself, or Base 16/32/64 encoded raw value. + * + * @return [ED25519_V3.PublicKey] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toED25519_V3PublicKeyOrNull(): PublicKey? { + var address = toOnionAddressV3OrNull() + + // If it wasn't a URL or base32 encoded, check if it's + // formatted as base16 or 64 + if (address == null) { + address = tryDecodeOrNull( + expectedSize = OnionAddress.V3.BYTE_SIZE, + decoders = listOf(Base16, Base64.Default) + )?.toOnionAddressV3OrNull() + } + + if (address == null) return null + return PublicKey(address) + } + + /** + * Transforms provided bytes into a ED25519-V3 public key. + * + * @return [ED25519_V3.PublicKey] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun ByteArray.toED25519_V3PublicKeyOrNull(): PublicKey? { + val address = toOnionAddressV3OrNull() ?: return null + return PublicKey(address) + } + } + } + + public class PrivateKey private constructor( + key: ByteArray, + ): AddressKey.Private(key) { + + public override fun algorithm(): String = ED25519_V3.algorithm() + + public companion object { + + /** + * Parses a String for an ED25519-V3 private key. + * + * String can be a Base 16/32/64 encoded raw value. + * + * @return [ED25519_V3.PrivateKey] + * @throws [IllegalArgumentException] if no key is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toED25519_V3PrivateKey(): PrivateKey { + return toED25519_V3PrivateKeyOrNull() + ?: throw IllegalArgumentException("Tried base 16/32/64 decoding, but failed to find a $BYTE_SIZE byte key") + } + + /** + * Transforms provided bytes into a ED25519-V3 private key. + * + * @return [ED25519_V3.PrivateKey] + * @throws [IllegalArgumentException] if byte array size is inappropriate + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun ByteArray.toED25519_V3PrivateKey(): PrivateKey { + return toED25519_V3PrivateKeyOrNull() + ?: throw IllegalArgumentException("Invalid key size. Must be $BYTE_SIZE bytes") + } + + /** + * Parses a String for an ED25519-V3 private key. + * + * String can be a Base 16/32/64 encoded raw value. + * + * @return [ED25519_V3.PrivateKey] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toED25519_V3PrivateKeyOrNull(): PrivateKey? { + val decoded = tryDecodeOrNull(expectedSize = BYTE_SIZE) ?: return null + return PrivateKey(decoded) + } + + /** + * Transforms provided bytes into a ED25519-V3 private key. + * + * @return [ED25519_V3.PrivateKey] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun ByteArray.toED25519_V3PrivateKeyOrNull(): PrivateKey? { + if (size != BYTE_SIZE) return null + return PrivateKey(copyOf()) + } + + private const val BYTE_SIZE = 64 + } + } +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/Key.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/Key.kt new file mode 100644 index 000000000..f0413664e --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/Key.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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. + **/ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.base32.Base32 +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.kmp.tor.runtime.api.Destroyable + +/** + * Base abstraction for Public/Private keys + * */ +public expect sealed class Key private constructor() { + + public abstract fun algorithm(): String + public abstract fun encoded(): ByteArray? + + public sealed class Public(): Key { + public abstract override fun encoded(): ByteArray + + public abstract fun base16(): String + public abstract fun base32(): String + public abstract fun base64(): String + + public final override fun toString(): String + } + + public sealed class Private(key: ByteArray): Key, Destroyable { + public final override fun destroy() + public final override fun isDestroyed(): Boolean + public final override fun encoded(): ByteArray? + + @Throws(IllegalStateException::class) + public fun encodedOrThrow(): ByteArray + + @Throws(IllegalStateException::class) + public fun base16(): String + @Throws(IllegalStateException::class) + public fun base32(): String + @Throws(IllegalStateException::class) + public fun base64(): String + + public fun base16OrNull(): String? + public fun base32OrNull(): String? + public fun base64OrNull(): String? + + protected fun withKeyOrNull(block: (key: ByteArray) -> T): T? + + public final override fun toString(): String + } + + protected companion object { + internal val BASE_16: Base16 + internal val BASE_32: Base32.Default + internal val BASE_64: Base64 + } +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/KeyType.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/KeyType.kt new file mode 100644 index 000000000..80979310c --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/KeyType.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.key + +public sealed class KeyType private constructor() { + + public abstract fun algorithm(): String + + public sealed class Address: KeyType() + public sealed class Auth: KeyType() + + // TODO: Factory functions + + public final override fun toString(): String = algorithm() +} diff --git a/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/X25519.kt b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/X25519.kt new file mode 100644 index 000000000..ec4c57556 --- /dev/null +++ b/library/runtime-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/X25519.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.kmp.tor.runtime.api.internal.tryDecodeOrNull +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic + +public object X25519: KeyType.Auth() { + + public override fun algorithm(): String = "x25519" + + public class PublicKey private constructor( + key: ByteArray + ): AuthKey.Public(key) { + + public override fun algorithm(): String = X25519.algorithm() + + public companion object { + + /** + * Parses a String for a x25519 public key. + * + * String can be a Base 16/32/64 encoded raw value. + * + * @return [X25519.PublicKey] + * @throws [IllegalArgumentException] if no key is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toX25519PublicKey(): PublicKey { + return toX25519PublicKeyOrNull() + ?: throw IllegalArgumentException("Tried base 16/32/64 decoding, but failed to find a $BYTE_SIZE byte key") + } + + /** + * Transforms provided bytes into a x25519 public key. + * + * @return [X25519.PublicKey] + * @throws [IllegalArgumentException] if byte array size is inappropriate + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun ByteArray.toX25519PublicKey(): PublicKey { + return toX25519PublicKeyOrNull() + ?: throw IllegalArgumentException("Invalid key size. Must be $BYTE_SIZE bytes") + } + + /** + * Parses a String for a x25519 public key. + * + * String can be a Base 16/32/64 encoded raw value. + * + * @return [X25519.PublicKey] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toX25519PublicKeyOrNull(): PublicKey? { + val decoded = tryDecodeOrNull(expectedSize = BYTE_SIZE) ?: return null + return PublicKey(decoded) + } + + /** + * Transforms provided bytes into a x25519 public key. + * + * @return [X25519.PublicKey] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun ByteArray.toX25519PublicKeyOrNull(): PublicKey? { + if (size != BYTE_SIZE) return null + return PublicKey(copyOf()) + } + } + } + + public class PrivateKey private constructor( + key: ByteArray + ): AuthKey.Private(key) { + + public override fun algorithm(): String = X25519.algorithm() + + public companion object { + + /** + * Parses a String for a x25519 private key. + * + * String can be a Base 16/32/64 encoded raw value. + * + * @return [X25519.PrivateKey] + * @throws [IllegalArgumentException] if no key is found + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun String.toX25519PrivateKey(): PrivateKey { + return toX25519PrivateKeyOrNull() + ?: throw IllegalArgumentException("Tried base 16/32/64 decoding, but failed to find a $BYTE_SIZE byte key") + } + + /** + * Transforms provided bytes into a x25519 private key. + * + * @return [X25519.PrivateKey] + * @throws [IllegalArgumentException] if byte array size is inappropriate + * */ + @JvmStatic + @JvmName("get") + @Throws(IllegalArgumentException::class) + public fun ByteArray.toX25519PrivateKey(): PrivateKey { + return toX25519PrivateKeyOrNull() + ?: throw IllegalArgumentException("Invalid key size. Must be $BYTE_SIZE bytes") + } + + /** + * Parses a String for a x25519 private key. + * + * String can be a Base 16/32/64 encoded raw value. + * + * @return [X25519.PrivateKey] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun String.toX25519PrivateKeyOrNull(): PrivateKey? { + val decoded = tryDecodeOrNull(expectedSize = BYTE_SIZE) ?: return null + return PrivateKey(decoded) + } + + /** + * Transforms provided bytes into a x25519 private key. + * + * @return [X25519.PrivateKey] or null + * */ + @JvmStatic + @JvmName("getOrNull") + public fun ByteArray.toX25519PrivateKeyOrNull(): PrivateKey? { + if (size != BYTE_SIZE) return null + return PrivateKey(copyOf()) + } + } + + protected override fun AddressKey.Public.isCompatible(): Boolean = when (this) { + is ED25519_V3.PublicKey -> true + } + } + + private const val BYTE_SIZE = 32 +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddressUnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddressUnitTest.kt new file mode 100644 index 000000000..59d73f1bb --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddressUnitTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.kmp.tor.runtime.api.address.IPAddress.Companion.toIPAddress +import io.matthewnelson.kmp.tor.runtime.api.address.IPAddressV4UnitTest.Companion.TEST_ADDRESSES_IPV4 +import io.matthewnelson.kmp.tor.runtime.api.address.IPAddressV6UnitTest.Companion.TEST_ADDRESSES_IPV6 +import kotlin.test.Test +import kotlin.test.assertIs + +class IPAddressUnitTest { + + @Test + fun givenIPAddress_whenIPv4_thenIsIPAddressV4() { + for (ipv4 in TEST_ADDRESSES_IPV4.lines()) { + assertIs("http://$ipv4:5050/some/path".toIPAddress()) + } + } + + @Test + fun givenIPAddress_whenIPv6_thenIsIPAddressV6() { + for (ipv6 in TEST_ADDRESSES_IPV6.lines()) { + assertIs("http://[$ipv6]:1023/some/path".toIPAddress()) + } + } +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddressV4UnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddressV4UnitTest.kt new file mode 100644 index 000000000..822cce813 --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddressV4UnitTest.kt @@ -0,0 +1,1074 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.kmp.tor.runtime.api.address.IPAddress.V4.Companion.toIPAddressV4 +import io.matthewnelson.kmp.tor.runtime.api.address.IPAddress.V4.Companion.toIPAddressV4OrNull +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class IPAddressV4UnitTest { + + @Test + fun givenIPAddressV4_whenLowestValue_thenIsSuccessful() { + "0.0.0.0".toIPAddressV4() + } + + @Test + fun givenIPAddressV4_whenTypicalLoopback_thenIsSuccessful() { + "127.0.0.1".toIPAddressV4() + } + + @Test + fun givenIPAddressV4_whenHighestValue_thenIsSuccessful() { + "255.255.255.255".toIPAddressV4() + } + + @Test + fun givenIPAddressV4_whenExceedsHighestValue_thenIsFailure() { + assertNull("256.255.255.255".toIPAddressV4OrNull()) + assertNull("255.256.255.255".toIPAddressV4OrNull()) + assertNull("255.255.256.255".toIPAddressV4OrNull()) + assertNull("255.255.255.256".toIPAddressV4OrNull()) + } + + @Test + fun givenIPAddressV4_whenRandom_thenIsSuccess() { + for (address in TEST_ADDRESSES_IPV4.lines()) { + address.toIPAddressV4() + } + } + + @Test + fun givenIPAddressV4_whenCanonicalized_thenReturnsUnderlyingValue() { + val expected = "127.0.0.1" + val actual = expected.toIPAddressV4().canonicalHostname() + assertEquals(expected, actual) + } + + @Test + fun givenIPAddressV4Url_whenFromString_thenReturnsSuccess() { + val expected = "192.168.7.111".toIPAddressV4() + val actual = "https://username:password@${expected.canonicalHostname()}:9822/some/path".toIPAddressV4() + assertEquals(expected, actual) + } + + companion object { + val TEST_ADDRESSES_IPV4 = """ + 66.250.238.47 + 245.100.3.219 + 7.193.64.71 + 177.174.197.177 + 251.38.30.74 + 1.125.102.192 + 33.65.46.41 + 49.54.195.132 + 183.53.174.180 + 160.205.57.33 + 240.228.92.15 + 36.36.76.123 + 252.115.160.95 + 66.43.112.120 + 215.181.18.22 + 111.93.238.101 + 217.150.102.111 + 89.17.208.198 + 67.139.50.9 + 77.207.252.14 + 247.216.218.215 + 98.163.8.138 + 47.119.70.74 + 78.212.46.110 + 136.44.21.95 + 173.70.172.116 + 34.8.236.174 + 123.102.51.41 + 130.170.172.254 + 20.172.174.87 + 209.68.112.62 + 23.33.73.171 + 67.67.226.188 + 57.214.163.6 + 165.222.213.187 + 91.3.101.158 + 115.188.147.244 + 156.54.132.163 + 49.69.16.120 + 240.63.201.195 + 54.25.125.217 + 78.18.49.116 + 54.221.251.164 + 32.243.126.32 + 248.68.6.29 + 225.128.5.26 + 91.166.135.54 + 255.19.93.121 + 239.148.239.211 + 82.219.123.116 + 123.130.164.84 + 169.96.150.153 + 37.110.99.97 + 72.66.177.58 + 30.18.213.9 + 211.12.115.45 + 205.83.96.118 + 31.250.166.170 + 212.193.196.127 + 67.135.169.70 + 108.50.85.203 + 122.211.142.228 + 77.27.118.80 + 219.56.74.62 + 249.211.95.180 + 151.92.177.106 + 164.227.205.47 + 186.105.134.157 + 163.154.220.52 + 113.181.62.158 + 89.253.19.212 + 161.137.206.157 + 61.140.231.237 + 48.41.124.206 + 135.101.120.47 + 83.8.99.13 + 58.39.127.216 + 203.182.27.10 + 228.145.68.70 + 62.99.58.109 + 155.101.222.165 + 53.87.134.108 + 206.160.191.214 + 167.181.45.234 + 192.235.141.192 + 3.226.30.77 + 55.76.236.255 + 105.233.149.82 + 214.136.35.146 + 15.193.153.184 + 138.60.148.131 + 253.31.120.103 + 237.79.106.249 + 171.142.115.143 + 50.26.8.167 + 97.85.37.81 + 225.145.171.54 + 75.43.28.22 + 41.49.166.206 + 57.244.115.102 + 73.160.63.54 + 208.167.234.188 + 186.138.27.94 + 133.159.69.17 + 217.238.171.206 + 98.163.165.81 + 71.75.181.231 + 94.239.254.16 + 23.102.224.36 + 48.232.74.251 + 121.149.232.182 + 74.119.109.228 + 58.27.73.47 + 215.10.87.122 + 251.237.148.148 + 3.55.163.68 + 113.68.9.47 + 164.111.223.39 + 250.29.155.40 + 219.117.71.107 + 192.127.100.175 + 131.6.192.76 + 28.68.53.123 + 149.196.18.26 + 134.230.178.50 + 243.25.90.224 + 200.199.164.216 + 129.136.213.17 + 237.97.185.102 + 216.102.166.206 + 77.171.131.14 + 162.43.17.74 + 196.18.176.12 + 244.13.255.128 + 128.23.163.153 + 129.181.239.52 + 136.32.85.151 + 12.223.7.99 + 10.244.179.6 + 254.186.73.199 + 28.189.79.23 + 32.143.138.142 + 24.116.5.32 + 217.232.113.152 + 34.18.165.223 + 87.161.232.61 + 82.19.170.251 + 214.182.26.208 + 250.246.86.105 + 148.95.82.249 + 239.51.109.149 + 99.229.248.51 + 119.126.130.66 + 58.79.128.148 + 224.160.131.219 + 99.62.20.248 + 92.53.241.167 + 190.197.133.126 + 184.169.70.49 + 122.206.131.95 + 98.85.11.127 + 253.135.15.214 + 232.215.87.38 + 49.244.36.125 + 32.97.119.252 + 144.52.91.50 + 23.240.159.166 + 220.130.225.115 + 127.2.139.184 + 98.204.167.164 + 196.3.250.172 + 222.255.225.222 + 27.170.35.135 + 240.8.85.255 + 0.214.121.218 + 116.35.132.214 + 163.212.7.127 + 183.248.127.68 + 255.21.171.191 + 220.176.104.177 + 85.167.229.131 + 9.170.130.69 + 86.73.199.103 + 111.193.243.104 + 123.57.96.0 + 163.188.206.93 + 77.204.172.188 + 83.160.137.37 + 145.90.155.77 + 179.174.255.69 + 253.11.69.246 + 154.33.238.113 + 173.148.5.229 + 220.207.123.144 + 20.127.230.20 + 215.0.113.221 + 95.49.141.113 + 26.204.205.14 + 3.46.187.39 + 174.79.160.24 + 155.105.48.161 + 154.204.73.61 + 233.169.8.160 + 84.72.78.165 + 111.61.129.49 + 237.43.216.141 + 129.48.249.20 + 89.138.175.56 + 17.166.129.253 + 176.56.236.218 + 30.201.226.91 + 99.187.222.255 + 220.33.178.80 + 16.139.91.80 + 253.213.171.238 + 9.252.27.196 + 199.222.181.72 + 9.110.164.190 + 39.185.156.37 + 209.238.253.1 + 185.103.122.143 + 69.3.131.89 + 235.91.122.247 + 189.222.95.20 + 10.82.184.242 + 175.126.54.23 + 204.75.91.83 + 55.31.165.112 + 202.92.202.250 + 196.1.200.160 + 224.75.13.82 + 193.22.228.20 + 186.242.195.7 + 234.103.242.145 + 233.232.5.30 + 7.166.189.206 + 98.87.172.201 + 252.21.180.12 + 94.54.108.20 + 26.46.64.61 + 59.245.101.2 + 93.214.35.185 + 169.176.92.2 + 23.5.243.219 + 97.51.205.3 + 212.97.255.167 + 56.210.170.231 + 126.199.168.134 + 141.251.251.50 + 158.133.237.53 + 141.175.77.141 + 51.11.129.199 + 143.17.227.169 + 69.244.96.206 + 188.106.114.167 + 199.146.60.119 + 40.143.106.111 + 80.120.187.120 + 127.205.12.250 + 204.229.63.75 + 67.91.111.187 + 120.103.107.9 + 247.255.198.185 + 141.176.3.147 + 239.134.201.45 + 164.44.63.123 + 13.0.86.161 + 144.7.197.12 + 89.54.31.224 + 18.136.33.172 + 68.248.11.200 + 101.209.102.93 + 136.243.24.33 + 149.128.104.8 + 114.252.44.181 + 78.184.121.77 + 202.151.51.169 + 179.125.32.225 + 150.194.51.242 + 109.207.146.74 + 185.203.12.196 + 131.130.162.138 + 113.237.191.127 + 110.205.87.67 + 193.100.0.170 + 169.223.141.57 + 99.213.83.72 + 59.45.1.81 + 200.176.149.157 + 19.36.207.200 + 244.74.136.167 + 153.170.182.212 + 125.149.150.246 + 232.220.98.16 + 178.231.244.58 + 160.164.16.164 + 147.125.78.135 + 124.55.103.133 + 33.171.158.139 + 44.209.94.209 + 130.160.240.69 + 237.162.244.210 + 108.3.37.170 + 167.238.153.129 + 191.96.223.178 + 203.17.85.204 + 251.152.31.10 + 27.137.142.167 + 175.200.232.193 + 101.95.189.178 + 11.196.180.248 + 254.239.27.119 + 21.194.135.155 + 85.115.183.18 + 187.232.253.224 + 132.207.41.58 + 122.136.213.12 + 142.23.189.255 + 155.206.207.213 + 37.96.188.169 + 165.67.110.120 + 140.146.110.225 + 145.129.136.243 + 196.99.95.116 + 224.226.86.37 + 250.161.237.166 + 142.42.38.107 + 209.33.251.48 + 117.69.52.213 + 144.15.215.37 + 137.201.51.62 + 137.173.77.155 + 119.58.115.58 + 255.43.11.245 + 78.184.238.102 + 130.64.206.197 + 49.26.33.217 + 200.251.240.35 + 255.157.121.20 + 141.82.82.22 + 110.61.43.181 + 243.236.49.238 + 222.125.170.60 + 155.62.46.25 + 192.193.193.152 + 118.230.243.103 + 81.157.232.254 + 55.24.50.144 + 106.46.95.228 + 66.40.12.119 + 212.207.113.170 + 140.172.90.182 + 145.184.62.247 + 16.196.3.35 + 55.137.120.121 + 95.125.125.123 + 76.80.112.47 + 118.217.243.35 + 148.198.39.174 + 20.12.216.244 + 202.49.113.82 + 90.0.108.62 + 116.110.13.8 + 25.171.44.215 + 227.208.86.244 + 228.115.23.166 + 247.211.172.65 + 176.209.100.74 + 155.87.167.151 + 58.219.149.25 + 96.110.46.214 + 26.64.210.15 + 167.138.212.82 + 50.156.120.92 + 31.75.208.95 + 36.116.231.159 + 18.246.8.87 + 153.185.100.173 + 93.121.182.252 + 34.33.162.1 + 12.187.3.53 + 25.212.74.27 + 254.19.174.145 + 173.253.103.6 + 124.217.121.254 + 25.70.165.18 + 233.211.159.108 + 79.91.40.70 + 19.167.67.79 + 83.112.97.184 + 241.103.170.243 + 238.172.36.46 + 107.204.27.78 + 224.173.34.133 + 215.65.115.158 + 102.121.61.46 + 52.119.166.155 + 180.76.107.120 + 222.225.244.179 + 87.34.88.104 + 93.33.68.170 + 179.250.0.188 + 157.68.8.196 + 76.176.132.4 + 187.243.63.36 + 34.208.234.252 + 140.77.63.143 + 130.236.248.84 + 53.52.163.89 + 87.27.241.178 + 188.93.138.150 + 26.174.212.220 + 135.14.151.200 + 76.83.249.59 + 10.36.133.23 + 180.140.6.212 + 10.20.12.6 + 75.139.126.214 + 178.214.176.112 + 70.107.185.2 + 192.169.199.35 + 19.222.186.146 + 231.200.163.188 + 197.100.67.63 + 54.27.102.244 + 186.14.78.50 + 142.224.21.17 + 80.160.42.142 + 59.147.189.245 + 96.58.210.193 + 253.43.207.33 + 177.94.254.106 + 53.69.165.80 + 253.175.42.34 + 223.246.33.25 + 23.245.108.53 + 138.218.54.177 + 173.14.184.25 + 239.6.183.70 + 197.51.193.6 + 174.144.29.10 + 38.47.128.112 + 46.247.135.234 + 48.231.14.104 + 65.127.22.7 + 210.113.172.147 + 240.69.168.160 + 239.50.91.254 + 105.191.68.39 + 201.181.27.183 + 251.118.35.232 + 228.137.161.39 + 234.165.47.135 + 240.105.196.230 + 164.225.186.20 + 60.146.86.64 + 142.50.168.120 + 178.209.246.13 + 73.38.231.46 + 110.10.231.220 + 78.201.129.125 + 96.184.20.90 + 92.104.124.189 + 171.112.122.73 + 110.97.151.16 + 150.154.125.65 + 116.213.143.222 + 26.181.132.44 + 29.173.251.174 + 37.162.190.74 + 181.7.9.248 + 238.191.189.163 + 184.196.254.75 + 31.210.50.126 + 163.111.138.243 + 244.168.33.144 + 82.70.239.253 + 76.246.132.90 + 211.206.132.231 + 26.203.18.165 + 243.149.220.55 + 39.9.167.157 + 243.227.38.95 + 193.86.66.170 + 189.21.43.78 + 207.123.106.43 + 85.124.150.120 + 54.170.37.46 + 166.252.42.32 + 179.225.22.66 + 152.234.66.93 + 28.83.134.119 + 246.232.233.196 + 111.127.170.175 + 86.216.102.241 + 69.87.196.164 + 145.79.227.150 + 2.215.200.121 + 158.90.139.239 + 97.7.198.200 + 105.245.154.53 + 13.54.181.127 + 160.201.152.121 + 244.102.70.204 + 113.42.250.66 + 232.60.210.164 + 165.250.234.109 + 59.149.12.35 + 242.96.113.91 + 74.189.246.174 + 148.40.9.190 + 31.125.27.147 + 198.223.210.120 + 11.10.220.57 + 51.82.147.59 + 62.184.18.86 + 21.234.116.234 + 177.73.22.138 + 144.245.55.1 + 143.220.214.240 + 87.25.208.205 + 178.134.87.60 + 65.211.203.122 + 149.31.165.233 + 115.187.65.239 + 246.135.237.223 + 246.77.159.202 + 87.171.47.202 + 101.56.238.114 + 97.195.241.241 + 56.151.42.211 + 210.209.243.158 + 162.220.192.87 + 179.91.39.145 + 195.88.63.45 + 157.171.75.108 + 173.47.18.69 + 125.212.227.84 + 161.9.47.47 + 63.196.160.98 + 143.246.94.193 + 209.97.13.155 + 13.216.79.21 + 72.246.206.55 + 255.95.84.47 + 204.217.195.17 + 160.178.253.199 + 3.174.104.86 + 213.15.166.236 + 81.199.158.0 + 57.61.99.208 + 106.39.75.101 + 81.97.200.123 + 179.124.208.60 + 174.0.148.57 + 79.183.178.253 + 134.229.21.43 + 163.121.144.237 + 230.49.120.143 + 42.115.194.38 + 27.157.35.87 + 185.158.251.185 + 201.189.149.198 + 64.16.210.116 + 243.133.202.60 + 86.21.59.29 + 51.248.169.184 + 170.142.175.228 + 189.232.154.93 + 250.254.225.95 + 90.234.242.149 + 187.219.15.67 + 196.30.9.26 + 114.245.58.27 + 62.164.50.19 + 34.111.50.106 + 229.166.22.100 + 184.128.252.45 + 51.177.18.179 + 65.38.109.130 + 217.247.220.37 + 57.199.53.89 + 74.183.81.72 + 100.153.148.84 + 126.60.61.90 + 94.40.28.174 + 127.22.211.143 + 157.206.253.71 + 133.226.32.116 + 122.180.196.2 + 249.126.100.193 + 13.157.166.61 + 22.36.202.58 + 247.40.33.229 + 101.164.203.62 + 237.167.143.242 + 237.76.17.31 + 44.14.67.231 + 28.110.36.78 + 41.194.74.76 + 17.0.81.125 + 248.240.120.16 + 25.35.121.212 + 21.63.20.182 + 60.95.5.186 + 190.195.140.189 + 55.44.38.174 + 64.107.69.227 + 52.238.87.159 + 98.244.12.255 + 74.199.157.105 + 113.188.136.234 + 166.9.30.246 + 54.162.156.57 + 70.253.170.195 + 86.43.163.143 + 93.207.197.50 + 187.25.7.50 + 208.33.85.50 + 215.199.43.19 + 1.142.43.113 + 23.212.162.179 + 135.89.212.87 + 182.62.252.99 + 121.79.4.212 + 89.76.196.126 + 204.6.195.156 + 29.14.184.206 + 97.251.167.76 + 121.218.97.207 + 112.83.87.217 + 248.1.145.8 + 153.190.207.226 + 51.76.190.168 + 231.119.78.215 + 22.58.198.195 + 13.161.58.93 + 74.230.175.23 + 75.181.142.191 + 70.255.137.247 + 30.188.142.12 + 66.205.40.252 + 189.35.187.145 + 149.129.27.45 + 63.198.224.156 + 72.181.237.0 + 135.165.55.214 + 120.87.170.76 + 239.17.168.215 + 79.87.82.172 + 2.163.229.221 + 202.214.65.150 + 104.173.128.87 + 107.80.38.173 + 62.66.233.252 + 19.179.78.234 + 220.233.21.11 + 178.150.191.234 + 195.244.108.40 + 129.60.127.197 + 130.17.50.244 + 30.20.105.9 + 93.190.185.143 + 103.185.188.99 + 233.202.218.69 + 51.150.0.239 + 162.15.80.125 + 9.179.173.113 + 252.28.144.154 + 61.19.141.103 + 195.149.203.33 + 142.118.196.97 + 93.83.178.44 + 148.243.197.80 + 150.44.97.224 + 233.79.181.68 + 14.228.25.23 + 33.204.175.76 + 7.149.76.96 + 44.198.106.57 + 23.62.236.176 + 52.171.223.178 + 50.251.113.168 + 117.229.168.161 + 142.171.129.4 + 254.39.55.182 + 42.47.106.88 + 197.153.154.8 + 111.155.118.94 + 25.86.62.207 + 145.86.248.219 + 223.32.75.13 + 183.36.9.106 + 114.237.38.34 + 200.148.155.101 + 38.83.206.159 + 13.154.130.102 + 125.145.195.10 + 27.98.174.145 + 238.56.139.240 + 246.15.149.213 + 200.178.55.127 + 102.159.0.189 + 160.194.70.72 + 4.59.187.116 + 167.189.58.29 + 157.41.122.163 + 54.39.20.135 + 64.26.35.165 + 127.24.137.16 + 49.7.164.90 + 16.38.160.90 + 12.87.128.238 + 98.14.34.46 + 99.100.60.76 + 132.67.45.52 + 166.170.97.23 + 133.66.28.83 + 227.191.141.9 + 176.166.47.233 + 237.17.186.254 + 93.238.65.87 + 29.37.62.42 + 50.120.107.138 + 169.207.149.161 + 14.241.39.156 + 85.3.98.111 + 131.40.22.176 + 91.146.7.95 + 205.3.65.199 + 147.153.48.193 + 41.17.182.246 + 138.183.182.31 + 146.171.255.125 + 40.215.135.151 + 51.72.158.40 + 171.159.61.235 + 0.33.38.59 + 9.78.130.187 + 105.104.227.85 + 137.102.79.101 + 152.216.7.67 + 175.253.167.142 + 73.189.4.90 + 87.172.76.248 + 199.196.216.67 + 51.226.99.69 + 99.21.111.101 + 253.46.252.255 + 135.132.185.146 + 227.106.200.241 + 205.152.37.180 + 227.122.147.10 + 182.44.31.213 + 46.96.139.243 + 95.26.22.13 + 117.91.229.210 + 245.241.195.162 + 175.156.91.23 + 25.44.147.82 + 5.170.72.146 + 185.225.248.244 + 111.167.38.37 + 33.61.214.32 + 51.228.241.211 + 154.123.80.82 + 12.241.59.149 + 165.172.191.70 + 30.62.135.45 + 84.107.77.85 + 72.99.126.137 + 141.46.194.119 + 158.34.49.252 + 66.118.220.88 + 231.178.187.143 + 203.198.247.236 + 112.236.209.42 + 112.41.242.142 + 155.162.147.61 + 137.3.43.238 + 58.189.188.45 + 65.121.39.164 + 122.7.243.34 + 143.129.4.8 + 0.122.28.53 + 84.19.169.75 + 138.156.247.211 + 13.77.180.89 + 153.69.118.37 + 202.160.139.124 + 172.40.181.90 + 222.122.87.67 + 235.15.152.228 + 129.227.137.7 + 181.74.234.169 + 185.48.26.95 + 248.18.132.58 + 55.37.20.105 + 43.121.24.113 + 22.11.96.26 + 71.78.5.46 + 105.202.246.39 + 111.172.35.31 + 43.50.68.239 + 166.163.254.216 + 81.80.175.98 + 218.175.10.172 + 255.83.108.6 + 162.161.171.160 + 159.41.12.172 + 100.79.212.210 + 154.118.50.209 + 92.72.224.252 + 39.92.1.220 + 156.17.232.63 + 54.238.97.96 + 204.76.243.94 + 62.19.55.151 + 132.37.39.52 + 125.33.244.93 + 127.218.72.82 + 160.31.240.139 + 7.64.177.81 + 110.254.246.72 + 48.33.119.50 + 171.167.217.221 + 14.248.124.111 + 54.217.160.39 + 192.98.143.110 + 219.43.232.210 + 186.254.250.117 + 117.125.66.114 + 11.146.20.12 + 178.229.75.113 + 27.74.243.96 + 201.1.199.128 + 205.199.129.166 + 189.80.52.254 + 225.140.165.188 + 142.132.186.29 + 189.247.128.116 + 82.182.189.146 + 182.75.60.21 + 224.14.177.161 + 105.183.15.200 + 133.47.51.174 + 88.160.119.254 + 165.94.51.64 + 174.17.32.46 + 79.54.37.57 + 5.233.35.58 + 18.152.222.20 + 183.220.67.196 + 38.54.153.211 + 78.242.100.109 + 134.157.227.180 + 238.14.216.141 + 217.19.111.224 + 76.84.60.211 + 120.150.164.20 + 116.136.126.14 + 242.51.139.83 + 82.116.223.58 + 227.86.119.75 + 32.184.162.227 + 162.58.67.15 + 55.137.10.56 + 178.82.54.110 + 170.105.33.176 + 169.58.160.88 + 75.149.97.213 + 188.34.107.244 + 205.96.44.214 + 168.35.163.72 + 90.51.246.22 + 18.235.213.185 + 126.97.47.205 + 122.1.242.39 + 252.170.126.43 + 234.162.41.84 + 158.252.117.114 + 131.169.205.185 + 84.49.26.160 + 155.50.194.138 + 243.29.144.216 + 119.142.161.201 + 172.2.185.18 + 9.45.227.124 + 101.41.93.32 + 164.56.254.211 + 106.237.170.151 + 11.207.13.52 + 205.255.145.247 + 156.216.45.158 + 171.250.130.5 + 101.177.22.80 + 167.56.35.207 + 139.63.224.246 + 90.249.186.113 + 183.233.194.108 + 226.106.184.240 + 76.82.72.28 + 57.139.215.143 + 68.215.5.174 + 74.200.91.134 + 84.55.119.33 + 65.8.183.99 + 251.91.92.75 + 29.196.27.227 + 243.68.198.16 + 97.89.202.133 + 23.165.213.97 + 226.81.24.169 + 80.236.213.174 + 21.10.117.147 + 210.202.123.239 + 17.110.39.1 + 132.19.19.93 + 143.143.225.210 + 180.167.124.242 + 14.36.166.215 + 79.149.235.135 + 68.125.153.134 + 201.254.118.170 + 52.216.255.93 + 191.244.189.246 + 185.217.46.231 + 201.201.214.108 + 83.44.240.249 + 169.180.196.132 + 16.239.123.110 + 44.147.78.50 + 202.154.151.97 + 180.27.123.24 + 55.88.58.171 + 136.181.170.164 + 177.36.5.159 + 146.48.81.221 + 60.25.103.118 + 201.182.27.226 + 75.242.159.25 + 93.15.22.86 + 7.216.99.100 + 208.5.188.210 + 250.171.42.54 + 132.78.0.183 + 162.7.222.189 + 110.116.82.73 + 212.51.214.101 + 24.84.151.53 + 246.70.37.36 + 225.107.191.12 + 27.73.175.234 + 216.161.203.82 + 105.75.118.220 + 132.190.212.24 + 89.191.216.75 + 202.28.226.119 + 29.115.122.75 + 40.91.122.42 + 66.109.31.176 + 66.150.107.171 + 129.187.143.111 + 239.171.180.181 + 1.5.117.104 + 73.92.180.7 + 153.151.231.182 + 4.78.42.192 + 180.248.2.210 + 89.2.158.156 + 137.27.34.46 + 33.134.219.230 + 254.255.249.62 + 241.219.50.37 + 160.54.10.216 + 134.156.178.34 + 52.148.118.231 + 196.60.250.227 + 45.193.110.103 + 76.62.71.114 + 140.75.57.127 + 175.105.105.152 + 253.215.15.191 + 146.185.126.153 + 35.7.11.173 + 204.117.58.236 + 10.83.5.78 + 75.104.252.147 + 239.226.216.114 + 85.22.219.135 + 109.37.88.141 + 155.34.125.39 + 217.112.90.128 + 53.201.193.53 + 69.221.126.158 + 117.208.144.194 + 43.155.141.6 + 31.213.80.198 + 200.34.14.16 + 19.49.249.16 + """.trimIndent() + } +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddressV6UnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddressV6UnitTest.kt new file mode 100644 index 000000000..7d18d0a17 --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/IPAddressV6UnitTest.kt @@ -0,0 +1,1069 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.kmp.tor.runtime.api.address.IPAddress.V6.Companion.toIPAddressV6 +import kotlin.test.Test +import kotlin.test.assertEquals + +class IPAddressV6UnitTest { + + @Test + fun givenIPAddressV6_whenLowestValue_thenIsSuccessful() { + "::0".toIPAddressV6() + } + + @Test + fun givenIPAddressV6_whenTypicalLoopback_thenIsSuccessful() { + "::1".toIPAddressV6() + } + + @Test + fun givenIPAddressV6_whenRandom_thenIsSuccessful() { + for (address in TEST_ADDRESSES_IPV6.lines()) { + address.toIPAddressV6() + } + } + + @Test + fun givenIPAddressV6_whenCanonicalized_thenReturnsBracketed() { + val rawAddress = "35f4:c60a:8296:4c90:79ad:3939:69d9:ba10" + val address = rawAddress.toIPAddressV6() + + val expected = "[$rawAddress]" + val actual = address.canonicalHostname() + assertEquals(expected, actual) + } + + @Test + fun givenIPAddressV6_whenBracketed_thenDropsFirstAndLast() { + val expected = "35f4:c60a:8296:4c90:79ad:3939:69d9:ba10" + val actual = "[$expected]".toIPAddressV6().value + assertEquals(expected, actual) + } + + @Test + fun givenIPAddressV6Url_whenFromString_thenReturnsSuccess() { + val expected = "35f4:c60a:8296:4c90:79ad:3939:69d9:ba10".toIPAddressV6() + val actual = "https://username:password@${expected.canonicalHostname()}:9822/some/path".toIPAddressV6() + assertEquals(expected, actual) + } + + companion object { + val TEST_ADDRESSES_IPV6 = """ + a01e:67d4:f5ac:1d66:8a17:ddc5:8a4a:190f + 203c:ea38:5e8:ca41:1730:15c0:511d:88a7 + e965:925e:3fb3:dd18:ff9:9836:67c5:a8b5 + 9f8e:9b96:c2a5:1c7e:fc9b:86fc:63c5:b607 + 6aa8:ba22:7d8d:cadd:af23:b9f8:4a83:e51a + bb1d:e56d:f3d1:fc39:bb7b:4f61:bce6:a056 + 35f4:c60a:8296:4c90:79ad:3939:69d9:ba10 + 8af7:b7ee:bba9:8840:c0:c637:dcf1:b5ad + 3dd3:a996:6a7c:65b:8a71:d1f3:1d8d:4162 + 6c:d1a3:1555:2d9a:ca0c:f2e4:215a:3039 + a873:69fe:ea0d:681e:19f6:5ab7:f1b1:b0d6 + af34:a903:357d:256b:729c:c19d:ca9d:5020 + 5fe4:f229:9256:1ae5:618f:e3fb:b552:27a0 + a991:a3e2:d1ea:c155:2651:1f08:3d05:398e + 8289:79a4:2b7d:92cb:2095:c6e5:bf78:f259 + 9055:f093:26dd:bf7d:1b31:5c3a:8dd1:1bd7 + aa6e:c274:ac18:ca4c:51a5:2633:bf27:de88 + fbb3:97b3:4e34:2c03:1ce2:8941:df8d:b8f4 + b128:c7f6:14df:2c56:c97c:ec4d:d530:3bfb + 2a3a:1223:8026:5266:bb05:cb:293e:365a + 20a2:2b4a:f717:8d95:bef2:4f86:69ec:728e + 6e50:ce63:ecb:bbae:eecb:c726:3fdd:9b2 + 6c16:f85e:e268:6936:9e02:f68:abff:1b15 + a39b:6f6f:ed51:af2d:7bd2:4381:27e5:cd3d + d047:e7bc:ef21:cb0b:5e3e:40d2:aa22:7d65 + 2b72:9a02:d45d:606b:14b8:b31:4716:88e + db33:12a0:18ca:4886:4c9d:7477:5825:d678 + ef45:3888:7d4c:25b3:87e0:4923:4f56:563a + ac95:974e:51bd:47b9:7ba6:4f36:e6d4:24db + 299:3c7f:9d5c:e6e9:f698:f312:6af9:bd32 + a1dc:263b:8bc7:a622:e77c:a9c:d5bf:a20f + 58ab:ae8a:d785:d5e4:aea7:8583:f66d:d2b0 + 2918:c0e7:2943:298f:e328:7a13:3f8a:7674 + e3a6:9456:fe16:73a7:39e:42ed:c573:611d + 729a:936:e60d:12ab:69c5:7eb0:f7ba:cf16 + 7dda:a0d7:a80:2bd7:d251:5732:e455:6229 + 16fa:476:2b93:93ee:6449:b712:e4d3:1da0 + 1759:702a:c25a:8678:371b:efa2:1e5d:c2d8 + b2dc:494d:ac2e:a3be:3652:dde8:1b5a:a256 + 9743:e1f:2c14:ee8e:b1d5:546:8656:46a9 + 334a:5e69:9af1:244:6772:a66f:19f9:502 + e4bf:66ad:66ca:d9dc:6b14:adf:34e6:7481 + d3d8:dabc:8157:f9c9:66d3:2708:e6f6:93ac + d213:e951:a900:e727:73bb:61e:98e4:a582 + f795:45c2:8445:c5e:bb76:156c:61fb:85c1 + aecc:c7d1:420f:8048:d510:cedc:82e1:7773 + 8b8c:7dd7:a6d1:d4b4:7931:5ede:fadd:4a13 + 2899:f489:d6bb:5e65:f495:c803:5e40:9023 + cc9e:1ea1:ccac:3c4b:47b9:ad89:1322:2e3 + a1ec:1049:981d:5b59:f7:aec0:a040:6e9e + 2f52:4a32:97e9:e53e:57d1:e013:2864:c3f3 + 233:9eba:769d:e653:9652:bb1d:6e58:d9fd + 35ba:7ebf:e3d3:ec35:1ee9:97da:2f4f:7757 + c0b4:f517:c0e2:1b73:1811:62f3:b430:5ef3 + 1ec5:cf77:c462:cb1:d4f0:4c93:6f2e:171 + 1faa:8f2a:ac78:9917:341a:8b8a:b370:dc3a + 1250:87e9:d6d6:8cec:3337:75e6:72fb:de68 + d78c:21ec:9d2b:9b8c:6adb:45a2:4e56:d49c + 2349:482c:5505:bbf8:dfd4:eb2b:e41:f368 + 4070:a169:d9a5:8020:eada:5271:6e43:b7d + 5d1b:a43b:2f28:f5c0:6555:8e55:d4a9:cb47 + 6b0f:577b:4ddb:1713:8dde:3c53:5283:f566 + f3ff:ab2a:7428:f131:12ac:a1c2:bc91:9eea + cce8:2992:f9b9:4e83:689e:2245:cd8d:e5ac + fa73:94ab:b578:7f83:69d8:2eb6:63c2:2894 + 9ff7:7aec:ebbd:6a5a:927:f63a:858e:5de7 + dd14:5ebb:e908:da6d:48bf:271d:868c:ee77 + eab4:529b:2e08:4d73:3529:4556:de1a:5f77 + a299:4d24:ef47:b2d5:680d:d053:a91f:1eba + 6fbe:80a7:4334:5ab:c0a8:20a7:c34b:c2b0 + a4d4:769e:bf66:3229:68f:dc10:5a3d:1061 + 2006:d897:670d:15dd:d773:68af:e71f:d383 + ab9c:e3b4:c5dc:ff71:15ef:73c7:ae45:acbb + b32c:ffec:809:b714:7562:ac2f:eb0c:458f + 916a:2d22:4adf:ca72:8cc4:f1cc:696a:5b + d04:e749:7aa2:d181:8ea3:5082:93ee:cfe4 + 8aa:6d99:d911:ae78:68f1:6610:8c7f:f760 + 1e58:20c8:ae12:a0ae:770:9609:a45a:22b3 + 18ed:43f1:a74e:b07a:e25:631c:25f8:84c6 + e2ef:6f35:c8b5:d890:5311:9755:56df:ceaa + ff25:5dfb:4cb7:a091:86da:1976:24e9:2c0e + fa93:f632:7ee3:582f:de67:f5cb:aef:cf50 + d9ce:cdc3:814a:3ad3:3add:6c37:e577:ad44 + 9256:4418:266a:5aa9:91cb:4b49:cbee:d74e + 6c63:17c3:95e:e888:8626:39d3:b973:2648 + 3f18:e324:3dcb:b03a:583d:e9a:3e46:ea2d + 867e:7500:ef49:332f:bae3:c38f:45ee:b9c9 + c6c:7b65:4dee:4952:6a05:87c5:3aa6:cd21 + 7b84:b1a4:36:3309:43ed:71cd:2a70:ade7 + f8d6:e15a:665:fcae:e4d:9bf:aea2:3aef + 31ba:15fc:c0f7:4225:b310:a046:3e71:1368 + d637:b6e1:6fc7:e680:241d:d64a:cf13:5b00 + a129:9c6f:88b4:9824:af39:33df:b423:d1bb + 2faa:6d8e:c667:1a01:9c1d:61a9:e528:e44a + 544c:8126:a25c:ca2:9f16:97d7:1f11:1770 + 4bab:3d5d:2c74:952f:d3a1:9102:91aa:f0b5 + fecc:ea8c:247b:7345:db4a:b6b1:1324:cdd + 5a26:9bb3:4af8:172d:6009:e9de:522d:a937 + 5a93:5acc:bb27:8ebc:3023:a6d5:5dc5:eb51 + 5038:181b:3fac:fba7:bc36:f76:e5b2:cbde + f230:ea79:bbaa:3dd8:1e49:26e4:cce3:1f77 + fab0:88bb:9d37:514c:6f6e:b0d9:110:211 + f056:5605:73ee:4417:2ad3:b701:8b96:c0c8 + de95:9d4a:1453:fda4:fca7:a2ad:ef59:5239 + 9f02:225a:6914:41a0:c58e:87ea:da5f:472b + 8a26:5c44:b279:5762:7b65:f7f6:f841:ba6c + 2a73:7001:1bdb:e299:3f1d:f457:9a08:24f3 + 1b2f:8bd4:8234:ef28:9d7f:f21:806a:7655 + 5095:2ac8:5bf1:50:4c94:45c1:ed0:554e + 90c1:4a16:b640:f257:5e2:48b8:9e5c:3021 + 7749:8763:d98d:de17:2f9f:1fd6:ab06:b996 + bc13:279a:65e:8937:7420:3376:afc5:54e5 + 8584:fbf4:c61a:b430:ea35:558e:ef18:26ab + 91e6:3fbc:9b7d:62de:305f:a87a:ca3e:aa70 + 7b9b:15a8:dcf0:3866:a1e2:de90:e183:76e3 + dbb2:b8f2:3728:6852:9172:51f8:e6df:183f + 2d8d:bdff:85e9:148:e8a0:1ef6:6979:81e6 + 440f:f6fa:e2e:bf13:7822:b045:7f5e:929 + 3b4d:25e6:877d:bf6:a152:b2ce:cb52:c22b + af31:4a6f:8771:8724:e53c:5fd3:b555:644 + 1cca:3739:d1a0:5eba:73f4:51f:41a:6ef1 + fad:4212:10a3:7019:77b2:64f3:3186:a40 + c778:10d2:d5:46bd:f38d:946:3f00:2bc2 + 41a9:6b17:545e:fe7b:d3db:872b:ca32:83e + dded:2d6c:3788:d46d:d91c:c9c1:a0f9:a358 + f724:1bd5:d385:f604:56d0:14b1:bef5:b02c + af87:9b6a:6e0b:457b:fd88:5501:c2da:5c6e + 2cdf:35d6:bc20:79fb:52de:2c70:7327:7b32 + 4134:c426:d28a:3dd2:1318:97b1:1b26:98b9 + c3ed:ead4:b02e:5e7b:b287:b1f9:1ed4:baaa + 4d3f:a694:28af:3d7a:ff53:a986:3e53:b83c + 152a:57e:c8a3:93f7:abf6:5f3c:e24c:3a3f + ee7f:7f88:ce22:3c45:8478:51f7:6f52:16d2 + 4bcf:13b1:213f:45f4:7dde:b596:ffd5:3c68 + 9ffc:bcbd:c41e:d99b:6113:a81b:c5fa:2964 + 9ac5:749f:947f:9980:165c:d1f4:bd7f:3b99 + 4cce:5eda:dd68:2e65:5f2a:4248:9676:524b + dde7:30e9:62f4:d99f:7c5c:98a5:6c5b:52d9 + cf7e:b55:b086:976d:ca32:292a:520d:98e1 + b57f:10f:9e11:c92a:7db8:5ab3:65:30de + 1e26:a94c:2951:fb6:9482:c6c8:dade:8ac5 + 43c3:14e:8c1a:3dd4:ab6f:f4e6:57b9:460 + 5c2f:fc3b:3f8f:fb50:6d7c:f1aa:2c1d:b75c + 5f9a:6d3c:b05f:94fe:5c1b:4b99:c5af:a9e4 + 3802:449d:b56:178b:dd:2184:8e34:478f + b447:ad87:3e83:a382:ddfc:6df2:1168:f9e0 + 80c8:1012:8f6d:7231:565a:67d5:e0f1:363f + fbe1:2672:f1a9:e556:6c02:1a8c:ecee:97e4 + 243f:3d31:b908:837e:516d:67a6:8800:3efb + d90a:9a8a:941e:29f6:e1dc:8cdd:8ae:f0e3 + 2e87:e7c1:ec28:2711:79fd:e018:cf80:1774 + 9daf:403f:5ff4:a3ab:f6d2:e0e8:ee38:77f1 + 473c:30d7:66a5:8d77:a902:c99f:2fa6:8bb9 + 2a0:2d4a:4ea6:504f:e1a1:d06f:bde7:c036 + d568:761e:61c2:2085:b9b5:ee14:e80a:e503 + 597b:ee4c:2102:6b3b:1b04:39f0:1acb:f692 + 17c1:193d:ac67:f8e9:a848:f896:1db6:5efb + a99e:5042:5ff3:e1e4:4371:4578:f4c8:c59d + 793:b22b:c730:6dd:6991:1175:1e04:77af + 79e:b1ed:3d48:2485:12c2:1d45:6bb7:c86 + 8144:5c75:83e3:c4fd:bbfd:8045:1b3f:8b3b + 772:a784:88eb:255a:f724:7bd0:395c:3964 + 60a5:bed3:95ce:c147:6c6a:8a45:af4f:c653 + 60b0:8846:67fe:d108:da0e:b422:3051:beb3 + 6bd9:b5ae:5a34:c291:8d27:4a7b:819d:280f + a465:944c:c885:43:3edc:5eaf:d7ec:51a9 + ff43:a0bf:8d0:61a9:de6f:8d54:135c:6202 + c39a:ea83:ada5:cc81:541f:832f:b731:670f + 1ed1:9f6:80a8:206e:eec9:6523:4f0b:7f38 + f166:b707:96f2:9f01:dcd9:44e8:fc7:9c18 + e7ca:44d1:54ea:cd94:f59d:ab74:ce64:a804 + c18f:9391:8e42:3fe0:777:72ff:6c9a:698 + 1514:bb9c:72a8:3b0b:e51b:ac9a:691c:2eb7 + a713:be25:1b8b:697b:736:338f:9820:fe1d + a9fd:e3b1:4e2:482e:62e4:d479:b5e6:ce0f + fcbd:6b26:94f6:3de9:665b:8edb:680d:c038 + b376:3b54:e403:677:ff0e:5cf5:4ab8:dc16 + d72f:88d8:360d:bf4d:593:53bf:9366:a94c + d254:173b:5749:bd3d:265e:f71a:20f5:833c + 8195:91bb:df03:7a86:6f30:7648:e29f:8af0 + efdb:c9a2:79fb:7b16:fa28:5507:72c9:d878 + 6f13:ab4e:8894:a9b9:af63:a55d:4f67:f5af + be0f:e1fc:c3eb:3456:29e6:57f4:a8a0:fddf + 93c4:6905:326:d744:4098:94e1:8b47:3db2 + c6fc:cad:dcf1:d9bf:5e7d:8b09:76ec:faba + f343:f038:e3e:f69a:8706:3ab8:b6da:48d9 + ff6b:aeb1:42d7:18fe:18bd:418:4cd9:370a + 2f72:2776:e61a:856a:368e:98a0:4080:76ec + f5a9:c74a:50a5:50b2:462:535:f63:bf7a + 4bc4:a1e9:880f:2243:7d3:eff3:55f3:b58e + 7b12:a75a:fa1f:4c53:41f1:d6a6:e79c:d584 + 5329:88ab:8e25:6cc:74b2:741a:95:864b + 1d2c:d73d:4686:861a:93d:7a98:b7de:28cb + 87c8:9164:ce8b:e74f:59b4:81db:2dca:8517 + 289f:9610:fc42:ba10:8d11:cc4a:9599:6722 + 503d:d75f:60:c0e5:fffd:9f84:8633:2335 + dfb4:d3c0:a28f:cbc8:ba47:3dca:32ef:ddda + 1a76:319f:ada0:fe6f:c4f9:349:360d:fa8f + 1003:1931:9f65:34c5:3186:916a:206d:6628 + 23a1:d3df:6d0e:6ed5:2712:6de4:f4fc:5a4a + 1b7f:2773:6f5c:e53d:ebc9:64ba:10a3:ee6f + 214b:f538:69c6:332a:619f:17cb:f372:8ad0 + 8974:e73c:3137:75af:b3b4:7e95:2015:5aa6 + 424c:dea4:3789:5548:5ef3:463b:3e70:b81f + 1d45:4f32:8975:ade2:97f6:4dc1:d109:79d + e160:3a1e:8158:39c9:188f:b300:8ba9:138 + a05c:a4fd:c184:911a:c466:be61:94c0:f11 + f80d:1273:b5d4:736f:f398:d8fc:fe34:8773 + cf8e:bcdf:f02c:14cf:f329:c229:ba3:39ef + b714:3143:89e4:6a27:148f:9ef5:fbea:af95 + f6b4:b7ba:3b24:378:eda5:391:47e8:6622 + 3e73:d867:767:1531:26f9:2799:d47d:f014 + 320a:69cf:45da:59d7:6bdd:de78:212d:272a + 44cc:d3e9:4918:5c05:8c78:cd52:eb99:1052 + 6d4a:b5a9:6798:470f:1167:e06:c53e:e7ff + e932:b03b:8e66:4e7e:f420:d7e2:2e45:4e8a + 1457:7a05:1471:1efd:209a:975d:66a1:8cce + 736e:26a5:f834:23:7873:1361:2c13:6ec9 + 5420:689c:d51b:188c:7eac:528a:b56c:8e2 + b8d7:b98c:5b10:9875:181f:6cb:4b16:e35e + ae09:9bee:8fea:d:74fe:23f8:91e6:affd + 5289:ad6e:5834:148e:3289:89ff:f1e0:b6c2 + 45af:72a9:4260:7595:65d1:c037:93e1:412d + 95bc:90be:5462:9f35:d9d2:44d0:c0ea:8aad + db39:5271:af12:786b:e1d0:9618:3279:88f6 + 4b45:1b71:9b95:2440:e98d:3117:263a:6248 + adb9:f9b0:8e77:a821:8554:7037:af55:fb38 + a4bd:b55c:e58c:e769:8e5:b944:1f17:5bd5 + 8631:fae4:89d2:9c45:f8b9:10ef:ddff:8ca1 + b79c:7c7a:210f:c641:7f69:fad2:f7ab:8e25 + acb9:94b6:e1f3:f82d:dfe3:6a05:6c7c:7a56 + 9e3e:43e8:bbf:1ad7:a9bb:42fd:af20:bf8d + 8de5:eab3:2674:a720:1e54:9721:e0aa:e733 + 4fc1:66d2:b59d:c792:925d:41c2:4f51:9ec + f548:ce4d:60e9:3698:31ad:d22:aad4:a404 + d765:5b0d:5157:72f6:decf:73fa:aec8:60a9 + 88cc:c600:208d:414b:8e37:f48b:e00b:b1f4 + f0df:67e0:db4:d549:ef03:3b21:1cd9:836f + 4872:9f37:d7aa:a14:16d5:c3c1:3439:2a12 + 89bf:a0ba:73c2:8766:c446:6bf6:3d66:ffec + aa0a:74fe:c8bc:7211:6706:b271:d65e:2a66 + a80e:96f7:1974:347:e92e:bcc0:633a:713a + 6865:7e54:c408:5f4d:9e9f:755a:fe3b:7a92 + fefc:84d1:b94a:a696:bf9e:aaa5:66cf:2d5d + 52dc:89a5:cc67:11e0:e1b4:ce38:b75a:8eed + c934:1c92:9fd6:bbc1:6c61:ad24:d43a:a381 + 1f5f:9a:8c61:cf51:c365:538a:d9ac:e8d7 + 346a:4:7ff7:74eb:a865:2dbb:834f:ffb8 + 2f61:ae:edae:58a8:e427:e903:d23d:1009 + d0b0:c21c:d26c:b83a:838:a33d:148d:8a5f + 74a0:d315:a3b3:2b55:5b7a:2dc7:d88b:29fe + 5497:a784:7233:4555:a0ff:4d73:27af:ce4f + 620b:762e:f415:3f58:fd72:2571:e9af:af48 + d289:c95e:9923:cdb3:cd48:7556:3a31:c0d3 + 10b8:1f55:3ebe:4a23:85c1:64f4:b6b:4ed9 + 327:4be:55cf:76d0:3b61:56f:bbc6:9ff7 + 8654:4010:96ed:18d3:885e:a3ea:1994:9a81 + fa1f:3a2b:dc90:49ee:cfea:c2e0:d482:2b01 + b138:f1a7:f51d:2389:14b8:d713:4c37:619d + ca17:82a2:3707:6e61:1f3:f5c1:3d3c:dacc + df73:beeb:9e69:7125:37f9:12b0:b42b:474e + c6f1:cf8a:bbb2:960:8eb4:37e0:fbb6:9b02 + 19ae:98ba:65a6:aaf7:bdcd:9a62:46d7:a59f + de7c:5acd:cc84:b43a:e758:dbd:61d1:4177 + 58fc:e455:37de:83da:bd51:4d4d:4ec7:e160 + 70f5:7ad8:51a7:19e:d572:3b81:44e6:4321 + fa72:b323:47c3:6c6f:d732:a64d:d5a4:15c5 + 15dd:6701:c794:fd87:bf08:71c9:d418:fc9b + 26a9:6703:e722:7ed8:104:a759:628b:c61c + 1206:c382:14ae:22:a2fb:fbc0:c1c:3db9 + e87c:6417:4863:d27a:ed25:e593:1401:acad + 39d4:1a7d:4400:999a:39a0:1be7:ba5f:bd16 + 3b84:6c60:959f:c76c:cc35:24d6:f0fb:de7d + 71a9:7503:f110:d87c:5bab:68a2:5117:440f + d5a3:80d3:d9d5:b8b6:ad15:378c:b232:9ed6 + b61b:dbae:3a9f:1f78:6b0e:8811:9053:5736 + 9d5:e296:14ee:e5c2:92d9:c954:3d0a:b3c0 + 975e:a398:a791:61a4:4bb5:8bd4:5412:7426 + 9b69:a273:9bb3:f779:2e8d:3ca5:99a:7b46 + 1c9a:d490:350c:7d1d:b0d:20d9:387d:407a + 1a2b:52e8:9708:a01d:af21:e109:2ad9:8a60 + e0a:55ef:b9ea:d7eb:4f18:ebcc:899b:4ce6 + c3eb:f61:a1ae:9df:8568:e391:2100:ed59 + bedf:c118:2207:fc72:ea1f:c85b:6220:c923 + 663:ee67:fc75:2af1:3b1b:429a:a042:5ad1 + d846:713e:3d5b:856a:a486:c840:66ff:94d4 + e751:3308:f0c2:2b6a:e902:e2c5:6ab5:7da4 + 797e:9c14:67c:45de:88c8:2d09:5757:96c7 + 7df1:c32a:b6e5:dd32:75c:d33b:bc37:87e6 + 9485:9f88:fd67:53ec:46a8:93fb:5417:294e + ea1a:d410:fe8f:7d11:8dc1:8a83:2267:61e7 + f01:c90d:854a:ed45:f6c1:7e73:602f:a12e + 416c:855f:b5f3:f86d:def1:1f2c:30d7:96f4 + e132:5862:bc6d:3da0:bc32:8d1c:459d:38c0 + c528:43d:dc13:7d85:977a:523b:8396:7dc + 36d6:bd8a:dba6:d03a:e002:327c:c607:dea1 + c2ab:b247:fa9f:15bd:9e2d:d563:d895:d530 + 555b:427f:f70e:b3a:42cc:b3fc:58fa:4c31 + b268:c098:240c:c296:11f:e35d:fd0e:106d + 4bf9:2084:b0:6b4b:c45:180b:dd32:cbdf + 49ed:2803:2989:dc14:d978:1ae0:af6e:7fcb + a4bc:8cea:5d9a:113e:cb93:dcf6:ad38:e6b9 + ab99:330e:1898:f52a:91b8:4787:8308:76b6 + ab5f:86e8:5cb8:d84c:658d:cd73:bd4e:d90d + 7c25:6b4d:8238:3a59:a8e8:a6b9:38f1:c5b + 33:d696:8050:cbbc:dcd8:35ea:e5c5:719d + a387:a56e:6e50:2ece:fc66:acf9:48aa:c51e + 35f0:b93f:dd2b:b502:c53b:9bac:e5fc:9694 + 432e:c206:a3e1:ce0c:b181:2f32:d1b5:965a + 7929:fd38:db99:7858:7a99:a18:ee57:a0c + 4efa:dc5c:6037:109b:2eb6:6aa2:f45f:d791 + 54fa:228:2028:5c8f:33f1:8594:bc1c:ace8 + 6b4e:f78f:8bda:3fd:7439:55c7:f45b:f2dc + 1e50:8275:8508:8a23:dbfa:67a:2e98:a020 + 4cbd:892f:5037:8090:7816:1c7f:932a:cd6e + 9c91:9b0c:80cd:4892:e46e:1b0d:4f2c:8910 + c83d:4b88:6c2b:2652:3d38:880c:1272:847c + 535c:82d:b577:f436:2827:f494:77c9:3b99 + 2178:9242:1803:62dc:5844:8bcc:936f:3615 + a217:10a8:62d9:795b:f7c1:9fb:9c08:e523 + 8971:3a7f:622c:a27c:6330:b852:96b:5c91 + b888:c9d8:a112:4cd0:6660:d40d:de68:62c0 + 7381:d06c:412c:9ac7:da2a:9a3e:799d:a81d + e74a:4bec:453a:7292:a1e3:c835:e34b:2a65 + f4dc:3d1b:5f3d:e83b:d737:13fc:c30d:15f9 + 565:9487:bf21:4905:1e8c:55d7:57d6:c711 + aff7:e1eb:fa0e:4400:9ff2:3338:5185:a2d5 + c47a:d681:2d63:b758:c4da:9a51:7a46:c78f + c60d:46:67da:ffd4:3fc9:f2d8:1410:a372 + 212:9ca3:8108:a846:1715:9f23:57d9:9a58 + 5d92:d5fe:a89a:2a5:4750:f924:145d:e1d6 + 6115:4eed:f118:5166:7831:ae21:d3b5:53b + 66ea:4501:d36f:b592:ef66:8ade:14ab:c164 + 1393:1ad7:459d:763c:8974:572c:9219:2cf3 + b599:a3ae:400a:c408:b882:4f09:2854:689d + 8b8b:3178:1c54:1de4:e58a:3c83:1f47:e63b + deab:33a6:6bbf:f5fd:d153:b6b0:3043:9d44 + d88b:9d4c:bbed:7b9d:f637:61cc:c67c:83ac + 74d1:dc7a:948:70c2:7b77:2190:91ac:5cb4 + 8caf:3c5a:cca9:1558:6b1a:2b21:d59:dd8e + 92b3:ad94:2be2:69d:3e2:b391:86eb:1623 + c9bf:1fe1:47e9:63a5:7616:935:c86e:db13 + b6dd:555f:e8f1:3af2:2c17:feb1:bd3a:3c61 + 9dbb:874b:6c3:1d7c:4e8a:cc94:12ee:4364 + c032:81cd:909c:c0e9:eb21:4949:b5d7:c5da + 5983:2475:75c4:191:4470:9f19:7a25:13d5 + 972e:ee56:b498:af2a:4da3:e3a4:26e:2da1 + 21e9:346a:8616:b3a0:1961:3b78:c70:ab2c + 8bb3:40f1:fd85:a6f5:201b:d762:aa0c:29a5 + f7f6:4f26:31aa:21dd:86ff:30f8:f108:3efb + efd3:44ea:a22d:6e9d:a153:2aeb:814a:9975 + b69e:e394:541f:50de:ce8a:84aa:5454:407 + 7ba6:1e27:47a:12b9:c8c9:cf84:944d:89d9 + c44b:420:441a:8dae:246b:c3d7:62fd:8a03 + 126a:3ff6:63a3:9928:c302:4c3f:ea95:3ec9 + 2265:a18f:1f1:19c4:22ff:d60e:8d4c:7650 + 17b7:f4:14:d78f:f80b:4916:8942:822d + 47ac:22b9:aa59:bbf4:bb45:d63e:70ec:8d58 + ff7d:3be4:f02b:a8fa:6a88:201f:a420:33ba + 282b:36a8:df8f:4050:e723:98e:bb9a:d1a + 5d14:ac97:775e:6a76:89ec:1784:f86a:82cf + 88a5:a716:2f8d:32e2:608c:533e:45dc:4468 + 216f:8ae5:ede1:4726:65a4:c9d8:704a:690a + af3d:a1e7:dac1:273c:929e:4ec7:b698:a32e + 9cfc:1213:79f:2c2c:b9b7:8d77:d66f:ff9 + 47c5:66e7:2d2:995f:c42:e865:5b1:d5ff + 85c9:f22f:1482:898c:a029:67fe:7db7:ad93 + 6c04:c787:a436:d129:8789:e940:4a48:fb49 + 9bcb:e56a:2285:d308:f625:bbfe:b4be:fc6d + 267d:82a5:134c:f5e3:3388:81de:cb51:2b61 + 208a:b2a2:4c4d:70b5:d406:df98:ac45:a4d0 + 646f:166:2670:d0d2:3fc2:d8e4:61ea:bf68 + 4134:1b05:1c11:c058:6594:623:ba4b:2a46 + 40e0:eba4:4ece:3304:9619:ccf5:ffc7:18ed + 5749:12bc:c5b9:9c77:2512:8562:4585:bca6 + d93f:b026:8dfe:4210:c755:a1fa:9338:417b + d3f:d4b7:601f:cfc4:3cd0:a48c:800d:90fb + 3190:ba08:85dc:277c:d642:9f77:57db:7b1f + 3f91:4ae6:b80c:9495:272e:6fba:6bff:63a3 + 5a13:5a5b:9f86:4128:e2c4:520b:caa0:c22f + c47f:ddc8:7bdf:b963:a834:b03e:61f1:4d4a + afd0:ddd4:cdb7:5697:3340:5dfe:5cdd:57fb + 11cc:c1b9:5f7e:7f55:6712:3db:8e1d:dd39 + 51fb:9e5a:ea09:87e:4aba:75e2:1a2b:5782 + 1784:dc6b:580b:ca99:870e:49a:5c04:aa8b + bb61:b4b9:f3d:f0c2:d3a4:7324:6ce8:4f4f + fd95:b5e9:80e6:13fe:7533:cf3:7bdc:9336 + 3ff8:22e0:eddb:7e17:2b77:dde4:af6d:76fe + 7edf:682:e1e4:b267:ae82:af52:e5e8:a9fa + cc8a:dde:d169:e3a:1d01:2ab0:8b19:784c + 9b0d:e581:7aa9:d92:dc11:78a9:7fd9:caa8 + f70:2cc6:1e80:f20e:21de:8b4b:b688:4220 + b87c:8147:ac8a:13a9:3c58:6b5a:9cd8:f473 + 58f:57dd:9c83:f2a4:c031:f0e8:e614:ff1e + d419:c99a:5e60:3c5d:77fa:2756:e73d:e929 + 2cae:c15f:2516:b5d4:532a:a2e6:5a98:5700 + aad0:f215:b30:bca2:6c9b:995c:d1ca:7b72 + dea2:fad4:db74:eb3c:1172:34f4:2ab5:1593 + 2fe8:ac9d:2778:9884:651a:359:2a69:9825 + 1b6b:6a00:1f6c:efe1:64f:3a86:5481:d0de + aa13:8687:4992:5919:700:bceb:7e4e:e8f3 + 2660:fc53:9d53:8226:3ed6:e49f:7fb9:e767 + 6f3:1e81:559b:f4c4:9f00:4b39:3056:e074 + e179:1155:c6db:75ad:f1e8:8ee3:71f3:6080 + a04d:5824:3fbd:2ed4:35d5:332e:1898:9952 + 1994:ae92:58ee:80b7:9d9a:e5ed:9f3d:1796 + bf44:6d0:5e5:dc19:227:170e:1758:e392 + 97c5:d3f0:e9fa:9bc6:fae5:d28a:7752:cf39 + 2a80:746a:886a:f8bf:8d7:a6e6:4a25:1ecf + c2f6:85c9:64d7:4333:c445:a209:16ef:17bd + 6c88:9aaf:ad69:8e47:cbb3:8605:a77:8fa3 + be3c:e3ce:1d36:f199:2262:e74a:72f9:8b0d + ea12:a0e1:f677:fca:12aa:856:fe78:bb8d + 9a45:f49d:d386:c867:9cb8:1ca7:e593:b8bd + 48d3:1e18:9ab:d7e8:5fd3:1f93:c489:325 + 2887:b029:f33d:8db:efab:1935:7072:4a6f + 8bef:6829:63ad:a621:eb62:c649:2964:155e + b6be:7789:4810:7d72:5e74:90ba:a383:bb78 + ea09:7673:3859:d16c:8afd:cd2e:18a6:2e74 + 6cd4:fda2:435:83c:7ff6:345e:9b3e:a03e + 9d4b:95be:c29e:a85f:338d:7a50:2edb:a43f + 1b5f:3c25:4251:d8f7:3941:2ac:5e68:18f0 + 799f:6a4a:6040:aa4b:5fbc:542:867f:645d + 9d7c:5d11:77bf:983b:4c3c:1305:d51c:779e + bb39:2e1f:67c7:7c07:5856:bdb7:2ddf:6057 + 9cca:69af:adf4:dce3:d96d:bb69:8ed7:7e14 + 997c:7116:595d:39f:781a:d085:f6bd:999f + 5eef:ffaf:4ff:41ee:426:c12b:fb25:ee9f + 2681:e7ed:7d3c:1879:3288:525e:c96f:a0d5 + 4a42:e6c3:74a8:3d5e:4dc5:5912:b6d8:8840 + 67a8:4f68:7da0:91ee:66fb:1cd7:ab9b:96d7 + 6fc3:8f04:c0f5:bf86:f27f:13a6:db5e:7a71 + aab:e355:6932:eb6b:19c7:24bf:d8da:40df + 8e1a:7a3c:4aaa:f807:d08d:737a:42f:b9ef + a378:5c42:9f0b:5ba3:3d59:cf2e:8d55:3e95 + 1567:97d3:5542:206a:34c6:8d87:fc9e:affd + 30df:8af7:55b4:2950:bb98:5af:2c27:df5a + 2110:d5e6:730b:763e:6c90:c968:fa1a:fc14 + e6a:b0a9:f4ab:5f2a:834c:ddd4:8256:f423 + 4baf:bf27:605b:8cf:5e47:2272:c4ec:167e + f11d:e5ed:8f68:79a:4754:a50c:dde4:7a5b + 43ef:dec4:bd24:4677:8a41:cf7e:7cc:6255 + 7a92:c746:af95:8a3:2a39:38a5:b048:fae7 + 7422:b3a7:8d42:99dd:cfc6:e888:b29e:dbf4 + 9d97:d43:8afd:de4f:244f:bb:810a:57d6 + 454d:3048:8970:564c:c16b:c08b:b32e:1d22 + 2f81:d437:d297:262e:c39b:aeb0:a09a:35aa + b18d:20d5:b315:78fc:e985:889a:1933:60 + c70f:d1c2:5b99:9121:f28f:cd21:36b5:88bd + 480f:5cfe:f2bd:ae12:af68:e1f6:5702:7f96 + ce3a:a808:760b:3ce0:5d51:1fd1:225c:84f8 + b112:f493:3b1a:a0cb:e584:26c7:fc6:f8c4 + 9e23:bd4b:72ea:ae86:c704:6674:4cbb:76d1 + 6d70:dad8:8752:bd9e:ea44:ceaf:8409:81e5 + 7c:fa2d:32cc:2e1d:6509:8633:634:646a + 4bf4:e34c:feb7:5e0b:21c6:85a5:31f:5645 + ab0a:2be4:ce63:fac5:be11:122d:7364:86d3 + 815b:a66:3547:3e79:39ba:f0f4:c362:ae95 + 8c3b:a1a:bbc6:eaf5:481b:7d79:dcbb:777a + bd66:310f:f8ed:e4bf:4dff:a105:2156:2045 + cb75:40d8:9b3f:aeb7:c6d9:ec58:4ed1:b4b4 + 44f1:6121:1e7f:31f2:825:8ca6:280f:ff67 + 33bb:c138:7ce3:e593:d358:2ae0:d066:8dda + 3322:3025:8b14:bcfc:30b6:afb4:b7d8:6ac1 + 7cba:7e15:2714:391e:93fd:23b7:cc24:6ee9 + 2816:6558:a705:8f11:40f1:1b67:ce46:6ff7 + 2fef:1113:d025:601a:e935:e01a:cea1:91db + ce62:c480:472d:10ba:e043:83b3:94c7:8a6 + e178:1b89:44a8:e382:f9ab:ab9e:73fe:fdaa + 5f7b:3e16:8d7:d3e0:56fa:a9ef:b5ce:949a + 744:40da:781e:1ad9:ec66:d189:3abd:5fad + 581:717c:3e28:6e34:b175:4cec:5b17:a8dd + 2f39:e64c:baa8:a42d:e7d1:4b8f:1c9d:a3a6 + 5201:e1ca:4bf:4c36:1ab4:30a1:7fdd:5ae5 + c4e1:ec50:d9a2:88ae:695e:c855:6750:8d16 + d779:f427:4f6c:f616:5693:38a3:a3b4:e525 + a0a3:13df:d27e:259f:525e:ce00:bf44:a07f + 7330:80a9:db:1675:53fe:8201:a6fe:12ba + 3730:dbbb:4333:cb89:7dde:dffb:5e84:825d + 22f9:93f4:9102:5796:90a:ce15:718:7bef + 5e60:b0f9:f33a:d7d8:f4b3:3fa0:c9fa:4704 + a77a:578b:dbd1:afb1:e6e:1539:c66c:ca1e + 9e2c:e00d:b0ff:818f:c14e:1067:8059:9f6e + 4be3:53a0:588b:ba5:180f:1984:2ddd:9f02 + bafc:e511:45ee:7d16:75a6:b739:4523:7884 + 49eb:5fb2:ea83:2851:ad18:9fba:a295:8a79 + af8e:3e0f:6d9d:2c9d:211b:4161:ada9:590c + 2582:ac42:8f4a:44c0:8005:d930:5fd:78d7 + f42d:7aea:f06:3758:5302:7e18:1a81:7aae + 501:d327:8bc2:efbd:2ae5:c3d1:67b1:b43b + f1f5:20d:f2b5:d641:8ade:542b:394c:20ee + 1b2:def5:15f2:378a:ce66:141d:c3b9:a06c + 5bf2:2bb:2e43:cd7e:4173:7fc1:867:714c + c3d5:feb4:3b95:4711:2f10:bd8e:cce3:516e + e4a4:d67c:f474:a4db:8130:6beb:5866:28e6 + c2d8:91cd:a1aa:1a16:6be4:c581:8190:c75e + de4b:4bb4:ecdb:d29d:994e:d48:fe97:6fa + 8cf8:364f:e7d9:92c9:f979:45e:64de:1578 + bf6:ac1b:b90b:e451:f648:b5f0:281f:efae + 5206:d494:26b8:6d17:90b1:88a4:b6d:c3a6 + 6cc4:6ae4:caac:33d1:4645:71ac:a83b:f65d + b530:cfcb:593b:c606:4ef2:cf2e:762:52f4 + f079:f804:3031:ef59:a49f:3f6e:fb50:d4e6 + 971b:6d8d:d2e1:dd2:7d45:e8b6:34cd:16e4 + d3ba:abd3:349e:64f2:d2dd:13f3:60b1:3721 + 274d:783f:b46b:6508:a0ee:67cb:e71f:93ec + fed4:ec08:d834:1f8b:a930:d9ac:cebb:a97c + 7dcf:3e23:7800:2fd:21a2:7325:4c0a:5533 + 4852:3553:620f:5028:41c7:6b1a:17ae:7300 + 7efb:6685:5e91:2343:9838:eac0:f94:fd95 + b0ee:3dc6:f654:349a:5126:e203:3975:7e61 + ddc:eadd:2156:f8e7:39a2:f213:e874:b513 + 680e:4bc8:87a:6e1b:8b8b:1711:cc7c:5819 + 52ae:744b:3b2d:cddf:aaf4:6ce6:596e:3362 + ea5f:d733:217a:306a:6021:879c:7f6a:4e81 + 607e:725b:1403:3b52:f714:d789:2d8b:3ca6 + a567:ddd4:dea1:12ac:715e:f7ef:da48:9572 + d9c1:42cd:e22a:5940:2e1c:b856:da61:fe8e + 4b98:7c9b:af73:13da:e1ac:921e:fc5:c1e6 + d255:aaf3:cd55:9c29:7b41:fdb:f34c:da41 + 5897:9079:6d4c:71a7:4ad6:a970:d381:1505 + 1564:236:9dac:b027:2420:663a:8641:3dfa + d736:d095:cd1:ec9f:f6be:e3c4:e7e:42d6 + 23a3:3288:eee1:92bc:e765:365e:64e5:cc2 + d46:98b2:e562:50cb:4979:4b93:c0be:b2d7 + b246:7b13:b78d:a9c:f9ad:a47c:7d0f:d521 + 2636:2358:60f0:3c17:f325:bf8:7c98:6073 + 53e:c54a:6308:3464:1893:c1f2:aeb:2a22 + a9b1:b5ad:970f:e07f:7212:40cb:cc7a:c21 + 8b79:4aa5:b392:49b1:a060:2eb3:6772:5c24 + 9b12:95db:cf70:e807:8206:a454:6192:7f8a + 25ab:ee53:e8e1:776:7169:42e1:e328:d9c2 + d959:e49e:2f98:3510:8f8e:c690:9117:5ff3 + 5464:9163:d41f:f509:778f:2be7:1a65:5deb + 7c4d:b26a:8adf:87f1:821:ed5a:d09e:d70b + 1967:af4f:3041:9eee:2abf:7d19:b7af:662d + 183a:b219:146c:364c:b530:89cc:8149:db4a + cb84:2108:b3c9:d97c:d483:ac1f:c074:4ca7 + b6a8:67fd:e257:4b21:d74a:aacc:bbf2:2690 + fac8:eb02:ba58:cb4e:7565:50c7:7011:3222 + 735d:7235:3d1e:1040:edbd:a932:bf27:9ab5 + 30b4:fd6e:4016:1ad1:aadb:f47a:8fa2:d173 + a68b:ad21:26bd:1ea9:a22a:54b2:cfc2:2d0 + 1181:9e5d:7daa:1dd2:af1:a6e2:dc0:9a9a + efdd:e15a:4020:1ba:f390:4efe:52bf:f5aa + 8dee:20c7:b77f:4df9:df6f:f4b1:8c38:96ec + 555f:9451:1355:8861:2eee:8b7d:1c61:7991 + 730f:aa2:f55:2736:d2ac:a910:c531:a523 + 457e:81f:3422:ac5c:a629:c40d:e29b:faf7 + 6a4e:7b3a:8cad:4edb:83a3:d77c:929d:a09c + ee37:1d23:1646:3552:d17c:cfef:3085:ad52 + 9886:866a:3338:b3ae:fdc6:623c:de4e:f1a2 + b04b:47bd:cf12:b43e:1246:7e7b:a78c:c4fc + 9927:28fe:82fc:5b87:c994:c129:a8e9:b7c + 3a56:5b8e:94b0:9780:8bdf:4d34:9c5e:d77d + 630e:811c:c6a9:d991:4e4e:c631:204b:fce0 + b46:d010:31b9:c64d:cd3:9db:e430:e1d1 + ed2b:388b:e791:f30:6993:2120:a55f:33f0 + 3195:805d:ef11:d51d:8afa:4638:b280:3053 + f4fe:f66d:9eb1:c87e:6e6f:7ce4:e63d:d252 + aed3:fc1e:fad0:7ab7:a6c3:ae50:8d56:5a84 + 2ddf:608f:3813:1181:19b7:3bf4:daed:a75d + e982:8f82:20f7:271f:7351:9387:cc6:1feb + 7ddf:dbe8:6441:29ee:58da:24d6:7074:b78b + 90f6:eaa9:4368:fcf3:5c7f:e94f:af96:d14d + 8f42:9a14:16c1:832c:1313:edf7:3801:9a16 + b9d8:7c8d:b332:faf3:edef:a111:c671:3f91 + d6a2:bacc:8c5c:3a68:a8bc:b7dc:48ea:3e58 + 5f04:5564:b51e:a23f:8e08:a3b9:f485:3abe + 22fb:5766:e736:66d1:5c7d:27a5:ccf:1461 + 3221:b62e:71f1:535d:6259:3642:4ca4:fa72 + 774d:1bad:302a:3c9c:624b:b702:bb:d85 + 9a22:c63a:52cb:86b5:160d:f724:33c0:fb61 + 88e2:5664:882b:f970:3733:6097:14f0:3393 + e725:5c1c:b501:cd0:1767:3ac7:32ec:a558 + 3dc9:b8dd:b6f2:8235:5c56:138d:8f18:6b1c + 71a8:93af:86b4:3fef:cd0c:f50c:e56e:bfae + b231:450:7005:863:2fc6:8421:7749:e615 + f20e:30fc:50e1:e0ba:c8e0:49ab:25bd:de96 + 2bea:53a5:49da:6a32:1f1f:4810:8112:1dec + ba3d:3aa:84f6:1b60:777a:c9f8:26cc:a1b7 + 45b6:6c46:7764:a660:da31:6d20:2ea1:82a9 + 5115:7a31:65d7:b3ca:77e4:96df:1e0f:656c + 6171:eb0d:e829:c10d:8416:5b78:d9ab:e029 + ee45:f772:2118:4748:5a07:a1f4:6dd9:6759 + 2808:6abf:8c3f:222c:5637:8089:477a:29f6 + 3c84:5aeb:f82:7f08:9f99:85ad:372e:b51d + e74f:487b:e61c:27e7:47b5:f6b8:69e8:85f1 + 6c64:1727:295b:c9c9:e707:cc73:be7c:d882 + 517b:ca37:c728:cc3e:6808:c39c:d628:f266 + b713:d225:71cc:a34b:9f2f:4081:442a:554e + fc43:f0a5:b611:1ea0:c4dc:bc1c:28d9:60ce + 4c13:210:260a:898c:11f9:cdbb:dae8:ac33 + f0f0:994a:853:30e2:d2c7:6ed:b98b:166c + c33d:9945:bced:5ee8:809e:c02a:9ac2:3daf + 37d:db7c:1fab:6de5:b74d:5e0f:26b5:2104 + c27a:1af0:33e4:11da:8202:c98c:e0e7:819a + 88cf:5517:5db:b3cc:6162:7a2f:d436:f113 + 99b5:819:b842:1a8a:7d0:2e74:113e:fa5e + a4fe:e3c9:c775:467a:9648:ed61:cf96:1937 + b1c6:17f5:8a88:b1b7:32b5:ff6d:33e7:1c7f + e2ac:4ac8:adcd:393:8d22:f475:1588:467b + 9b98:35aa:4f47:c4b5:c33a:1b62:4289:4d94 + 7fd6:1202:f6c5:cf6b:ed1b:929d:8172:ada + 7985:427c:c8b8:9a60:4d8a:6d11:a0ab:5187 + ed78:f2cb:587f:8657:b26a:4924:2db4:480c + eb36:54b8:fb4f:9453:c8f3:8458:d674:33a3 + 2671:1369:b75:1106:56f:f880:9fec:2a1a + e2b4:1400:93a4:75ef:7e2:8906:9c91:819e + d1ff:2d3a:971a:eea:3225:96f0:d6fe:8de + 2dbc:76be:c431:33f7:bf85:ab03:bed8:582 + 7c65:4e21:23b3:59f0:67d2:b965:d69d:1d8d + 354f:7653:872:be92:7ddb:f3c9:a0ca:c04d + eff8:8726:e322:4dab:8cf2:15e7:4f94:dc01 + f015:ca38:d453:e41f:9b31:a737:502e:f6ad + 9ef2:cac4:cd2d:8812:e381:3e0d:568c:bc45 + 1287:ab4d:5aad:d02a:7fb8:ebbd:d43e:4aad + 3373:dfb3:a1e9:24e5:8d03:f474:fab6:a548 + 61e3:aa22:7bca:c1e1:106d:423e:a227:7d0c + 4631:ed90:cc9f:a7c:ea11:f20a:a03e:823e + 686d:f10d:47c4:2a73:1e16:ee8e:8eba:f793 + 5f3:18bf:ce96:8daa:19f6:50e9:a29c:1bae + 43b:295f:12a1:b5a2:2c4d:e31a:82c2:e900 + cd66:720f:f78a:8d3d:ef:e03d:7500:842f + 23c3:fb5d:c23c:782e:170e:be21:bb8d:895b + 859e:e83a:b5c:764c:257e:61b3:5d4:bb62 + 5979:1c89:d753:4672:deb0:687f:a9b0:c96f + 889f:2f2d:1f5f:f7ff:f52:cc02:a126:a98a + 14b0:6ffe:718:f065:808b:e171:ab84:c5 + e6c:3311:391e:b0e7:aadf:d445:90fb:1b4 + 6e17:f80d:f0c8:e01e:f34c:7313:4f55:8570 + 5b99:c400:a95f:5b6b:8dba:a975:39eb:6e58 + 163d:90b0:a740:c6:bf87:5a32:8583:919b + f914:b250:ee76:e47d:3820:f3bb:b964:f1b6 + 5070:e971:8f90:5e9e:bfa8:bdd4:1cba:c0ac + 8d36:a84b:9e16:1ef4:698d:fea6:513a:8c4b + 29f7:9bbc:6809:e212:865a:6db:25b4:1816 + e1f2:dce9:261b:a15c:772f:e39a:2450:b4f9 + 9254:bd07:7e95:ec21:f2d4:c477:a9ab:178e + e2ee:7a:231a:ec14:17e8:dfba:eff4:a6e3 + 3edd:7695:7c6e:2816:a89a:6851:de62:2ee8 + 32df:80e3:493a:d908:5c35:aba8:9aa6:f7fe + cba0:7dfe:4fb7:d980:61d8:1bfd:34ce:9406 + bb77:460e:d558:164f:b2d4:24f8:c53f:e745 + cb45:a1c7:631e:2b7c:a8dc:40c8:d051:3355 + ef5e:7fba:372f:c229:f8bd:6c46:b40d:4c84 + c5e4:bc58:4411:21ca:e1ac:40a6:8158:fec7 + e084:c8db:bfa0:187f:ac86:c6:62ae:de07 + 6ceb:cfbe:8250:ccbc:a9d2:80f8:b18:6c17 + f91f:2a95:a3e8:15cf:f019:e9b7:14b4:af79 + affc:ef51:9005:b725:fcbc:67e8:9e63:ab88 + 2717:155f:17e0:ad92:eab7:a3a2:4db3:902b + 3dd0:aa05:eea0:6213:820b:683d:aae9:bc98 + faab:b282:f4dd:9cbe:bde9:72f6:bce9:51aa + 3dde:50b6:90ef:f725:f5f4:ecaf:7757:c064 + 9dcc:63e0:9fb2:352a:3e2:e2f8:a82d:83db + bfcf:42ac:60fa:10de:7aa5:3204:a135:8e50 + 9f6f:304f:162f:55e6:1b4a:db33:75fd:3d15 + a5dc:591d:99e4:2955:9fcd:1c3b:323b:1101 + 3a66:fc6e:66be:f870:1531:c271:9566:b02c + 9d24:39ad:63ed:d48a:b370:b821:3635:28fd + 1b06:c521:f232:4acb:a5e3:b684:c106:ff6d + e173:759a:f79f:201:5759:a652:6ed1:2ad1 + 698f:2845:36a7:6dc0:76b:e924:f32b:2584 + 6daf:8db0:383b:a440:f678:206e:696f:98b4 + 101d:3551:b5c3:92f3:e923:d580:5530:5d8 + 1aff:1ca6:4cdf:a45:8b02:e856:dc99:ddaf + e759:f8d9:9996:50cc:da9b:6a13:8c54:4fb + 19e9:2e47:ea5a:c672:a4ed:d06c:96cc:b696 + e85:3609:b324:2ec5:4c98:9302:9171:b33d + 596:8f82:c893:5cc:b502:bd26:7f4c:ab17 + 741a:7669:a5e5:1c05:128a:db0c:9fbd:a4f + 6c83:cb01:568f:b26d:f290:fca0:db0c:1d97 + f6cf:57f5:d8f7:8f7a:2a9d:2e34:ff58:3c1b + a07:b640:59cb:5e39:cc08:f652:573f:a064 + e62e:5374:5a12:c952:d8cb:d27c:47f9:3551 + 2235:b3c8:4a90:e616:39bb:804:2092:b2a + 9cbf:4102:e772:d437:eba2:abae:e64d:962e + f183:fe8b:2e0b:f308:2c01:7596:91ea:ea86 + ac5a:25d5:60d7:1e68:afbc:4df9:fcba:e10c + 447b:dff1:d2e6:cf0e:8dc9:4275:6055:1fcf + 47f4:49e2:1bbc:2751:b60a:52b4:17:a2c1 + 4100:f479:4523:6236:cab2:205d:117f:8065 + 3061:f1ae:b422:8c05:1d84:957e:4d34:f8ac + 33d8:cfd9:9ac2:83c6:c5db:afc2:1022:34e9 + bebc:707e:4504:9e6e:d255:3e24:118c:fc17 + 752b:9136:4668:b8b7:88cf:9a1c:51d3:4833 + 1dec:a2d9:7e8b:7b85:37ae:7319:205e:759f + 58ec:ec24:d2ca:36ba:6bd8:267e:e7ca:7c15 + 26b2:386c:4a95:4a69:5423:3912:565b:6721 + 3866:870c:6ed2:673c:3417:4945:1e4e:ae70 + 66b0:d61e:92e9:881:4c8:d624:7f05:3bc3 + b6a2:262:6efc:b406:395:c84e:b591:597b + 1caf:2a8:6f05:81aa:d3ad:7ea2:53cc:a52c + ab4:5498:8b9d:2007:3684:7247:8da9:4425 + 80c:a02a:1992:e77e:c029:da16:7186:8ef7 + 28b5:4c26:2051:7349:e986:7377:4b94:4df3 + 866d:7d4f:1a38:8799:99d5:e855:430e:1882 + 5c37:d7a1:69d0:ddc:5765:76e2:da6a:5d6 + 7558:95f9:e627:110c:684b:1596:ee1f:cdab + b397:e20d:c6a7:1e0b:792b:b05c:3ec5:1d40 + 1b25:5e99:3fc1:9ccd:3e31:c6cc:6a11:525d + 7550:5381:629c:f51c:b970:56bc:f852:19e + f28d:1214:ad91:d119:efb6:88cd:434d:3b2d + de4a:6544:b00a:38fc:1f0:1ed8:ca03:f3a7 + d9fe:c679:5f79:52a4:8837:4e74:64a2:fc6 + 1a42:189d:e257:344b:1ac0:3484:683b:f908 + e0d7:f40:3002:37ba:b2e8:557e:16c8:fec1 + 8948:c4da:1894:48b2:adbe:164d:49ce:3ea8 + 48ac:faee:2a45:9085:c925:edfd:de1a:2753 + 1ec6:191d:d20c:cec7:45e5:9f5b:7963:be4e + ed73:7b04:c0fb:1aec:1bab:6c5a:d00f:cf2e + fc41:610e:3fb5:7ff0:12ed:977b:c8bc:2557 + a288:f169:706a:cc69:549:140b:145:100f + 3406:e307:89b1:862f:a722:5064:79d6:ae41 + 631b:bc8a:ccee:5595:3aec:70e3:7aeb:21dd + 40fb:73a1:ddab:4564:bbca:712e:9790:c06b + 7a3d:8e63:2785:9f78:e25c:612f:8736:dd03 + a897:165:be7d:bb29:393:462e:4a0c:b366 + f10b:8f83:c48:8075:69c8:51cb:62c9:aeff + 7592:af1a:2cf7:af0b:ad3d:5d30:8197:2a40 + e29a:4acc:6d1b:587a:6840:4dc4:6505:6439 + a7aa:da3:a7d:3d1f:4b45:2f44:bc5f:226c + 26d6:a740:1781:2d1:f342:21cc:7e8a:6255 + 6f91:77f2:cd:52c0:dd5e:980a:29ba:1881 + 1fe9:cc8d:53dd:e254:67c9:d324:dcb7:7e64 + c618:3c78:560a:4744:f3cf:d747:5e1e:541e + 789c:4a72:a324:3175:86b0:8d4d:69f5:8da7 + 4779:f909:43de:b7c6:de98:8a63:ae2e:98ef + 9736:ecc3:efb9:4c13:41ca:128f:9d32:123f + 7b24:c110:db66:3d4c:b51e:f58b:57bb:bb7 + fda7:d73e:e1e8:5902:cef0:58e8:973c:59ca + 21d9:95ed:2e98:9e5c:11fc:ef02:b5f:981f + e24e:77eb:cd32:8692:c740:ca68:e411:bfa3 + 9455:2fb5:ab05:18b3:a0e9:472d:72c3:1dcf + ca85:673b:eaf8:5090:8d48:c78e:adb8:cfd2 + 38dd:b63a:b4aa:5729:2e87:a33b:beaa:2670 + 69bc:c8b2:67b6:cce3:fc95:860:9f6c:b9bd + 608e:7da0:be6e:5ec6:953e:ffa8:58b7:5b23 + aca:5006:de2c:8934:4b54:ba6:dbc8:8fbd + b48a:b926:cfa:bb71:89b3:2fb3:f413:2e86 + fb36:ddad:3c4e:4edb:2d5a:9ba4:dd07:8301 + f9ac:10b7:fdaa:2b39:8beb:b135:5379:5a80 + b64a:3fca:5e7d:c23b:d555:fb9a:9d04:1dbb + f82d:f94a:b873:4416:411f:8457:e474:1c68 + ae37:2c35:27d9:a134:2dfa:67e1:7a9:978e + bac2:c33e:ebff:c0c0:cc47:d389:8f01:8334 + 8d8c:1377:7d88:f286:a7f9:396e:de05:327c + f20a:841f:6879:b2fa:7850:2279:e2b4:197a + ffe1:47ee:19e4:7e41:2f28:6f04:70f9:380a + c56a:5c59:38f7:9c6c:9297:a2cd:7492:cd4e + a373:8274:c519:f28d:cccd:5991:9a37:f9bb + fb3e:79ed:2d45:337e:9f68:80f3:e796:1840 + 5a32:298b:8fee:bee2:e93:ff0:9cf5:c95b + 3ce1:ecff:1358:c04b:e5a5:b059:79bd:e00b + fd80:8a29:7ce6:1ad6:c237:fabd:b841:d86f + d103:e43f:3ccc:352a:ad7e:b771:ebb4:78d8 + 15d5:3d25:2250:3c52:a9b8:387:5f20:2356 + c01:aabb:39db:bb73:2392:3f56:e353:7c39 + e85f:b6c6:32fb:b985:eddc:64d8:34d2:eb65 + 971a:1f40:1082:ff1a:8ae0:a149:aa28:3f45 + 2d5c:48d0:26f:5fbc:789d:e363:b4d6:c365 + daae:cef1:7adb:9b47:67b:bf34:987e:a17e + 1d00:f7a0:1087:2c4f:1b17:3fbb:299e:2a8a + dd7f:3010:349a:2790:372a:fd24:d88b:c112 + 35e1:87f8:8790:5f6c:5a91:f165:7c42:80a1 + ba0b:372b:fff1:511:a55:837e:63a4:3c70 + d3d6:1cf3:b3c8:f686:9742:5bc4:71e1:2670 + 4e3:9269:f472:e432:a8a9:c597:8330:3124 + 588a:104e:af3f:8c76:a215:df35:b1f5:b869 + b62c:4bac:467a:16a1:f39f:1d0:45dd:6547 + 1e51:9797:1098:87a:5645:19e0:80ee:91b6 + be61:abad:967a:f464:f565:e461:ea98:c4b4 + 6424:b1a0:3623:1789:620a:6505:b58f:c2f3 + 9d2b:7df:912f:f6ad:db09:136b:bddd:f5e1 + 4f7d:bad2:911c:b975:8799:ec91:9e4e:13de + f667:5ade:e7ae:8364:ccec:4b55:edfb:a2d8 + da03:cadb:fbfc:b575:16f0:ff15:5145:8719 + 6592:885b:e67c:316b:48dc:26e2:db1d:84e0 + 2305:21e2:a893:92ba:764e:ce9a:88e2:7fac + 8c72:44a1:11b7:9ce9:40b1:10a0:48d6:fb1a + 58d9:9d45:cf24:3336:b9c0:724:d23a:a410 + d8a7:2470:7b97:a09c:a454:bfed:d95e:6212 + a92c:58dc:6fcb:e029:9ba1:71f3:85ec:4b5b + 8389:88f6:781c:bd2f:9ecb:50a2:b5c1:ba21 + f1a5:835b:1071:b5aa:7a9a:b50:2ce0:3a1b + 6c3a:dd9c:350a:f982:ad3f:64e0:8605:aeaf + 9965:f7a1:7c37:91bf:4399:df3f:8b29:9eb + 62ff:1dac:722a:3572:79a5:3023:cb9c:32cd + 6b46:7842:93ec:14b4:ae9d:b65e:d218:9ca5 + bf7d:2195:20b7:20fa:bfa2:28ce:a4c5:f527 + 9e0f:655e:3a3f:53bc:8ad3:9524:8c3f:12a2 + 77b:4be9:3166:3f3f:c0d:cc18:b343:fd4 + b390:e5f1:f1c1:596:e3f1:cc9d:e2a4:e377 + 66ae:3100:1171:6d87:3901:c3e3:bdd2:e42d + 14f7:fc58:70e2:8210:565a:412b:4a1:3efd + 14fb:90f7:522e:e5:97dd:6ba:da80:b8fa + 55ae:e7ec:d8c3:96ba:3286:333d:bd43:d07e + 623d:7324:ed75:f33f:56fe:9c6d:ff32:7243 + 41c6:14ea:7207:943e:d9f2:d5b2:c02e:fac0 + 883c:f377:b9c9:fcd9:d68:73f5:ad0d:abde + dbf:9fc2:9203:bfa0:82a9:2845:ef25:b7b7 + 1b73:da7d:70ab:a288:49c6:68df:13e5:6c9f + dc1c:48f2:6fd1:c69d:27c0:2a71:5aba:679e + 9b4c:1fc:3760:11f1:b1d4:ec86:b78c:c6e2 + a925:f2b:3e93:469a:186d:545f:c1f:1ed2 + 6b95:ce78:60c7:a846:95ad:b387:b8a4:adff + 603d:a9dc:787e:2365:fd2e:aaa6:89aa:5808 + d995:4322:e8bd:7450:817d:77c6:69a6:1259 + fb3d:12b6:62b5:1737:82e5:3e62:7bfa:fd42 + c9ea:cc31:c4bc:312b:6b84:5582:6bbd:4a8a + fab1:5e60:37e9:8168:d690:4280:a93c:f87f + 14e9:183d:ccab:246c:1b0d:c612:7039:a64f + 2284:c46d:3602:ef63:aed1:b14c:aa44:2f1 + 98df:c56d:6430:945c:63ee:79da:76a8:c337 + 211f:c356:4d39:60a9:e932:d459:c0c8:2ea2 + 140c:145b:5c06:aa2:7d5c:40d5:830d:400 + 753e:15a6:6cab:84eb:b7a3:d527:8a46:8d8d + 8935:cb3:ecb7:8fd5:6608:4a2e:fb41:4aaa + bd6b:90e6:9303:2f2b:7a6e:7397:c108:9b64 + 7ff1:b27e:b8ec:f07c:5a71:1b1b:d22f:293f + 530d:ee0c:8635:987c:761f:9501:ad60:6df9 + e0b0:673e:5b8c:dd1:a57d:bc63:e60d:6dc2 + c516:b087:6c2d:9870:a556:26d8:fde1:fd03 + c33c:752e:4df2:9675:18a:4f53:7ed3:8d52 + ff50:9d7a:9090:9ece:7b12:eef2:420e:86cd + 3595:1af2:832d:eab0:6e7e:6f1e:6b87:cf4b + cda5:fb:c54d:6142:a47f:c488:f721:cdc7 + bf26:a7a1:7bd:1c96:d615:e78c:5656:e4c2 + a7ce:6c39:ec54:3d2c:4dd2:f5b3:666:ec71 + 468c:ff17:d13a:c3f0:73e3:bdcd:639f:6bbb + 4ad2:4b6d:cae3:b6b4:6d29:b851:4b35:51b1 + 6726:b54f:3456:49d9:d479:e47c:ebe0:6fba + fbc9:20cb:f1ef:daa4:8c93:39f3:c4da:d630 + 7b1:d326:a302:1153:6945:4bdd:bf9e:7db6 + 194a:f23a:d28f:df49:7ae2:2021:f1d9:e80a + da17:e8ac:92bb:b7f2:dcaa:d900:5370:8b62 + 45cc:b699:3941:2594:5ee:e6fa:9908:1a4a + 16aa:fa0b:6167:c7e6:c539:b8e8:6be8:d073 + 8649:87be:21f7:38da:b4c1:c8cd:1721:bf6e + bc69:4ba6:44bb:cb3d:6e9d:4d12:19c9:2061 + eb06:b6b4:7dab:6fc3:4eb7:3690:a9e7:fdc6 + 96d5:fdec:5fae:98d8:bd3f:6435:7f05:f806 + 31b1:899:b87f:fa94:74bf:80c7:ed18:942e + 9374:b051:e65b:f765:1344:7d83:d4d1:3649 + f6f3:d60f:2969:2851:5299:8fb1:2aac:ef80 + 8c24:bb8:d4bb:bea4:8e0d:10da:b240:eff + ee3b:8d30:e80d:beb0:3a93:d75d:7780:5ce7 + 7cd3:52f8:cc00:3a7f:f916:a1b3:a7c2:ce17 + 60a7:886d:3f44:f34c:fe94:9648:a14:52b1 + c960:7025:f733:9f87:9bd6:36b4:3a43:4c51 + 26e3:4ea8:d374:46ee:faee:bc1b:7bf8:97bd + a5ed:eb36:8659:f37:3eee:a6a1:bd61:45d0 + 46df:b7f8:96b1:bbb9:d1e8:643f:ddd6:ac9b + e69d:cafb:8193:f2ea:f4f3:e9cb:c662:182f + 3b75:162a:ec2f:54e8:b2dc:4fcc:2517:1d93 + 1bde:40b1:22e2:f66d:4f79:94a8:af25:b817 + 1bb9:8ea:38ec:401a:396d:260f:5934:db33 + 5c59:7a87:3d5:68d:e218:433f:7f84:3d8 + c6f2:1ba5:5909:4b42:904e:a38d:9ead:2ce6 + ddeb:6c80:9601:ebd8:6e0c:4fcb:7ecf:1522 + a30b:8b55:d61d:f674:2184:8907:12e7:2aba + 85d4:acd:4a96:a922:96da:cc55:577a:b52a + 58e5:4b03:73e9:968e:60c4:d7da:881e:740d + d464:2ec:91e:4d25:77c3:57dc:3062:9857 + 8136:c43f:c08c:3646:9123:e3e4:886:699 + c7f:6d8a:9fe1:5dec:cd74:723a:1656:42ef + 3e93:aac4:7e6a:93ae:8b72:86ac:bbfd:2651 + 2f1a:c709:2d2d:55d9:bc30:d3e0:1f12:6143 + 274:cd72:738:7536:ec34:3213:7164:937f + 408f:c9f6:106d:510d:e91e:97a3:61e4:e4c2 + f37:dca7:7721:1584:2457:fd1d:7138:d71a + a6d1:4062:9e32:b0d6:4494:3cce:f7bb:8a76 + 33de:f485:3842:210e:4eb:3702:a882:1080 + 76c3:76a4:49a1:8740:3d60:fe1b:1e29:8b41 + d62c:8e21:91f4:bb62:f054:d968:a17e:6313 + bef2:f1f6:8d39:42ba:8555:e64d:6e0c:128c + f4a8:8061:5b77:b64f:757b:c73a:58a7:e2d8 + 5292:1f39:3a12:4258:2b18:78a5:223f:c73d + b5c5:76d3:c906:f976:5f0:70fd:40ec:f158 + 353b:b88:60b5:5e44:5fba:ff4c:95b2:1632 + 32b9:229:bdd2:1e35:bd5a:96f:1d8e:bf09 + 9a80:e008:f711:1dc:c5cf:6f88:9137:ae8c + 71c1:3fb9:f032:8109:1afa:b241:bbbe:6fde + ec13:f481:9aba:7fd5:54ff:e539:5f04:7d52 + dad4:396d:2e60:e4e1:dfff:e6b2:4c83:d004 + b238:b281:be52:3df0:6044:212b:372d:57d9 + 4401:60ba:6563:eef:6ce9:93f7:b366:195f + 1112:995d:7c1:2f09:9a5c:dc9:fbae:7f04 + ce2e:e3c8:1e84:29f1:a9a4:7cec:6363:6e44 + 9059:3a29:592:7f1f:fa41:636a:9818:1d8a + adca:e66e:5256:111e:38d4:988:9849:37f5 + dbdd:32e:e26b:a789:f663:708e:c1ea:24a + 9d08:bcb1:d9a:ae0a:8375:dac3:e973:de58 + 331:77a6:2b72:4125:2bd5:7e58:8d8c:1894 + ee5e:7f80:3523:6212:1bd2:e2cb:d1b1:fa4a + 15ff:b0d2:b37:a4f1:26b3:8680:d467:9adb + 685c:a78e:4ae3:2eb6:750c:1727:7e15:d479 + 209b:a6e0:63f1:f963:e817:7050:1ff8:819 + c019:e3b3:57ad:7cc2:ad29:7c72:3576:9ef + f11:2adb:5f8e:d91a:aac3:8aca:6133:95d + 1ec:192b:bb14:b2b:ee92:b23d:807:328e + 1c6c:a683:fb0f:1505:8c47:1e7f:f540:a075 + bd4c:e28f:63e1:2a74:2676:1f2a:bebe:d1e5 + b449:9fac:bfe3:efd8:814a:f35:12f9:cc16 + bdb7:4f66:39b2:5c81:1f1e:c318:9596:73 + 741c:7152:5f10:9df5:150b:22b7:3c1d:ee54 + aa84:1e98:3050:91f3:555d:6526:4fe4:2e2d + 38e4:de84:963e:32c3:4fe5:4a56:e972:a4cf + d875:46b1:3440:5920:a190:7949:9859:bae0 + f3a5:deea:e56:965e:6995:b36b:e6a1:d170 + 4e06:a10b:7f64:ecc8:3f47:c1ae:a4eb:64e4 + 462d:8386:2ee3:bbb2:a0e2:93d1:c3f:29e6 + 4338:162:1aba:8b5b:61eb:d524:55d2:3503 + 75bc:7902:23ed:2711:78e4:ed05:e61f:61c + d57e:2215:ab0:1ad9:aa9c:b048:7e5d:8f4 + 401a:67b5:9382:98b9:30e3:780f:1b5:c4ab + 579e:83bd:4a70:83bb:2155:1261:fbde:9af2 + cadb:1b42:52c0:2b8c:57b1:311f:fa9:5ae0 + aa10:e88e:59d1:1081:e911:c544:9654:b61d + ce95:8f52:a688:a524:21f4:275a:5e0b:f2e7 + 9b9c:656e:ea45:cb9:bc07:124a:ee2a:6b9d + f7f3:61e:e9fe:4ec3:10d9:82fe:e6a3:af60 + e642:7f41:ac93:7c19:d9f0:593:32f8:1d2 + 82c5:2205:3988:1f6e:aa40:1a1b:2a26:35df + fb85:1116:7124:6a40:ee23:ca23:cb0a:c038 + cdf7:5ed8:20f1:e8ac:1acf:1b6a:3746:348a + 98ce:a1fc:e7c1:7333:2352:461a:c853:cfee + 201b:7eab:96e7:4a25:62b:790d:1d2d:41d5 + c076:7e29:61a3:d0a9:b838:e4dc:9e14:5d27 + 751e:6be:859c:a0c6:8985:8749:f44a:ae47 + 5515:7f4:b4fd:7268:8495:b812:8795:9f4b + ad38:532c:17dc:1ccd:6ee0:74b5:4cbe:d336 + f369:8e22:623:d153:6584:d8d2:a217:f099 + 12e7:a1ad:7cb3:f001:fa40:eb93:488d:87eb + 4896:e9d9:b84e:fc3e:e7d7:5b40:34d2:223e + c833:dc92:778a:db03:524f:b66d:76ed:4489 + 7bc8:6585:fc9:9972:3ce1:bb2a:e67c:b0a0 + fdda:19cb:e535:7dbe:6f23:5d6b:f384:e026 + d79f:27ca:5c65:17d1:e124:cd86:2a1:a6e8 + e13c:6b20:6ceb:389:c014:2a45:8133:5065 + d490:95a9:e4ff:e713:a5e:656b:703a:2cb + c901:94d:3a11:23b2:9206:4285:72f2:804a + 14e9:bd97:37fb:d0a2:cc85:754e:42e7:d099 + de48:afcc:709e:314f:1dc1:240f:f47:1c52 + f097:59df:a9e0:395d:7749:5043:2c63:fc42 + 1ea8:9016:9b5a:3817:a692:8948:7f3e:5fbb + 4b02:5f44:4f3c:2dc6:8213:5859:f361:5d78 + 53bd:a09a:f98a:7d76:d809:f110:c1a1:9216 + 50cb:20a1:a261:1ee9:872a:2aa0:ecc9:839d + b453:a324:92c9:4677:2e69:d6d6:58d0:c47f + 46ea:4024:7132:cd14:7fe6:917d:6c7f:9e4f + bf72:1392:1a7e:7138:e99e:2c63:fdfa:4e44 + d3ad:cdb5:6a7f:fe83:3278:b63d:c1bb:d6db + 53a1:eaee:6632:d130:7028:80da:ed9e:81cb + c8bb:6604:a2ad:3425:8c3b:3021:eda2:b0dc + e26f:4487:8a8c:e126:6d65:3a70:5b43:9061 + 602c:88f3:a18e:98bd:4412:fe2:9167:4afd + 5866:b131:bdf3:6ac3:32a:323c:d545:432e + 380e:5485:585e:63f6:5454:f8f9:8b06:e9c9 + 3ed6:35e0:62a4:4977:32b1:7eb9:749a:587d + 4493:b63c:e298:714d:bc6d:bfb7:faa4:1ec0 + ddd9:b818:28d0:d6ce:7ec1:6ba9:1097:ac7e + fcdf:56eb:41de:f759:1718:340c:185:10b + 38f7:3970:6601:dc48:84eb:bb8:8205:70b8 + 32cd:5932:6790:f462:cb54:937d:2acf:bbbb + df:7099:8488:f1f6:36d2:c1e5:5a7f:4a36 + 4ac1:b964:567c:fee5:443:213:4f6b:d49a + 7272:2fcd:aaf7:e78e:14f7:a408:17a1:3466 + 7f67:2b31:5d20:9b62:ad29:2ec0:24db:5450 + ec07:b8ab:5a2c:71d1:c981:3b9d:a026:41a8 + ac5a:af96:2022:9c03:ea3a:e25b:b18:202d + ceed:c4aa:5f23:e107:7593:4516:b73e:e977 + 3600:f1f2:6a6a:8c49:e7aa:1e29:ce4a:ae94 + e9a7:3b30:4936:89ad:56ac:12c9:8953:d6bd + 15e9:697e:b95e:b709:6c57:12ff:cde9:4afb + 9321:96a8:1609:13cc:bf60:e4dc:9aeb:dcb1 + 4a1f:76cf:93cd:422e:7e9b:595:6dac:2461 + e441:7a59:2940:16ed:4c3:3378:435d:7d4d + 9ce0:65aa:c484:7d8f:d781:65bc:df78:b6d + 5a99:9f58:b0d1:c9c6:3fc9:4e48:42a6:bedd + c4f7:f8a9:8ad8:a57c:f109:8d32:f388:d627 + 6e81:b6d5:234e:128e:e5a3:120b:5456:878c + 3d32:c793:8218:c70:e3a9:fbfb:f0d:9db8 + bac4:1b9d:4b95:8416:b9b1:1b5b:25c:e9af + f3c7:f541:3dcf:add7:e3e6:7faf:544d:5f7d + 932c:3299:a8e0:77ca:94fa:3992:aad4:28c7 + ea46:a11c:3293:efdd:5568:86b1:759a:83f8 + a5e:8374:5706:5734:5444:731c:6814:1877 + 5cdf:6a42:394c:73a4:b2d1:894b:4348:e151 + 42c3:c1e9:294b:1dd7:6a50:35a6:581a:1be6 + ce0e:cca6:7ed9:538d:7e11:ca54:848:3a66 + dd0a:3f08:2970:6c0f:83f0:824e:9365:f698 + 6060:4dee:ad50:c4f2:bb82:415e:a881:f453 + f706:dc21:d9c5:587f:638f:64f8:be2c:cde7 + e258:24b6:de5c:8d47:ffe6:9678:da21:6d60 + f00d:3abe:6255:1b13:981b:7aa1:7307:1f8c + 8958:dcf1:dae5:94fa:4a2a:fc32:442f:6595 + ac47:d32d:5ff2:e487:4b11:71f8:47d2:b038 + fa2:867f:e:1d77:b070:8e27:ae4d:68b4 + """.trimIndent() + } +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/OnionAddressV3UnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/OnionAddressV3UnitTest.kt new file mode 100644 index 000000000..2b9f9cc92 --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/OnionAddressV3UnitTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddress.V3.Companion.toOnionAddressV3 +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddress.V3.Companion.toOnionAddressV3OrNull +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class OnionAddressV3UnitTest { + + @Test + fun givenValidAddressString_whenToOnionAddressV3_thenIsSuccess() { + ONION_ADDRESS_V3.toOnionAddressV3() + } + + @Test + fun givenInvalidAddress_whenStoredAsOnionAddress_thenThrowsException() { + val msg = "invalid onion address did not throw exception as expected" + + assertNull("".toOnionAddressV3OrNull(), msg) + assertNull("$ONION_ADDRESS_V3.".toOnionAddressV3OrNull(), msg) + assertNull(ONION_ADDRESS_V3.dropLast(1).toOnionAddressV3OrNull(), msg) + } + + @Test + fun givenOnionAddress_whenDecoded_thenIsSuccess() { + assertEquals(ONION_ADDRESS_V3, ONION_ADDRESS_V3.toOnionAddressV3().decode().toOnionAddressV3().value) + } + + @Test + fun givenUrls_whenToOnionAddressV3_thenIsSuccess() { + TEST_DATA_ONION_V3.forEach { url -> + val address = url.toOnionAddressV3() + assertEquals(ONION_ADDRESS_V3, address.value) + } + } + + companion object { + const val ONION_ADDRESS_V3 = "6yxtsbpn2k7exxiarcbiet3fsr4komissliojxjlvl7iytacrnvz2uyd" + + val TEST_DATA_ONION_V3 = listOf( + ONION_ADDRESS_V3, + ONION_ADDRESS_V3.uppercase(), + "$ONION_ADDRESS_V3.onion", + "some.subdomain.$ONION_ADDRESS_V3", + "some.subdomain.$ONION_ADDRESS_V3.onion", + "http://$ONION_ADDRESS_V3.onion", + "http://$ONION_ADDRESS_V3.onion/path", + "http://$ONION_ADDRESS_V3.onion:8080/some/path/#some-fragment", + "http://subdomain.$ONION_ADDRESS_V3.onion:8080/some/path", + "http://sub.domain.$ONION_ADDRESS_V3.onion:8080/some/path", + "http://username@$ONION_ADDRESS_V3.onion", + "http://username@$ONION_ADDRESS_V3.onion:8080", + "http://username@$ONION_ADDRESS_V3.onion:8080/some/path", + "http://username:password@$ONION_ADDRESS_V3.onion", + "http://username:password@$ONION_ADDRESS_V3.onion:8080", + "http://username:password@$ONION_ADDRESS_V3.onion:8080/some/path", + "http://some.sub.domain.$ONION_ADDRESS_V3.onion:8080/some/path", + "http://username@some.sub.domain.$ONION_ADDRESS_V3.onion:8080/some/path", + "http://username:password@some.sub.domain.$ONION_ADDRESS_V3.onion:8080/some/path", + "https://username:password@some.sub.domain.$ONION_ADDRESS_V3.onion:8080/some/path", + "ws://username:password@some.sub.domain.$ONION_ADDRESS_V3.onion:8080/some/path", + "wss://username:password@some.sub.domain.$ONION_ADDRESS_V3.onion:8080/some/path", + ) + } +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/PortUnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/PortUnitTest.kt new file mode 100644 index 000000000..bcdac260b --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/PortUnitTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.kmp.tor.runtime.api.address.Port.Companion.toPort +import io.matthewnelson.kmp.tor.runtime.api.address.Port.Companion.toPortOrNull +import io.matthewnelson.kmp.tor.runtime.api.address.Port.Proxy.Companion.toPortProxy +import io.matthewnelson.kmp.tor.runtime.api.address.Port.Proxy.Companion.toPortProxyOrNull +import kotlin.test.Test +import kotlin.test.assertIs +import kotlin.test.assertNull + +class PortUnitTest { + + @Test + fun givenMinToMax_whenToPort_thenIsSuccessful() { + (Port.MIN..Port.MAX).forEach { port -> port.toPort() } + } + + @Test + fun givenMinToMax_whenToPortProxy_thenIsSuccessful() { + (Port.Proxy.MIN..Port.Proxy.MAX).forEach { port -> port.toPortProxy() } + } + + @Test + fun givenMinMinus1_whenToPort_thenIsNull() { + assertNull((Port.MIN - 1).toPortOrNull()) + } + + @Test + fun givenMinMinus1_whenToPortProxy_thenIsNull() { + assertNull((Port.Proxy.MIN - 1).toPortProxyOrNull()) + } + + @Test + fun givenMaxPlus1_whenToPort_thenIsNull() { + assertNull((Port.MAX + 1).toPortOrNull()) + } + + @Test + fun givenMaxPlus1_whenToPortProxy_thenIsNull() { + assertNull((Port.Proxy.MAX + 1).toPortProxyOrNull()) + } + + @Test + fun givenURLWithPort_whenToPort_thenIsSuccessful() { + "http://something.com:80".toPort() + } + + @Test + fun givenURLWithPort_whenToPortProxy_thenIsSuccessful() { + "http://something.com:8080/some/path".toPortProxy() + } + + @Test + fun givenInt_whenPortProxyPossible_thenToPortReturnsPortProxy() { + assertIs(1024.toPort()) + assertIs("http://some.com:1025/path".toPort()) + } +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/ProxyAddressUnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/ProxyAddressUnitTest.kt new file mode 100644 index 000000000..b037a1aa5 --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/address/ProxyAddressUnitTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.address + +import io.matthewnelson.kmp.tor.runtime.api.address.ProxyAddress.Companion.toProxyAddress +import io.matthewnelson.kmp.tor.runtime.api.address.ProxyAddress.Companion.toProxyAddressOrNull +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProxyAddressUnitTest { + + @Test + fun givenIPAddressV4WithPort_whenProxyAddress_thenToStringIsAsExpected() { + val expected = "127.0.0.1:9050" + val actual = expected.toProxyAddress().toString() + assertEquals(expected, actual) + } + + @Test + fun givenIPAddressV4WithoutPort_whenProxyAddress_thenToStringIsAsExpected() { + assertNull("127.0.0.1".toProxyAddressOrNull()) + } + + @Test + fun givenIPAddressV6WithPort_whenProxyAddress_thenToStringIsAsExpected() { + val expected = "[35f4:c60a:8296:4c90:79ad:3939:69d9:ba10]:9050" + val actual = expected.toProxyAddress().toString() + assertEquals(expected, actual) + } + + @Test + fun givenIPAddressV6WithoutPort_whenProxyAddress_thenToStringIsAsExpected() { + assertNull("[35f4:c60a:8296:4c90:79ad:3939:69d9:ba10]".toProxyAddressOrNull()) + } + @Test + fun givenURL_whenProxyAddress_thenToStringIsAsExpected() { + val expected = "192.168.10.100:8080" + val actual = "http://$expected/some/path.html".toProxyAddress().toString() + assertEquals(expected, actual) + } +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AddressKeyBaseUnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AddressKeyBaseUnitTest.kt new file mode 100644 index 000000000..cc4761705 --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AddressKeyBaseUnitTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.key + +import kotlin.test.Test +import kotlin.test.assertEquals + +abstract class AddressKeyBaseUnitTest( + protected val keyType: KeyType.Address, + expectedAlgorithm: String, +): KeyBaseUnitTest(expectedAlgorithm) { + + abstract override val publicKey: T + abstract override val privateKey: V + + @Test + fun givenAddressKey_whenAlgorithm_thenIsAsExpected() { + assertEquals(expectedAlgorithm, keyType.algorithm()) + } + + // TODO: Factory function tests +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AuthKeyBaseUnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AuthKeyBaseUnitTest.kt new file mode 100644 index 000000000..0becadbe2 --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/AuthKeyBaseUnitTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddress.Companion.toOnionAddress +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddressV3UnitTest +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddressV3UnitTest.Companion.ONION_ADDRESS_V3 +import kotlin.test.Test +import kotlin.test.assertEquals + +abstract class AuthKeyBaseUnitTest( + protected val keyType: KeyType.Auth, + expectedAlgorithm: String, +): KeyBaseUnitTest(expectedAlgorithm) { + + abstract override val publicKey: T + abstract override val privateKey: V + + @Test + fun givenAuthKey_whenAlgorithm_thenIsAsExpected() { + assertEquals(expectedAlgorithm, keyType.algorithm()) + } + + @Test + fun givenPublicKey_whenBase32Descriptor_thenIsAsExpected() { + val expected = "descriptor:" + expectedAlgorithm + ":" + publicKey.base32() + assertEquals(expected, publicKey.descriptorBase32()) + } + + @Test + fun givenPublicKey_whenBase64Descriptor_thenIsAsExpected() { + val expected = "descriptor:" + expectedAlgorithm + ":" + publicKey.base64() + assertEquals(expected, publicKey.descriptorBase64()) + } + + @Test + fun givenPrivateKey_whenBase32Descriptor_thenIsAsExpected() { + val expected = ONION_ADDRESS_V3 + ":" + expectedAlgorithm + ":" + privateKey.base32() + assertEquals(expected, privateKey.descriptorBase32(ONION_ADDRESS_V3.toOnionAddress())) + } + + @Test + fun givenPrivateKey_whenBase64Descriptor_thenIsAsExpected() { + val expected = ONION_ADDRESS_V3 + ":" + expectedAlgorithm + ":" + privateKey.base64() + assertEquals(expected, privateKey.descriptorBase64(ONION_ADDRESS_V3.toOnionAddress())) + } + + // TODO: Factory function tests +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3UnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3UnitTest.kt new file mode 100644 index 000000000..ce65a8a98 --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/ED25519_V3UnitTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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. + **/ +@file:Suppress("ClassName") + +package io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import io.matthewnelson.kmp.tor.runtime.api.address.OnionAddressV3UnitTest +import io.matthewnelson.kmp.tor.runtime.api.key.ED25519_V3.PrivateKey.Companion.toED25519_V3PrivateKey +import io.matthewnelson.kmp.tor.runtime.api.key.ED25519_V3.PrivateKey.Companion.toED25519_V3PrivateKeyOrNull +import io.matthewnelson.kmp.tor.runtime.api.key.ED25519_V3.PublicKey.Companion.toED25519_V3PublicKey +import io.matthewnelson.kmp.tor.runtime.api.key.ED25519_V3.PublicKey.Companion.toED25519_V3PublicKeyOrNull +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNull + +class ED25519_V3UnitTest: AddressKeyBaseUnitTest( + keyType = ED25519_V3, + expectedAlgorithm = "ED25519-V3", +) { + + override val publicKey: ED25519_V3.PublicKey = OnionAddressV3UnitTest.ONION_ADDRESS_V3.toED25519_V3PublicKey() + override val privateKey: ED25519_V3.PrivateKey = PRIVATE_KEY_B64.toED25519_V3PrivateKey() + + @Test + fun givenED25519V3PublicKey_whenEncodedString_thenToKeyIsSuccessful() { + val expected = PUBLIC_KEY_B16.decodeToByteArray(Base16) + + assertContentEquals(expected, PUBLIC_KEY_B16.toED25519_V3PublicKey().encoded()) + assertContentEquals(expected, PUBLIC_KEY_B32.toED25519_V3PublicKey().encoded()) + assertContentEquals(expected, PUBLIC_KEY_B64.toED25519_V3PublicKey().encoded()) + } + + @Test + fun givenED25519V3PrivateKey_whenEncodedString_thenToKeyIsSuccessful() { + val expected = PRIVATE_KEY_B16.decodeToByteArray(Base16) + + assertContentEquals(expected, PRIVATE_KEY_B16.toED25519_V3PrivateKey().encoded()) + assertContentEquals(expected, PRIVATE_KEY_B32.toED25519_V3PrivateKey().encoded()) + assertContentEquals(expected, PRIVATE_KEY_B64.toED25519_V3PrivateKey().encoded()) + } + + @Test + fun givenED25519V3PublicKey_whenBytes_thenToKeyIsSuccessful() { + PUBLIC_KEY_B64.decodeToByteArray(Base64.Default).toED25519_V3PublicKey() + } + + @Test + fun givenED25519V3PrivateKey_whenBytes_thenToKeyIsSuccessful() { + PRIVATE_KEY_B64.decodeToByteArray(Base64.Default).toED25519_V3PrivateKey() + } + + @Test + fun givenInvalidInput_whenToPublicKey_thenReturnsNull() { + assertNull(PUBLIC_KEY_B16.dropLast(2).toED25519_V3PublicKeyOrNull()) + assertNull(PUBLIC_KEY_B16.dropLast(2).decodeToByteArray(Base16).toED25519_V3PublicKeyOrNull()) + } + + @Test + fun givenInvalidInput_whenToPrivateKey_thenReturnsNull() { + assertNull(PRIVATE_KEY_B16.dropLast(2).toED25519_V3PrivateKeyOrNull()) + assertNull(PRIVATE_KEY_B16.dropLast(2).decodeToByteArray(Base16).toED25519_V3PrivateKeyOrNull()) + } + + companion object { + const val PRIVATE_KEY_B16 = "18968BE7C1BA78AA14501A07700EF59C0694B75E788D8987FDE5B221E60690431EDDED6CB13481DB52E9F7E5DB65E1ED375A14EB91D676ACF926370C9F6DD5ED" + const val PRIVATE_KEY_B32 = "DCLIXZ6BXJ4KUFCQDIDXADXVTQDJJN26PCGYTB754WZCDZQGSBBR5XPNNSYTJAO3KLU7PZO3MXQ62N22CTVZDVTWVT4SMNYMT5W5L3I" + const val PRIVATE_KEY_B64 = "GJaL58G6eKoUUBoHcA71nAaUt154jYmH/eWyIeYGkEMe3e1ssTSB21Lp9+XbZeHtN1oU65HWdqz5JjcMn23V7Q" + + const val PUBLIC_KEY_B16 = "F62F3905EDD2BE4BDD008882824F659478A7311292D0E4DD2BAAFE8C4C028B6B9D5303" + const val PUBLIC_KEY_B32 = OnionAddressV3UnitTest.ONION_ADDRESS_V3 + const val PUBLIC_KEY_B64 = "9i85Be3SvkvdAIiCgk9llHinMRKS0OTdK6r+jEwCi2udUwM" + } +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/KeyBaseUnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/KeyBaseUnitTest.kt new file mode 100644 index 000000000..2d25aef6b --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/KeyBaseUnitTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.base32.Base32Default +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import kotlin.test.* + +abstract class KeyBaseUnitTest(protected val expectedAlgorithm: String) { + + protected abstract val publicKey: Key.Public + protected abstract val privateKey: Key.Private + + @Test + fun givenKey_whenAlgorithm_thenIsAsExpected() { + assertEquals(expectedAlgorithm, publicKey.algorithm()) + assertEquals(expectedAlgorithm, privateKey.algorithm()) + } + + @Test + fun givenKey_whenBase16_thenIsSingleLineUppercase() { + val public = publicKey.base16() + assertEquals(1, public.lines().size) + public.forEach { assertEquals(it, it.uppercaseChar()) } + + val private = privateKey.base16() + assertEquals(1, private.lines().size) + private.forEach { assertEquals(it, it.uppercaseChar()) } + } + + @Test + fun givenKey_whenBase32_thenIsSingleLineUppercase() { + val public = publicKey.base32() + assertEquals(1, public.lines().size) + public.forEach { assertEquals(it, it.uppercaseChar()) } + + val private = privateKey.base32() + assertEquals(1, private.lines().size) + private.forEach { assertEquals(it, it.uppercaseChar()) } + } + + @Test + fun givenKey_whenBase64_thenIsSingleLine() { + assertEquals(1, publicKey.base64().lines().size) + assertEquals(1, privateKey.base64().lines().size) + } + + @Test + fun givenPublicKey_whenEncoded_thenReturnedBytesAreACopy() { + val bytes = publicKey.encoded() + bytes.fill(5) + + try { + assertContentEquals(publicKey.encoded(), bytes) + throw IllegalStateException() + } catch (_: AssertionError) { + // pass + } + } + + @Test + fun givenPrivateKey_whenEncoded_thenReturnedBytesAreACopy() { + val bytes = privateKey.encodedOrThrow() + bytes.fill(5) + + try { + assertContentEquals(privateKey.encodedOrThrow(), bytes) + throw IllegalStateException() + } catch (_: AssertionError) { + // pass + } + } + + @Test + fun givenPublicKey_whenEncodings_thenAreAsExpected() { + val bytes = publicKey.encoded() + + assertEquals(bytes.encodeToString(Base16()), publicKey.base16()) + assertEquals(bytes.encodeToString(Base32Default { padEncoded = false }), publicKey.base32()) + assertEquals(bytes.encodeToString(Base64 { padEncoded = false }), publicKey.base64()) + } + + @Test + fun givenPrivateKey_whenEncodings_thenAreAsExpected() { + val bytes = privateKey.encoded()!! + + assertEquals(bytes.encodeToString(Base16()), privateKey.base16()) + assertEquals(bytes.encodeToString(Base32Default { padEncoded = false }), privateKey.base32()) + assertEquals(bytes.encodeToString(Base64 { padEncoded = false }), privateKey.base64()) + } + + @Test + fun givenPrivateKey_whenDestroyed_thenThingsReturnAsExpected() { + assertFalse(privateKey.isDestroyed()) + privateKey.destroy() + assertTrue(privateKey.isDestroyed()) + assertNull(privateKey.base16OrNull()) + assertNull(privateKey.base32OrNull()) + assertNull(privateKey.base64OrNull()) + assertNull(privateKey.encoded()) + + assertFailsWith { privateKey.base16() } + assertFailsWith { privateKey.base32() } + assertFailsWith { privateKey.base64() } + + // Does not throw b/c that'd be awful + privateKey.toString() + } + + @Test + fun givenPublicKey_whenToString_thenFormatIsAsExpected() { + val expected = expectedAlgorithm + ".PublicKey[" + publicKey.base32() + "]@" + publicKey.hashCode() + assertEquals(expected, publicKey.toString()) + } + + @Test + fun givenPrivateKey_whenToString_thenFormatIsAsExpected() { + val expected = expectedAlgorithm + ".PrivateKey[REDACTED]@" + privateKey.hashCode() + assertEquals(expected, privateKey.toString()) + } +} diff --git a/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/X25519UnitTest.kt b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/X25519UnitTest.kt new file mode 100644 index 000000000..4dfe27c09 --- /dev/null +++ b/library/runtime-api/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/X25519UnitTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import io.matthewnelson.kmp.tor.runtime.api.key.X25519.PrivateKey.Companion.toX25519PrivateKey +import io.matthewnelson.kmp.tor.runtime.api.key.X25519.PrivateKey.Companion.toX25519PrivateKeyOrNull +import io.matthewnelson.kmp.tor.runtime.api.key.X25519.PublicKey.Companion.toX25519PublicKey +import io.matthewnelson.kmp.tor.runtime.api.key.X25519.PublicKey.Companion.toX25519PublicKeyOrNull +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNull + +class X25519UnitTest: AuthKeyBaseUnitTest( + keyType = X25519, + expectedAlgorithm = "x25519", +) { + + override val publicKey: X25519.PublicKey = PUBLIC_KEY_B64.toX25519PublicKey() + override val privateKey: X25519.PrivateKey = PRIVATE_KEY_B64.toX25519PrivateKey() + + @Test + fun givenX25519PublicKey_whenEncodedString_thenToKeyIsSuccessful() { + val expected = PUBLIC_KEY_B16.decodeToByteArray(Base16) + + assertContentEquals(expected, PUBLIC_KEY_B16.toX25519PublicKey().encoded()) + assertContentEquals(expected, PUBLIC_KEY_B32.toX25519PublicKey().encoded()) + assertContentEquals(expected, PUBLIC_KEY_B64.toX25519PublicKey().encoded()) + } + + @Test + fun givenX25519PrivateKey_whenEncodedString_thenToKeyIsSuccessful() { + val expected = PRIVATE_KEY_B16.decodeToByteArray(Base16) + + assertContentEquals(expected, PRIVATE_KEY_B16.toX25519PrivateKey().encoded()) + assertContentEquals(expected, PRIVATE_KEY_B32.toX25519PrivateKey().encoded()) + assertContentEquals(expected, PRIVATE_KEY_B64.toX25519PrivateKey().encoded()) + } + + @Test + fun givenX25519PublicKey_whenBytes_thenToKeyIsSuccessful() { + PUBLIC_KEY_B64.decodeToByteArray(Base64.Default).toX25519PublicKey() + } + + @Test + fun givenX25519PrivateKey_whenBytes_thenToKeyIsSuccessful() { + PRIVATE_KEY_B64.decodeToByteArray(Base64.Default).toX25519PrivateKey() + } + + @Test + fun givenInvalidInput_whenToPublicKey_thenReturnsNull() { + assertNull(PUBLIC_KEY_B16.dropLast(2).toX25519PublicKeyOrNull()) + assertNull(PUBLIC_KEY_B16.dropLast(2).decodeToByteArray(Base16).toX25519PublicKeyOrNull()) + } + + @Test + fun givenInvalidInput_whenToPrivateKey_thenReturnsNull() { + assertNull(PRIVATE_KEY_B16.dropLast(2).toX25519PrivateKeyOrNull()) + assertNull(PRIVATE_KEY_B16.dropLast(2).decodeToByteArray(Base16).toX25519PrivateKeyOrNull()) + } + + companion object { + const val PRIVATE_KEY_B16 = "70663288D322A66BBEB1D6C60FE0A3207A8C99643A4FD321150A8CBECB476B63" + const val PRIVATE_KEY_B32 = "OBTDFCGTEKTGXPVR23DA7YFDEB5IZGLEHJH5GIIVBKGL5S2HNNRQ" + const val PRIVATE_KEY_B64 = "cGYyiNMipmu+sdbGD+CjIHqMmWQ6T9MhFQqMvstHa2M" + + const val PUBLIC_KEY_B16 = "5D5F7F58977822131E5CE1B3F351E0F2C3546BB5C7622269BECC731BA4490545" + const val PUBLIC_KEY_B32 = "LVPX6WEXPARBGHS44GZ7GUPA6LBVI25VY5RCE2N6ZRZRXJCJAVCQ" + const val PUBLIC_KEY_B64 = "XV9/WJd4IhMeXOGz81Hg8sNUa7XHYiJpvsxzG6RJBUU" + } +} diff --git a/library/runtime-api/src/jvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/Key.kt b/library/runtime-api/src/jvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/Key.kt new file mode 100644 index 000000000..e31e35e5c --- /dev/null +++ b/library/runtime-api/src/jvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/Key.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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. + **/ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.base32.Base32 +import io.matthewnelson.encoding.base32.Base32Default +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.core.resource.SynchronizedObject +import io.matthewnelson.kmp.tor.core.resource.synchronized +import io.matthewnelson.kmp.tor.runtime.api.Destroyable +import kotlin.concurrent.Volatile + +public actual sealed class Key private actual constructor(): java.security.Key { + + public actual abstract fun algorithm(): String + public actual abstract fun encoded(): ByteArray? + + final override fun getAlgorithm(): String = algorithm() + final override fun getEncoded(): ByteArray? = encoded() + // For now keys are always raw but could be bumped up + // to commonMain if need be + final override fun getFormat(): String = "RAW" + + public actual sealed class Public actual constructor(): Key(), java.security.PublicKey { + public actual abstract override fun encoded(): ByteArray + + public actual abstract fun base16(): String + public actual abstract fun base32(): String + public actual abstract fun base64(): String + + public actual final override fun toString(): String = "${algorithm()}.PublicKey[${base32()}]@${hashCode()}" + } + + public actual sealed class Private actual constructor( + private val key: ByteArray, + ) : Key(), Destroyable, java.security.PrivateKey { + + @Volatile + private var isDestroyed = false + @OptIn(InternalKmpTorApi::class) + private val lock = SynchronizedObject() + + public actual final override fun destroy() { + if (isDestroyed) return + + @OptIn(InternalKmpTorApi::class) + synchronized(lock) { + if (isDestroyed) return@synchronized + key.fill(0) + isDestroyed = true + } + } + + public actual final override fun isDestroyed(): Boolean = isDestroyed + + public actual final override fun encoded(): ByteArray? = withKeyOrNull { it.copyOf() } + + @Throws(IllegalStateException::class) + public actual fun encodedOrThrow(): ByteArray = encoded() ?: throw IllegalStateException("isDestroyed[$isDestroyed]") + + @Throws(IllegalStateException::class) + public actual fun base16(): String = base16OrNull() ?: throw IllegalStateException("isDestroyed[$isDestroyed]") + @Throws(IllegalStateException::class) + public actual fun base32(): String = base32OrNull() ?: throw IllegalStateException("isDestroyed[$isDestroyed]") + @Throws(IllegalStateException::class) + public actual fun base64(): String = base64OrNull() ?: throw IllegalStateException("isDestroyed[$isDestroyed]") + + public actual fun base16OrNull(): String? = withKeyOrNull { it.encodeToString(BASE_16) } + public actual fun base32OrNull(): String? = withKeyOrNull { it.encodeToString(BASE_32) } + public actual fun base64OrNull(): String? = withKeyOrNull { it.encodeToString(BASE_64) } + @OptIn(InternalKmpTorApi::class) + protected actual fun withKeyOrNull( + block: (key: ByteArray) -> T + ): T? = synchronized(lock) { + if (isDestroyed) return@synchronized null + block(key) + } + + public actual final override fun toString(): String = "${algorithm()}.PrivateKey[REDACTED]@${hashCode()}" + } + + protected actual companion object { + internal actual val BASE_16: Base16 = Base16() + internal actual val BASE_32: Base32.Default = Base32Default { padEncoded = false } + internal actual val BASE_64: Base64 = Base64 { padEncoded = false } + } +} diff --git a/library/runtime-api/src/nonJvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/Key.kt b/library/runtime-api/src/nonJvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/Key.kt new file mode 100644 index 000000000..91018e0fe --- /dev/null +++ b/library/runtime-api/src/nonJvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/api/key/Key.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 Matthew Nelson + * + * 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 + * + * https://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. + **/ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package io.matthewnelson.kmp.tor.runtime.api.key + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.base32.Base32 +import io.matthewnelson.encoding.base32.Base32Default +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.core.resource.SynchronizedObject +import io.matthewnelson.kmp.tor.core.resource.synchronized +import io.matthewnelson.kmp.tor.runtime.api.Destroyable +import kotlin.concurrent.Volatile + +public actual sealed class Key private actual constructor() { + + public actual abstract fun algorithm(): String + public actual abstract fun encoded(): ByteArray? + + public actual sealed class Public actual constructor(): Key() { + public actual abstract override fun encoded(): ByteArray + + public actual abstract fun base16(): String + public actual abstract fun base32(): String + public actual abstract fun base64(): String + + public actual final override fun toString(): String = "${algorithm()}.PublicKey[${base32()}]@${hashCode()}" + } + + public actual sealed class Private actual constructor( + private val key: ByteArray, + ): Key(), Destroyable { + + @Volatile + private var isDestroyed = false + @OptIn(InternalKmpTorApi::class) + private val lock = SynchronizedObject() + + public actual final override fun destroy() { + if (isDestroyed) return + + @OptIn(InternalKmpTorApi::class) + synchronized(lock) { + if (isDestroyed) return@synchronized + key.fill(0) + isDestroyed = true + } + } + + public actual final override fun isDestroyed(): Boolean = isDestroyed + + public actual final override fun encoded(): ByteArray? = withKeyOrNull { it.copyOf() } + + @Throws(IllegalStateException::class) + public actual fun encodedOrThrow(): ByteArray = encoded() ?: throw IllegalStateException("isDestroyed[$isDestroyed]") + + @Throws(IllegalStateException::class) + public actual fun base16(): String = base16OrNull() ?: throw IllegalStateException("isDestroyed[$isDestroyed]") + @Throws(IllegalStateException::class) + public actual fun base32(): String = base32OrNull() ?: throw IllegalStateException("isDestroyed[$isDestroyed]") + @Throws(IllegalStateException::class) + public actual fun base64(): String = base64OrNull() ?: throw IllegalStateException("isDestroyed[$isDestroyed]") + + public actual fun base16OrNull(): String? = withKeyOrNull { it.encodeToString(BASE_16) } + public actual fun base32OrNull(): String? = withKeyOrNull { it.encodeToString(BASE_32) } + public actual fun base64OrNull(): String? = withKeyOrNull { it.encodeToString(BASE_64) } + + @OptIn(InternalKmpTorApi::class) + protected actual fun withKeyOrNull( + block: (key: ByteArray) -> T + ): T? = synchronized(lock) { + if (isDestroyed) return@synchronized null + block(key) + } + + public actual final override fun toString(): String = "${algorithm()}.PrivateKey[REDACTED]@${hashCode()}" + } + + protected actual companion object { + internal actual val BASE_16: Base16 = Base16() + internal actual val BASE_32: Base32.Default = Base32Default { padEncoded = false } + internal actual val BASE_64: Base64 = Base64 { padEncoded = false } + } +}