fix: parser duplicate keys, key rotation, name comment

This commit is contained in:
zaneschepke
2026-06-01 13:47:34 -04:00
parent ff22df24c6
commit 12c8634136
5 changed files with 147 additions and 232 deletions
@@ -1087,9 +1087,6 @@
{ {
"name": "Geir Magnusson Jr." "name": "Geir Magnusson Jr."
}, },
{
"name": "Gary Gregory"
},
{ {
"name": "Stian Soiland-Reyes" "name": "Stian Soiland-Reyes"
}, },
@@ -1114,6 +1111,10 @@
{ {
"name": "Yoav Shapira" "name": "Yoav Shapira"
}, },
{
"organisationUrl": "https://www.apache.org/",
"name": "Gary Gregory"
},
{ {
"name": "Rob Tompkins" "name": "Rob Tompkins"
}, },
@@ -1148,15 +1149,15 @@
"name": "Benedikt Ritter" "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.", "description": "Apache Commons BeanUtils provides an easy-to-use but flexible wrapper around reflection and introspection.",
"scm": { "scm": {
"connection": "scm:svn:http://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": "http://svn.apache.org/viewvc/commons/proper/beanutils/tags/BEANUTILS_1_9_3_RC3", "url": "https://gitbox.apache.org/repos/asf?p=commons-beanutils.git",
"developerConnection": "scm:svn:https://svn.apache.org/repos/asf/commons/proper/beanutils/tags/BEANUTILS_1_9_3_RC3" "developerConnection": "scm:git:https://gitbox.apache.org/repos/asf?p=commons-beanutils.git"
}, },
"name": "Apache Commons BeanUtils", "name": "Apache Commons BeanUtils",
"website": "https://commons.apache.org/proper/commons-beanutils/", "website": "https://commons.apache.org/proper/commons-beanutils",
"licenses": [ "licenses": [
"Apache-2.0" "Apache-2.0"
], ],
@@ -1326,8 +1327,8 @@
"name": "Rodney Waldhoff" "name": "Rodney Waldhoff"
} }
], ],
"artifactVersion": "1.3.0", "artifactVersion": "1.3.5",
"description": "Apache Commons Logging is a thin adapter allowing configurable bridging to other,\n well known logging systems.", "description": "Apache Commons Logging is a thin adapter allowing configurable bridging to other,\n well-known logging systems.",
"scm": { "scm": {
"connection": "scm:git:https://gitbox.apache.org/repos/asf/commons-logging", "connection": "scm:git:https://gitbox.apache.org/repos/asf/commons-logging",
"url": "https://gitbox.apache.org/repos/asf/commons-logging", "url": "https://gitbox.apache.org/repos/asf/commons-logging",
@@ -1399,15 +1400,15 @@
"name": "Benedikt Ritter" "name": "Benedikt Ritter"
} }
], ],
"artifactVersion": "1.8.0", "artifactVersion": "1.10.1",
"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.", "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": { "scm": {
"connection": "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/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" "developerConnection": "scm:git:https://gitbox.apache.org/repos/asf/commons-validator.git/commons-validator"
}, },
"name": "Apache Commons Validator", "name": "Apache Commons Validator",
"website": "http://commons.apache.org/proper/commons-validator/", "website": "https://commons.apache.org/proper/commons-validator/",
"licenses": [ "licenses": [
"Apache-2.0" "Apache-2.0"
], ],
@@ -1460,90 +1461,6 @@
"Apache-2.0" "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", "uniqueId": "io.github.dokar3:sonner",
"funding": [ "funding": [
@@ -2583,6 +2500,27 @@
"name": "The Apache Software Foundation" "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", "uniqueId": "org.checkerframework:checker-qual",
"funding": [ "funding": [
@@ -5141,6 +5079,11 @@
"url": "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html", "url": "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html",
"name": "GNU Lesser General Public License" "name": "GNU Lesser General Public License"
}, },
"73252b46f36df25ef51a7994de439aea": {
"hash": "73252b46f36df25ef51a7994de439aea",
"url": "https://www.bouncycastle.org/licence.html",
"name": "Bouncy Castle Licence"
},
"8cd94e3ff25fb90fa794464eee297b17": { "8cd94e3ff25fb90fa794464eee297b17": {
"hash": "8cd94e3ff25fb90fa794464eee297b17", "hash": "8cd94e3ff25fb90fa794464eee297b17",
"url": "https://www.mozilla.org/en-US/MPL/1.1/", "url": "https://www.mozilla.org/en-US/MPL/1.1/",
+2 -2
View File
@@ -10,7 +10,6 @@ kotlinx-coroutines = "1.10.2"
material3 = "1.11.0-alpha01" material3 = "1.11.0-alpha01"
serialization = "1.9.0" serialization = "1.9.0"
cryptoRand = "0.6.0" cryptoRand = "0.6.0"
curve25519Kotlin = "0.0.8"
koin = "4.2.0-beta2" koin = "4.2.0-beta2"
ktor = "3.3.3" ktor = "3.3.3"
conveyor = "1.13" conveyor = "1.13"
@@ -28,6 +27,7 @@ sonner = "0.3.9"
materialKolor = "4.1.1" materialKolor = "4.1.1"
nativeTray = "1.1.0" nativeTray = "1.1.0"
mps = "1.3.0" mps = "1.3.0"
bc = "1.84"
# Files # Files
kmpIo = "0.3.0" kmpIo = "0.3.0"
@@ -87,7 +87,7 @@ kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-
# cryto # cryto
crypto-rand = { module = "org.kotlincrypto.random:crypto-rand", version.ref = "cryptoRand" } 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 # DI
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
+2 -2
View File
@@ -11,7 +11,7 @@ dependencies {
implementation(libs.kotlinx.serialization.core) implementation(libs.kotlinx.serialization.core)
implementation(libs.crypto.rand) implementation(libs.crypto.rand)
implementation(libs.curve25519.kotlin) implementation(libs.bouncycastle)
implementation(libs.human.readable) implementation(libs.human.readable)
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
@@ -30,7 +30,7 @@ publishing {
register<MavenPublication>("release") { register<MavenPublication>("release") {
groupId = "com.zaneschepke.wireguardautotunnel" groupId = "com.zaneschepke.wireguardautotunnel"
artifactId = "amneziawg-parser" artifactId = "amneziawg-parser"
version = "1.0.7" version = "1.1.0"
from(components["java"]) from(components["java"])
pom { pom {
name.set("AmneziaWG Parser") name.set("AmneziaWG Parser")
@@ -14,9 +14,13 @@ import kotlinx.serialization.Serializable
data class Config( data class Config(
@SerialName("Interface") val `interface`: InterfaceSection, @SerialName("Interface") val `interface`: InterfaceSection,
@SerialName("Peer") val peers: List<PeerSection> = emptyList(), @SerialName("Peer") val peers: List<PeerSection> = emptyList(),
val name: String? = null,
val headerComments: List<String> = emptyList(), val headerComments: List<String> = emptyList(),
) { ) {
fun withName(newName: String?): Config =
copy(name = newName?.trim()?.takeIf { it.isNotBlank() })
@Throws(ConfigParseException::class) @Throws(ConfigParseException::class)
fun validate() { fun validate() {
`interface`.validate() `interface`.validate()
@@ -25,6 +29,7 @@ data class Config(
fun asQuickString(): String = fun asQuickString(): String =
buildString { buildString {
name?.let { appendLine("# Name = $it") }
headerComments.forEach { appendLine(it) } headerComments.forEach { appendLine(it) }
ConfigFormatter.appendInterfaceSection(this, `interface`) ConfigFormatter.appendInterfaceSection(this, `interface`)
peers.forEach { ConfigFormatter.appendPeerSection(this, it) } peers.forEach { ConfigFormatter.appendPeerSection(this, it) }
@@ -92,8 +97,28 @@ data class Config(
val parts = raw.split("=", limit = 2) val parts = raw.split("=", limit = 2)
if (parts.size == 2) { if (parts.size == 2) {
val key = parts[0].trim() val rawKey = parts[0].trim()
var value = parts[1].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) { if (currentSectionMap === interfaceMap) {
when (key) { when (key) {
@@ -107,14 +132,13 @@ data class Config(
} }
} }
// remove whitespaces // Remove whitespaces
if ( if (
key in key in
listOf( listOf(
"PrivateKey", "PrivateKey",
"PublicKey", "PublicKey",
"PresharedKey", "PresharedKey",
"PreSharedKey",
"H1", "H1",
"H2", "H2",
"H3", "H3",
@@ -123,12 +147,55 @@ data class Config(
) { ) {
value = value.replace(Regex("\\s+"), "") value = value.replace(Regex("\\s+"), "")
} }
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) 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( return Config(
headerComments = headerComments, headerComments = cleanedHeaderComments,
name = extractedName,
`interface` = buildInterface(interfaceMap, scripts.build(), interfaceComments), `interface` = buildInterface(interfaceMap, scripts.build(), interfaceComments),
peers = peerMaps.map { (map, comments) -> buildPeer(map, comments) }, peers = peerMaps.map { (map, comments) -> buildPeer(map, comments) },
) )
@@ -1,37 +1,16 @@
// SPDX-License-Identifier: Apache-2.0 // com.zaneschepke.wireguardautotunnel.parser.crypto.Key.kt
// Copyright © 2026 WG Tunnel.
// Adapted from WireGuard LLC.
package com.zaneschepke.wireguardautotunnel.parser.crypto package com.zaneschepke.wireguardautotunnel.parser.crypto
import io.github.andreypfau.curve25519.x25519.X25519
import kotlin.experimental.and 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 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) { class Key private constructor(private val key: ByteArray) {
fun getBytes(): ByteArray = key.copyOf() fun getBytes(): ByteArray = key.copyOf()
fun toBase64(): String { fun toBase64(): String = Base64.encode(key).decodeToString()
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()
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -39,113 +18,32 @@ class Key private constructor(private val key: ByteArray) {
return key.contentEquals(other.key) return key.contentEquals(other.key)
} }
override fun hashCode(): Int { override fun hashCode(): Int = key.contentHashCode()
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
}
companion object { companion object {
fun fromBase64(str: String): Key { fun fromBase64(str: String): Key {
val input = str.toCharArray() val bytes = Base64.decode(str)
if (input.size != Format.BASE64.length || input[Format.BASE64.length - 1] != '=') { if (bytes.size != 32) throw KeyFormatException(Format.BINARY, Type.LENGTH)
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)
}
return Key(bytes) 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 { fun generatePrivateKey(): Key {
val privateKey = ByteArray(Format.BINARY.length) val priv = ByteArray(32)
CryptoRand.nextBytes(privateKey) CryptoRand.nextBytes(priv)
privateKey[0] = privateKey[0] and 248.toByte() priv[0] = priv[0] and 248.toByte()
privateKey[31] = privateKey[31] and 127.toByte() priv[31] = (priv[31].toInt() and 127 or 64).toByte()
privateKey[31] = privateKey[31] or 64.toByte() return Key(priv)
return Key(privateKey)
} }
fun generatePublicKey(privateKey: Key): Key { fun generatePublicKey(privateKey: Key): Key {
val publicKey = ByteArray(Format.BINARY.length) val pub = ByteArray(32)
X25519.x25519(privateKey.getBytes(), output = publicKey) X25519.scalarMultBase(privateKey.getBytes(), 0, pub, 0)
return Key(publicKey) return Key(pub)
}
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()
}
} }
} }
@@ -160,3 +58,10 @@ class Key private constructor(private val key: ByteArray) {
CONTENTS, CONTENTS,
} }
} }
class KeyFormatException : Exception {
constructor(
format: Key.Format,
type: Key.Type,
) : super("Invalid key format: $format, type: $type")
}