diff --git a/composeApp/src/jvmMain/composeResources/files/aboutlibraries.json b/composeApp/src/jvmMain/composeResources/files/aboutlibraries.json index 5767aec..03964f7 100644 --- a/composeApp/src/jvmMain/composeResources/files/aboutlibraries.json +++ b/composeApp/src/jvmMain/composeResources/files/aboutlibraries.json @@ -1087,9 +1087,6 @@ { "name": "Geir Magnusson Jr." }, - { - "name": "Gary Gregory" - }, { "name": "Stian Soiland-Reyes" }, @@ -1114,6 +1111,10 @@ { "name": "Yoav Shapira" }, + { + "organisationUrl": "https://www.apache.org/", + "name": "Gary Gregory" + }, { "name": "Rob Tompkins" }, @@ -1148,15 +1149,15 @@ "name": "Benedikt Ritter" } ], - "artifactVersion": "1.9.4", + "artifactVersion": "1.11.0", "description": "Apache Commons BeanUtils provides an easy-to-use but flexible wrapper around reflection and introspection.", "scm": { - "connection": "scm:svn:http://svn.apache.org/repos/asf/commons/proper/beanutils/tags/BEANUTILS_1_9_3_RC3", - "url": "http://svn.apache.org/viewvc/commons/proper/beanutils/tags/BEANUTILS_1_9_3_RC3", - "developerConnection": "scm:svn:https://svn.apache.org/repos/asf/commons/proper/beanutils/tags/BEANUTILS_1_9_3_RC3" + "connection": "scm:git:https://gitbox.apache.org/repos/asf?p=commons-beanutils.git", + "url": "https://gitbox.apache.org/repos/asf?p=commons-beanutils.git", + "developerConnection": "scm:git:https://gitbox.apache.org/repos/asf?p=commons-beanutils.git" }, "name": "Apache Commons BeanUtils", - "website": "https://commons.apache.org/proper/commons-beanutils/", + "website": "https://commons.apache.org/proper/commons-beanutils", "licenses": [ "Apache-2.0" ], @@ -1326,8 +1327,8 @@ "name": "Rodney Waldhoff" } ], - "artifactVersion": "1.3.0", - "description": "Apache Commons Logging is a thin adapter allowing configurable bridging to other,\n well known logging systems.", + "artifactVersion": "1.3.5", + "description": "Apache Commons Logging is a thin adapter allowing configurable bridging to other,\n well-known logging systems.", "scm": { "connection": "scm:git:https://gitbox.apache.org/repos/asf/commons-logging", "url": "https://gitbox.apache.org/repos/asf/commons-logging", @@ -1399,15 +1400,15 @@ "name": "Benedikt Ritter" } ], - "artifactVersion": "1.8.0", - "description": "Apache Commons Validator provides the building blocks for both client side validation and server side data validation.\n It may be used standalone or with a framework like Struts.", + "artifactVersion": "1.10.1", + "description": "Apache Commons Validator provides the building blocks for both client-side and server-side data validation.\n It may be used standalone or with a framework like Struts.", "scm": { - "connection": "scm:git:https://gitbox.apache.org/repos/asf/commons-validator", - "url": "https://gitbox.apache.org/repos/asf/commons-validator", - "developerConnection": "scm:git:https://gitbox.apache.org/repos/asf/commons-validator" + "connection": "scm:git:https://gitbox.apache.org/repos/asf/commons-validator.git/commons-validator", + "url": "https://gitbox.apache.org/repos/asf?p=commons-validator.git/commons-validator", + "developerConnection": "scm:git:https://gitbox.apache.org/repos/asf/commons-validator.git/commons-validator" }, "name": "Apache Commons Validator", - "website": "http://commons.apache.org/proper/commons-validator/", + "website": "https://commons.apache.org/proper/commons-validator/", "licenses": [ "Apache-2.0" ], @@ -1460,90 +1461,6 @@ "Apache-2.0" ] }, - { - "uniqueId": "io.github.andreypfau:curve25519-kotlin", - "funding": [ - - ], - "developers": [ - { - "name": "Andrey Pfau" - } - ], - "artifactVersion": "0.0.8", - "description": "A pure Kotlin/Multiplatform implementation of group operations on Curve25519.", - "scm": { - "url": "https://github.com/andreypfau/curve25519-kotlin" - }, - "name": "curve25519-kotlin", - "website": "https://github.com/andreypfau/curve25519-kotlin", - "licenses": [ - "Apache-2.0" - ] - }, - { - "uniqueId": "io.github.andreypfau:kotlinx-crypto-digest", - "funding": [ - - ], - "developers": [ - { - "name": "Andrey Pfau" - } - ], - "artifactVersion": "0.0.4", - "description": "A multiplatform Kotlin library providing basic cryptographic functions and primitives", - "scm": { - "url": "https://github.com/andreypfau/kotlinx-crypto" - }, - "name": "kotlinx-crypto-digest", - "website": "https://github.com/andreypfau/kotlinx-crypto", - "licenses": [ - "Apache-2.0" - ] - }, - { - "uniqueId": "io.github.andreypfau:kotlinx-crypto-sha2", - "funding": [ - - ], - "developers": [ - { - "name": "Andrey Pfau" - } - ], - "artifactVersion": "0.0.4", - "description": "A multiplatform Kotlin library providing basic cryptographic functions and primitives", - "scm": { - "url": "https://github.com/andreypfau/kotlinx-crypto" - }, - "name": "kotlinx-crypto-sha2", - "website": "https://github.com/andreypfau/kotlinx-crypto", - "licenses": [ - "Apache-2.0" - ] - }, - { - "uniqueId": "io.github.andreypfau:kotlinx-crypto-subtle", - "funding": [ - - ], - "developers": [ - { - "name": "Andrey Pfau" - } - ], - "artifactVersion": "0.0.4", - "description": "A multiplatform Kotlin library providing basic cryptographic functions and primitives", - "scm": { - "url": "https://github.com/andreypfau/kotlinx-crypto" - }, - "name": "kotlinx-crypto-subtle", - "website": "https://github.com/andreypfau/kotlinx-crypto", - "licenses": [ - "Apache-2.0" - ] - }, { "uniqueId": "io.github.dokar3:sonner", "funding": [ @@ -2583,6 +2500,27 @@ "name": "The Apache Software Foundation" } }, + { + "uniqueId": "org.bouncycastle:bcprov-jdk18on", + "funding": [ + + ], + "developers": [ + { + "name": "The Legion of the Bouncy Castle Inc." + } + ], + "artifactVersion": "1.84", + "description": "The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms. This jar contains the JCA/JCE provider and low-level API for the BC Java version 1.84 for Java 1.8 and later.", + "scm": { + "url": "https://github.com/bcgit/bc-java" + }, + "name": "Bouncy Castle Provider", + "website": "https://www.bouncycastle.org/download/bouncy-castle-java/", + "licenses": [ + "73252b46f36df25ef51a7994de439aea" + ] + }, { "uniqueId": "org.checkerframework:checker-qual", "funding": [ @@ -5141,6 +5079,11 @@ "url": "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html", "name": "GNU Lesser General Public License" }, + "73252b46f36df25ef51a7994de439aea": { + "hash": "73252b46f36df25ef51a7994de439aea", + "url": "https://www.bouncycastle.org/licence.html", + "name": "Bouncy Castle Licence" + }, "8cd94e3ff25fb90fa794464eee297b17": { "hash": "8cd94e3ff25fb90fa794464eee297b17", "url": "https://www.mozilla.org/en-US/MPL/1.1/", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f47036d..94933e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,6 @@ kotlinx-coroutines = "1.10.2" material3 = "1.11.0-alpha01" serialization = "1.9.0" cryptoRand = "0.6.0" -curve25519Kotlin = "0.0.8" koin = "4.2.0-beta2" ktor = "3.3.3" conveyor = "1.13" @@ -28,6 +27,7 @@ sonner = "0.3.9" materialKolor = "4.1.1" nativeTray = "1.1.0" mps = "1.3.0" +bc = "1.84" # Files kmpIo = "0.3.0" @@ -87,7 +87,7 @@ kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx- # cryto crypto-rand = { module = "org.kotlincrypto.random:crypto-rand", version.ref = "cryptoRand" } -curve25519-kotlin = { module = "io.github.andreypfau:curve25519-kotlin", version.ref = "curve25519Kotlin" } +bouncycastle = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bc" } # DI koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } diff --git a/parser/build.gradle.kts b/parser/build.gradle.kts index 1b9b7bf..4b9f659 100644 --- a/parser/build.gradle.kts +++ b/parser/build.gradle.kts @@ -11,7 +11,7 @@ dependencies { implementation(libs.kotlinx.serialization.core) implementation(libs.crypto.rand) - implementation(libs.curve25519.kotlin) + implementation(libs.bouncycastle) implementation(libs.human.readable) implementation(libs.kotlinx.datetime) @@ -30,7 +30,7 @@ publishing { register("release") { groupId = "com.zaneschepke.wireguardautotunnel" artifactId = "amneziawg-parser" - version = "1.0.7" + version = "1.1.0" from(components["java"]) pom { name.set("AmneziaWG Parser") diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/Config.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/Config.kt index ade11ce..e9b0fe5 100644 --- a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/Config.kt +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/Config.kt @@ -14,9 +14,13 @@ import kotlinx.serialization.Serializable data class Config( @SerialName("Interface") val `interface`: InterfaceSection, @SerialName("Peer") val peers: List = emptyList(), + val name: String? = null, val headerComments: List = emptyList(), ) { + fun withName(newName: String?): Config = + copy(name = newName?.trim()?.takeIf { it.isNotBlank() }) + @Throws(ConfigParseException::class) fun validate() { `interface`.validate() @@ -25,6 +29,7 @@ data class Config( fun asQuickString(): String = buildString { + name?.let { appendLine("# Name = $it") } headerComments.forEach { appendLine(it) } ConfigFormatter.appendInterfaceSection(this, `interface`) peers.forEach { ConfigFormatter.appendPeerSection(this, it) } @@ -92,8 +97,28 @@ data class Config( val parts = raw.split("=", limit = 2) if (parts.size == 2) { - val key = parts[0].trim() - var value = parts[1].trim() + val rawKey = parts[0].trim() + val lowerKey = rawKey.lowercase() + + // Normalize wireguard keys + val key = + when (lowerKey) { + "allowedips" -> "AllowedIPs" + "address" -> "Address" + "dns" -> "DNS" + "presharedkey" -> "PresharedKey" + "privatekey" -> "PrivateKey" + "publickey" -> "PublicKey" + "listenport" -> "ListenPort" + "persistentkeepalive" -> "PersistentKeepalive" + "mtu" -> "MTU" + "table" -> "Table" + "saveconfig" -> "SaveConfig" + else -> rawKey + } + + // Strip inline comments before trimming + var value = parts[1].substringBefore("#").substringBefore(";").trim() if (currentSectionMap === interfaceMap) { when (key) { @@ -107,14 +132,13 @@ data class Config( } } - // remove whitespaces + // Remove whitespaces if ( key in listOf( "PrivateKey", "PublicKey", "PresharedKey", - "PreSharedKey", "H1", "H2", "H3", @@ -123,12 +147,55 @@ data class Config( ) { value = value.replace(Regex("\\s+"), "") } - currentSectionMap?.put(key, value) + + when (key) { + "AllowedIPs", + "Address", + "DNS" -> { + val existing = currentSectionMap?.get(key) + currentSectionMap?.put( + key, + if (existing.isNullOrEmpty()) value else "$existing, $value", + ) + } + "PresharedKey" -> { + currentSectionMap?.put("PresharedKey", value) + currentSectionMap?.put("PreSharedKey", value) + } + else -> { + currentSectionMap?.put(key, value) + } + } } } + val extractedName = + headerComments.firstOrNull()?.let { firstComment -> + val content = firstComment.trimStart('#', ' ', '\t').trim() + + when { + content.startsWith("Name", ignoreCase = true) -> { + content + .substringAfter("Name", "") + .trimStart('=', ' ', '\t') + .trim() + .takeIf { it.isNotBlank() } + } + else -> null + } + } + + // prevent name duplicates + val cleanedHeaderComments = + if (extractedName != null) { + headerComments.drop(1) + } else { + headerComments + } + return Config( - headerComments = headerComments, + headerComments = cleanedHeaderComments, + name = extractedName, `interface` = buildInterface(interfaceMap, scripts.build(), interfaceComments), peers = peerMaps.map { (map, comments) -> buildPeer(map, comments) }, ) diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/crypto/Key.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/crypto/Key.kt index 7bae1cf..d2668b3 100644 --- a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/crypto/Key.kt +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/crypto/Key.kt @@ -1,37 +1,16 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright © 2026 WG Tunnel. -// Adapted from WireGuard LLC. - +// com.zaneschepke.wireguardautotunnel.parser.crypto.Key.kt package com.zaneschepke.wireguardautotunnel.parser.crypto -import io.github.andreypfau.curve25519.x25519.X25519 import kotlin.experimental.and -import kotlin.experimental.or +import org.bouncycastle.math.ec.rfc7748.X25519 +import org.bouncycastle.util.encoders.Base64 import org.kotlincrypto.random.CryptoRand -class KeyFormatException : Exception { - constructor( - format: Key.Format, - type: Key.Type, - ) : super("Invalid key format: $format, type: $type") -} - class Key private constructor(private val key: ByteArray) { fun getBytes(): ByteArray = key.copyOf() - fun toBase64(): String { - val output = CharArray(Format.BASE64.length) - var i = 0 - while (i < key.size / 3) { - encodeBase64(key, i * 3, output, i * 4) - i++ - } - val endSegment = byteArrayOf(key[i * 3], key[i * 3 + 1], 0) - encodeBase64(endSegment, 0, output, i * 4) - output[Format.BASE64.length - 1] = '=' - return output.concatToString() - } + fun toBase64(): String = Base64.encode(key).decodeToString() override fun equals(other: Any?): Boolean { if (this === other) return true @@ -39,113 +18,32 @@ class Key private constructor(private val key: ByteArray) { return key.contentEquals(other.key) } - override fun hashCode(): Int { - var ret = 0 - var i = 0 - while (i < key.size / 4) { - ret = - ret xor - ((key[i * 4 + 0].toInt() shr 0) + - (key[i * 4 + 1].toInt() shr 8) + - (key[i * 4 + 2].toInt() shr 16) + - (key[i * 4 + 3].toInt() shr 24)) - i++ - } - return ret - } + override fun hashCode(): Int = key.contentHashCode() companion object { fun fromBase64(str: String): Key { - val input = str.toCharArray() - if (input.size != Format.BASE64.length || input[Format.BASE64.length - 1] != '=') { - throw KeyFormatException(Format.BASE64, Type.LENGTH) - } - val key = ByteArray(Format.BINARY.length) - var ret = 0 - var i = 0 - while (i < key.size / 3) { - val value = decodeBase64(input, i * 4) - ret = ret or (value ushr 31) - key[i * 3] = ((value ushr 16) and 0xff).toByte() - key[i * 3 + 1] = ((value ushr 8) and 0xff).toByte() - key[i * 3 + 2] = (value and 0xff).toByte() - i++ - } - val endSegment = charArrayOf(input[i * 4], input[i * 4 + 1], input[i * 4 + 2], 'A') - val value = decodeBase64(endSegment, 0) - ret = ret or ((value ushr 31) or (value and 0xff)) - key[i * 3] = ((value ushr 16) and 0xff).toByte() - key[i * 3 + 1] = ((value ushr 8) and 0xff).toByte() - - if (ret != 0) { - throw KeyFormatException(Format.BASE64, Type.CONTENTS) - } - return Key(key) - } - - fun fromBytes(bytes: ByteArray): Key { - if (bytes.size != Format.BINARY.length) { - throw KeyFormatException(Format.BINARY, Type.LENGTH) - } + val bytes = Base64.decode(str) + if (bytes.size != 32) throw KeyFormatException(Format.BINARY, Type.LENGTH) return Key(bytes) } + fun fromBytes(bytes: ByteArray): Key { + if (bytes.size != 32) throw KeyFormatException(Format.BINARY, Type.LENGTH) + return Key(bytes.copyOf()) + } + fun generatePrivateKey(): Key { - val privateKey = ByteArray(Format.BINARY.length) - CryptoRand.nextBytes(privateKey) - privateKey[0] = privateKey[0] and 248.toByte() - privateKey[31] = privateKey[31] and 127.toByte() - privateKey[31] = privateKey[31] or 64.toByte() - return Key(privateKey) + val priv = ByteArray(32) + CryptoRand.nextBytes(priv) + priv[0] = priv[0] and 248.toByte() + priv[31] = (priv[31].toInt() and 127 or 64).toByte() + return Key(priv) } fun generatePublicKey(privateKey: Key): Key { - val publicKey = ByteArray(Format.BINARY.length) - X25519.x25519(privateKey.getBytes(), output = publicKey) - return Key(publicKey) - } - - private fun decodeBase64(src: CharArray, srcOffset: Int): Int { - var value = 0 - for (i in 0 until 4) { - val c = src[i + srcOffset].code - value = - value or - (-1 + - ((((('A'.code - 1) - c) and (c - ('Z'.code + 1))) ushr 8) and - (c - 64)) + - ((((('a'.code - 1) - c) and (c - ('z'.code + 1))) ushr 8) and - (c - 70)) + - ((((('0'.code - 1) - c) and (c - ('9'.code + 1))) ushr 8) and (c + 5)) + - (((('+'.code - 1) - c) and (c - ('+'.code + 1))) ushr 8 and 63) + - (((('/'.code - 1) - c) and (c - ('/'.code + 1))) ushr 8 and 64)) shl - (18 - 6 * i) - } - return value - } - - private fun encodeBase64(src: ByteArray, srcOffset: Int, dest: CharArray, destOffset: Int) { - val input = - byteArrayOf( - (src[srcOffset].toInt() shr 2 and 63).toByte(), - ((src[srcOffset].toInt() shl - 4 or - (src[1 + srcOffset].toInt() and 0xff ushr 4)) and 63) - .toByte(), - ((src[1 + srcOffset].toInt() shl - 2 or - (src[2 + srcOffset].toInt() and 0xff ushr 6)) and 63) - .toByte(), - (src[2 + srcOffset].toInt() and 63).toByte(), - ) - for (i in 0 until 4) { - dest[i + destOffset] = - (input[i].toInt() + 'A'.code + (((25 - input[i].toInt()) ushr 8) and 6) - - (((51 - input[i].toInt()) ushr 8) and 75) - - (((61 - input[i].toInt()) ushr 8) and 15) + - (((62 - input[i].toInt()) ushr 8) and 3)) - .toChar() - } + val pub = ByteArray(32) + X25519.scalarMultBase(privateKey.getBytes(), 0, pub, 0) + return Key(pub) } } @@ -160,3 +58,10 @@ class Key private constructor(private val key: ByteArray) { CONTENTS, } } + +class KeyFormatException : Exception { + constructor( + format: Key.Format, + type: Key.Type, + ) : super("Invalid key format: $format, type: $type") +}