From 5ecd732cc205824acc5008e8cbb101fad23c5f1f Mon Sep 17 00:00:00 2001 From: Tejas Mehta Date: Sun, 6 Nov 2022 11:46:10 -0500 Subject: [PATCH] Add Complete XOR Patch Functionality (#255) Co-authored-by: Davide De Rosa --- .github/workflows/test.yml | 8 +- Package.resolved | 2 +- Package.swift | 9 ++ README.md | 21 +++- .../include/XORMethodNative.h | 34 ++++++ .../CTunnelKitOpenVPNProtocol/PacketStream.m | 35 +++--- .../include/PacketStream.h | 16 ++- .../CTunnelKitOpenVPNProtocol/include/XOR.h | 99 +++++++++++++++++ .../TunnelKitAppExtension/LinkProducer.swift | 5 +- Sources/TunnelKitCore/LinkInterface.swift | 3 - .../NETCPLink.swift | 38 +++++-- .../NEUDPLink.swift | 39 +++---- .../OpenVPNTunnelProvider.swift | 4 +- .../TunnelKitOpenVPNCore/Configuration.swift | 20 ++-- .../ConfigurationParser.swift | 51 +++++++-- Sources/TunnelKitOpenVPNCore/XORMethod.swift | 77 ++++++++++++++ .../XORProcessor.swift | 100 ++++++++++++++++++ .../ConfigurationParserTests.swift | 16 ++- Tests/TunnelKitOpenVPNTests/XORTests.swift | 87 +++++++++++++++ 19 files changed, 567 insertions(+), 97 deletions(-) create mode 100644 Sources/CTunnelKitOpenVPNCore/include/XORMethodNative.h create mode 100644 Sources/CTunnelKitOpenVPNProtocol/include/XOR.h create mode 100644 Sources/TunnelKitOpenVPNCore/XORMethod.swift create mode 100644 Sources/TunnelKitOpenVPNProtocol/XORProcessor.swift create mode 100644 Tests/TunnelKitOpenVPNTests/XORTests.swift diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af6df53..91b034d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,12 +6,18 @@ on: - "master" paths-ignore: - "*.md" + pull_request: + types: [ opened, synchronize ] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} jobs: run_tests: name: Run tests runs-on: macos-12 - timeout-minutes: 10 + timeout-minutes: 5 steps: - uses: actions/checkout@v2 - uses: maxim-lobanov/setup-xcode@v1 diff --git a/Package.resolved b/Package.resolved index 71be3de..fe4df5b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,7 +24,7 @@ "repositoryURL": "https://github.com/passepartoutvpn/wireguard-apple", "state": { "branch": null, - "revision": "b4f74b7bcba9004a1852e615298f9cbc68fb7f67", + "revision": "d3b8f1ac6f3361d69bd3daf8aee3c43012c6ec0b", "version": "1.0.16" } } diff --git a/Package.swift b/Package.swift index 6a488bd..9288f25 100644 --- a/Package.swift +++ b/Package.swift @@ -161,6 +161,11 @@ let package = Package( name: "TunnelKitCoreTests", dependencies: [ "TunnelKitCore" + ], + exclude: [ + "RandomTests.swift", + "RawPerformanceTests.swift", + "RoutingTests.swift" ]), .testTarget( name: "TunnelKitOpenVPNTests", @@ -169,6 +174,10 @@ let package = Package( "TunnelKitOpenVPNAppExtension", "TunnelKitLZO" ], + exclude: [ + "DataPathPerformanceTests.swift", + "EncryptionPerformanceTests.swift" + ], resources: [ .process("Resources") ]), diff --git a/README.md b/README.md index b1111cf..9858499 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,20 @@ TunnelKit can parse .ovpn configuration files. Below are a few details worth men #### Non-standard -- Single-byte XOR masking - - Via `--scramble xormask ` - - XOR all incoming and outgoing bytes by the ASCII value of the character argument - - See [Tunnelblick website][about-tunnelblick-xor] for more details +- XOR-patch functionality: + - Multi-byte XOR Masking + - Via `--scramble xormask ` + - XOR all incoming and outgoing bytes by the passphrase given + - XOR Position Masking + - Via `--scramble xorptrpos` + - XOR all bytes by their position in the array + - Packet Reverse Scramble + - Via `--scramble reverse` + - Keeps the first byte and reverses the rest of the array + - XOR Scramble Obfuscate + - Via `--scramble obfuscate ` + - Performs a combination of the three above (specifically `xormask ` -> `xorptrpos` -> `reverse` -> `xorptrpos` for reading, and the opposite for writing) + - See [Tunnelblick website][about-tunnelblick-xor] for more details (Patch was written in accordance with Tunnelblick's patch for compatibility) #### Unsupported @@ -221,6 +231,7 @@ A custom TunnelKit license, e.g. for use in proprietary software, may be negotia - [SURFnet][ppl-surfnet] - [SwiftyBeaver][dep-swiftybeaver-repo] - Copyright (c) 2015 Sebastian Kreutzberger - [XMB5][ppl-xmb5] for the [XOR patch][ppl-xmb5-xor] - Copyright (c) 2020 Sam Foxman +- [tmthecoder][ppl-tmthecoder] for the complete [XOR patch][ppl-tmthecoder-xor] - Copyright (c) 2022 Tejas Mehta - [eduVPN][ppl-eduvpn] for the convenient WireGuardKitGo script ### OpenVPN @@ -264,6 +275,8 @@ Website: [passepartoutvpn.app][about-website] [ppl-surfnet]: https://www.surf.nl/en/about-surf/subsidiaries/surfnet [ppl-xmb5]: https://github.com/XMB5 [ppl-xmb5-xor]: https://github.com/passepartoutvpn/tunnelkit/pull/170 +[ppl-tmthecoder]: https://github.com/tmthecoder +[ppl-tmthecoder-xor]: https://github.com/passepartoutvpn/tunnelkit/pull/255 [ppl-eduvpn]: https://github.com/eduvpn/apple [about-tunnelblick-xor]: https://tunnelblick.net/cOpenvpn_xorpatch.html [about-pr-bitcode]: https://github.com/passepartoutvpn/tunnelkit/issues/51 diff --git a/Sources/CTunnelKitOpenVPNCore/include/XORMethodNative.h b/Sources/CTunnelKitOpenVPNCore/include/XORMethodNative.h new file mode 100644 index 0000000..bb252bc --- /dev/null +++ b/Sources/CTunnelKitOpenVPNCore/include/XORMethodNative.h @@ -0,0 +1,34 @@ +// +// XORMethodNative.h +// TunnelKit +// +// Created by Davide De Rosa on 11/4/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +#import + +typedef NS_ENUM(NSInteger, XORMethodNative) { + XORMethodNativeNone, + XORMethodNativeMask, + XORMethodNativePtrPos, + XORMethodNativeReverse, + XORMethodNativeObfuscate +}; diff --git a/Sources/CTunnelKitOpenVPNProtocol/PacketStream.m b/Sources/CTunnelKitOpenVPNProtocol/PacketStream.m index c15cc66..adc2304 100644 --- a/Sources/CTunnelKitOpenVPNProtocol/PacketStream.m +++ b/Sources/CTunnelKitOpenVPNProtocol/PacketStream.m @@ -24,23 +24,16 @@ // #import "PacketStream.h" +#import "XOR.h" static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t); @implementation PacketStream -+ (void)memcpyXor:(uint8_t *)dst src:(NSData *)src xorMask:(uint8_t)xorMask -{ - if (xorMask != 0) { - for (int i = 0; i < src.length; ++i) { - dst[i] = ((uint8_t *)(src.bytes))[i] ^ xorMask; - } - return; - } - memcpy(dst, src.bytes, src.length); -} - -+ (NSArray *)packetsFromStream:(NSData *)stream until:(NSInteger *)until xorMask:(uint8_t)xorMask ++ (NSArray *)packetsFromInboundStream:(NSData *)stream + until:(NSInteger *)until + xorMethod:(XORMethodNative)xorMethod + xorMask:(NSData *)xorMask { NSInteger ni = 0; NSMutableArray *parsed = [[NSMutableArray alloc] init]; @@ -54,11 +47,7 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t); } NSData *packet = [stream subdataWithRange:NSMakeRange(start, packlen)]; uint8_t* packetBytes = (uint8_t*) packet.bytes; - if (xorMask != 0) { - for (int i = 0; i < packet.length; i++) { - packetBytes[i] ^= xorMask; - } - } + xor_memcpy(packetBytes, packet, xorMethod, xorMask, false); [parsed addObject:packet]; ni = end; } @@ -68,19 +57,23 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t); return parsed; } -+ (NSData *)streamFromPacket:(NSData *)packet xorMask:(uint8_t)xorMask ++ (NSData *)outboundStreamFromPacket:(NSData *)packet + xorMethod:(XORMethodNative)xorMethod + xorMask:(NSData *)xorMask { NSMutableData *raw = [[NSMutableData alloc] initWithLength:(PacketStreamHeaderLength + packet.length)]; uint8_t *ptr = raw.mutableBytes; *(uint16_t *)ptr = CFSwapInt16HostToBig(packet.length); ptr += PacketStreamHeaderLength; - [PacketStream memcpyXor:ptr src:packet xorMask:xorMask]; + xor_memcpy(ptr, packet, xorMethod, xorMask, true); return raw; } -+ (NSData *)streamFromPackets:(NSArray *)packets xorMask:(uint8_t)xorMask ++ (NSData *)outboundStreamFromPackets:(NSArray *)packets + xorMethod:(XORMethodNative)xorMethod + xorMask:(NSData *)xorMask { NSInteger streamLength = 0; for (NSData *p in packets) { @@ -92,7 +85,7 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t); for (NSData *packet in packets) { *(uint16_t *)ptr = CFSwapInt16HostToBig(packet.length); ptr += PacketStreamHeaderLength; - [PacketStream memcpyXor:ptr src:packet xorMask:xorMask]; + xor_memcpy(ptr, packet, xorMethod, xorMask, true); ptr += packet.length; } return raw; diff --git a/Sources/CTunnelKitOpenVPNProtocol/include/PacketStream.h b/Sources/CTunnelKitOpenVPNProtocol/include/PacketStream.h index c62ad05..93e94f2 100644 --- a/Sources/CTunnelKitOpenVPNProtocol/include/PacketStream.h +++ b/Sources/CTunnelKitOpenVPNProtocol/include/PacketStream.h @@ -24,14 +24,24 @@ // #import +#import "XORMethodNative.h" NS_ASSUME_NONNULL_BEGIN @interface PacketStream : NSObject -+ (NSArray *)packetsFromStream:(NSData *)stream until:(NSInteger *)until xorMask:(uint8_t)xorMask; -+ (NSData *)streamFromPacket:(NSData *)packet xorMask:(uint8_t)xorMask; -+ (NSData *)streamFromPackets:(NSArray *)packets xorMask:(uint8_t)xorMask; ++ (NSArray *)packetsFromInboundStream:(NSData *)stream + until:(NSInteger *)until + xorMethod:(XORMethodNative)xorMethod + xorMask:(nullable NSData *)xorMask; + ++ (NSData *)outboundStreamFromPacket:(NSData *)packet + xorMethod:(XORMethodNative)xorMethod + xorMask:(nullable NSData *)xorMask; + ++ (NSData *)outboundStreamFromPackets:(NSArray *)packets + xorMethod:(XORMethodNative)xorMethod + xorMask:(nullable NSData *)xorMask; @end diff --git a/Sources/CTunnelKitOpenVPNProtocol/include/XOR.h b/Sources/CTunnelKitOpenVPNProtocol/include/XOR.h new file mode 100644 index 0000000..df1347c --- /dev/null +++ b/Sources/CTunnelKitOpenVPNProtocol/include/XOR.h @@ -0,0 +1,99 @@ +// +// XOR.h +// TunnelKit +// +// Created by Tejas Mehta on 5/24/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +#import +#import "XORMethodNative.h" + +static inline void xor_mask(uint8_t *dst, const uint8_t *src, NSData *xorMask, size_t length) +{ + if (xorMask.length > 0) { + for (size_t i = 0; i < length; ++i) { + dst[i] = src[i] ^ ((uint8_t *)(xorMask.bytes))[i % xorMask.length]; + } + return; + } + memcpy(dst, src, length); +} + +static inline void xor_ptrpos(uint8_t *dst, const uint8_t *src, size_t length) +{ + for (size_t i = 0; i < length; ++i) { + dst[i] = src[i] ^ (i + 1); + } +} + +static inline void xor_reverse(uint8_t *dst, const uint8_t *src, size_t length) +{ + size_t start = 1; + size_t end = length - 1; + uint8_t temp = 0; + dst[0] = src[0]; + while (start < end) { + temp = src[start]; + dst[start] = src[end]; + dst[end] = temp; + start++; + end--; + } + if (start == end) { + dst[start] = src[start]; + } +} + +static inline void xor_memcpy(uint8_t *dst, NSData *src, XORMethodNative method, NSData *mask, BOOL outbound) +{ + const uint8_t *source = (uint8_t *)src.bytes; + switch (method) { + case XORMethodNativeNone: + memcpy(dst, source, src.length); + break; + + case XORMethodNativeMask: + xor_mask(dst, source, mask, src.length); + break; + + case XORMethodNativePtrPos: + xor_ptrpos(dst, source, src.length); + break; + + case XORMethodNativeReverse: + xor_reverse(dst, source, src.length); + break; + + case XORMethodNativeObfuscate: + if (outbound) { + xor_ptrpos(dst, source, src.length); + xor_reverse(dst, dst, src.length); + xor_ptrpos(dst, dst, src.length); + xor_mask(dst, dst, mask, src.length); + } else { + xor_mask(dst, source, mask, src.length); + xor_ptrpos(dst, dst, src.length); + xor_reverse(dst, dst, src.length); + xor_ptrpos(dst, dst, src.length); + } + break; + } +} diff --git a/Sources/TunnelKitAppExtension/LinkProducer.swift b/Sources/TunnelKitAppExtension/LinkProducer.swift index 56ab458..c0ed575 100644 --- a/Sources/TunnelKitAppExtension/LinkProducer.swift +++ b/Sources/TunnelKitAppExtension/LinkProducer.swift @@ -32,7 +32,8 @@ public protocol LinkProducer { /** Returns a `LinkInterface`. - - Parameter xorMask: The XOR mask. + - Parameter userObject: Optional user data. + - Returns: A generic `LinkInterface`. **/ - func link(xorMask: UInt8?) -> LinkInterface + func link(userObject: Any?) -> LinkInterface } diff --git a/Sources/TunnelKitCore/LinkInterface.swift b/Sources/TunnelKitCore/LinkInterface.swift index 38f5396..939c4fb 100644 --- a/Sources/TunnelKitCore/LinkInterface.swift +++ b/Sources/TunnelKitCore/LinkInterface.swift @@ -50,7 +50,4 @@ public protocol LinkInterface: IOInterface { /// The number of packets that this interface is able to bufferize. var packetBufferSize: Int { get } - - /// A byte to xor all packet payloads with. - var xorMask: UInt8 { get } } diff --git a/Sources/TunnelKitOpenVPNAppExtension/NETCPLink.swift b/Sources/TunnelKitOpenVPNAppExtension/NETCPLink.swift index e2fe539..a5117dc 100644 --- a/Sources/TunnelKitOpenVPNAppExtension/NETCPLink.swift +++ b/Sources/TunnelKitOpenVPNAppExtension/NETCPLink.swift @@ -27,19 +27,23 @@ import Foundation import NetworkExtension import TunnelKitCore import TunnelKitAppExtension +import TunnelKitOpenVPNCore import CTunnelKitOpenVPNProtocol class NETCPLink: LinkInterface { private let impl: NWTCPConnection private let maxPacketSize: Int - - let xorMask: UInt8 - - init(impl: NWTCPConnection, maxPacketSize: Int? = nil, xorMask: UInt8?) { + + private let xorMethod: OpenVPN.XORMethod? + + private let xorMask: Data? + + init(impl: NWTCPConnection, maxPacketSize: Int? = nil, xorMethod: OpenVPN.XORMethod?) { self.impl = impl self.maxPacketSize = maxPacketSize ?? (512 * 1024) - self.xorMask = xorMask ?? 0 + self.xorMethod = xorMethod + xorMask = xorMethod?.mask } // MARK: LinkInterface @@ -81,7 +85,12 @@ class NETCPLink: LinkInterface { var newBuffer = buffer newBuffer.append(contentsOf: data) var until = 0 - let packets = PacketStream.packets(fromStream: newBuffer, until: &until, xorMask: self.xorMask) + let packets = PacketStream.packets( + fromInboundStream: newBuffer, + until: &until, + xorMethod: self.xorMethod?.native ?? .none, + xorMask: self.xorMask + ) newBuffer = newBuffer.subdata(in: until.. Void)?) { - let stream = PacketStream.stream(fromPacket: packet, xorMask: xorMask) + let stream = PacketStream.outboundStream( + fromPacket: packet, + xorMethod: xorMethod?.native ?? .none, + xorMask: xorMask + ) impl.write(stream) { (error) in completionHandler?(error) } } func writePackets(_ packets: [Data], completionHandler: ((Error?) -> Void)?) { - let stream = PacketStream.stream(fromPackets: packets, xorMask: xorMask) + let stream = PacketStream.outboundStream( + fromPackets: packets, + xorMethod: xorMethod?.native ?? .none, + xorMask: xorMask + ) impl.write(stream) { (error) in completionHandler?(error) } @@ -106,7 +123,8 @@ class NETCPLink: LinkInterface { } extension NETCPSocket: LinkProducer { - public func link(xorMask: UInt8?) -> LinkInterface { - return NETCPLink(impl: impl, maxPacketSize: nil, xorMask: xorMask) + public func link(userObject: Any?) -> LinkInterface { + let xorMethod = userObject as? OpenVPN.XORMethod + return NETCPLink(impl: impl, maxPacketSize: nil, xorMethod: xorMethod) } } diff --git a/Sources/TunnelKitOpenVPNAppExtension/NEUDPLink.swift b/Sources/TunnelKitOpenVPNAppExtension/NEUDPLink.swift index 1da5f8b..8dcb3a6 100644 --- a/Sources/TunnelKitOpenVPNAppExtension/NEUDPLink.swift +++ b/Sources/TunnelKitOpenVPNAppExtension/NEUDPLink.swift @@ -27,18 +27,20 @@ import Foundation import NetworkExtension import TunnelKitCore import TunnelKitAppExtension +import TunnelKitOpenVPNCore +import TunnelKitOpenVPNProtocol class NEUDPLink: LinkInterface { private let impl: NWUDPSession private let maxDatagrams: Int - let xorMask: UInt8 - - init(impl: NWUDPSession, maxDatagrams: Int? = nil, xorMask: UInt8?) { + private let xor: XORProcessor + + init(impl: NWUDPSession, maxDatagrams: Int? = nil, xorMethod: OpenVPN.XORMethod?) { self.impl = impl self.maxDatagrams = maxDatagrams ?? 200 - self.xorMask = xorMask ?? 0 + xor = XORProcessor(method: xorMethod) } // MARK: LinkInterface @@ -68,12 +70,8 @@ class NEUDPLink: LinkInterface { return } var packetsToUse: [Data]? - if let packets = packets, self.xorMask != 0 { - packetsToUse = packets.map { packet in - Data(bytes: packet.map { $0 ^ self.xorMask }, count: packet.count) - } - } else { - packetsToUse = packets + if let packets = packets { + packetsToUse = self.xor.processPackets(packets, outbound: false) } queue.sync { handler(packetsToUse, error) @@ -82,26 +80,14 @@ class NEUDPLink: LinkInterface { } func writePacket(_ packet: Data, completionHandler: ((Error?) -> Void)?) { - var dataToUse: Data - if xorMask != 0 { - dataToUse = Data(bytes: packet.map { $0 ^ xorMask }, count: packet.count) - } else { - dataToUse = packet - } + let dataToUse = xor.processPacket(packet, outbound: true) impl.writeDatagram(dataToUse) { error in completionHandler?(error) } } func writePackets(_ packets: [Data], completionHandler: ((Error?) -> Void)?) { - var packetsToUse: [Data] - if xorMask != 0 { - packetsToUse = packets.map { packet in - Data(bytes: packet.map { $0 ^ xorMask }, count: packet.count) - } - } else { - packetsToUse = packets - } + let packetsToUse = xor.processPackets(packets, outbound: true) impl.writeMultipleDatagrams(packetsToUse) { error in completionHandler?(error) } @@ -109,7 +95,8 @@ class NEUDPLink: LinkInterface { } extension NEUDPSocket: LinkProducer { - public func link(xorMask: UInt8?) -> LinkInterface { - return NEUDPLink(impl: impl, maxDatagrams: nil, xorMask: xorMask) + public func link(userObject: Any?) -> LinkInterface { + let xorMethod = userObject as? OpenVPN.XORMethod + return NEUDPLink(impl: impl, maxDatagrams: nil, xorMethod: xorMethod) } } diff --git a/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift b/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift index cd799ad..7b240c7 100644 --- a/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift +++ b/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift @@ -408,10 +408,10 @@ extension OpenVPNTunnelProvider: GenericSocketDelegate { return } if session.canRebindLink() { - session.rebindLink(producer.link(xorMask: cfg.configuration.xorMask)) + session.rebindLink(producer.link(userObject: cfg.configuration.xorMethod)) reasserting = false } else { - session.setLink(producer.link(xorMask: cfg.configuration.xorMask)) + session.setLink(producer.link(userObject: cfg.configuration.xorMethod)) } } diff --git a/Sources/TunnelKitOpenVPNCore/Configuration.swift b/Sources/TunnelKitOpenVPNCore/Configuration.swift index 98acfe3..5534477 100644 --- a/Sources/TunnelKitOpenVPNCore/Configuration.swift +++ b/Sources/TunnelKitOpenVPNCore/Configuration.swift @@ -209,9 +209,6 @@ extension OpenVPN { /// The number of seconds after which a renegotiation should be initiated. If `nil`, the client will never initiate a renegotiation. public var renegotiatesAfter: TimeInterval? - /// A byte to xor all packet payloads with. - public var xorMask: UInt8? - // MARK: Client /// The list of server endpoints. @@ -314,6 +311,11 @@ extension OpenVPN { /// Server settings that must not be pulled. public var noPullMask: [PullMask]? + // MARK: Extra + + /// The method to follow in regards to the XOR patch. + public var xorMethod: XORMethod? + /** Creates a `ConfigurationBuilder`. @@ -348,7 +350,6 @@ extension OpenVPN { keepAliveInterval: keepAliveInterval, keepAliveTimeout: keepAliveTimeout, renegotiatesAfter: renegotiatesAfter, - xorMask: xorMask, remotes: remotes, checksEKU: checksEKU, checksSANHost: checksSANHost, @@ -376,7 +377,8 @@ extension OpenVPN { proxyAutoConfigurationURL: proxyAutoConfigurationURL, proxyBypassDomains: proxyBypassDomains, routingPolicies: routingPolicies, - noPullMask: noPullMask + noPullMask: noPullMask, + xorMethod: xorMethod ) } } @@ -434,9 +436,6 @@ extension OpenVPN { /// - Seealso: `ConfigurationBuilder.renegotiatesAfter` public let renegotiatesAfter: TimeInterval? - /// - Seealso: `ConfigurationBuilder.xorMask` - public let xorMask: UInt8? - /// - Seealso: `ConfigurationBuilder.remotes` public let remotes: [Endpoint]? @@ -521,6 +520,9 @@ extension OpenVPN { /// - Seealso: `ConfigurationBuilder.noPullMask` public let noPullMask: [PullMask]? + /// - Seealso: `ConfigurationBuilder.xorMethod` + public let xorMethod: XORMethod? + // MARK: Shortcuts public var fallbackCipher: Cipher { @@ -597,7 +599,6 @@ extension OpenVPN.Configuration { builder.keepAliveInterval = keepAliveInterval builder.keepAliveTimeout = keepAliveTimeout builder.renegotiatesAfter = renegotiatesAfter - builder.xorMask = xorMask builder.remotes = remotes builder.checksEKU = checksEKU builder.checksSANHost = checksSANHost @@ -626,6 +627,7 @@ extension OpenVPN.Configuration { builder.proxyBypassDomains = proxyBypassDomains builder.routingPolicies = routingPolicies builder.noPullMask = noPullMask + builder.xorMethod = xorMethod return builder } } diff --git a/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift b/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift index c2e7d20..8b6ee0b 100644 --- a/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift +++ b/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift @@ -65,8 +65,6 @@ extension OpenVPN { static let renegSec = NSRegularExpression("^reneg-sec +\\d+") - static let xorMask = NSRegularExpression("^scramble +xormask +.$") - static let blockBegin = NSRegularExpression("^<[\\w\\-]+>") static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>") @@ -120,7 +118,11 @@ extension OpenVPN { static let redirectGateway = NSRegularExpression("^redirect-gateway.*") static let routeNoPull = NSRegularExpression("^route-nopull") + + // MARK: Extra + static let xorInfo = NSRegularExpression("^scramble +(xormask|xorptrpos|reverse|obfuscate)[\\s]?([^\\s]+)?") + // MARK: Unsupported // static let fragment = NSRegularExpression("^fragment +\\d+") @@ -266,7 +268,6 @@ extension OpenVPN { var optKeepAliveSeconds: TimeInterval? var optKeepAliveTimeoutSeconds: TimeInterval? var optRenegotiateAfterSeconds: TimeInterval? - var optXorMask: UInt8? // var optDefaultProto: SocketType? var optDefaultPort: UInt16? @@ -294,6 +295,8 @@ extension OpenVPN { var optProxyBypass: [String]? var optRedirectGateway: Set? var optRouteNoPull: Bool? + // + var optXorMethod: XORMethod? log.verbose("Configuration file:") for line in lines { @@ -518,13 +521,6 @@ extension OpenVPN { } optRenegotiateAfterSeconds = TimeInterval(arg) } - Regex.xorMask.enumerateSpacedArguments(in: line) { - isHandled = true - if $0.count != 2 { - return - } - optXorMask = Character($0[1]).asciiValue - } // MARK: Client @@ -712,7 +708,37 @@ extension OpenVPN { Regex.routeNoPull.enumerateSpacedComponents(in: line) { _ in optRouteNoPull = true } + + // MARK: Extra + Regex.xorInfo.enumerateSpacedArguments(in: line) { + isHandled = true + guard !$0.isEmpty else { + return + } + + switch $0[0] { + case "xormask": + if $0.count > 1, let mask = $0[1].data(using: .utf8) { + optXorMethod = .xormask(mask: mask) + } + + case "xorptrpos": + optXorMethod = .xorptrpos + + case "reverse": + optXorMethod = .reverse + + case "obfuscate": + if $0.count > 1, let mask = $0[1].data(using: .utf8) { + optXorMethod = .obfuscate(mask: mask) + } + + default: + return + } + } + // if let error = unsupportedError { @@ -821,7 +847,6 @@ extension OpenVPN { sessionBuilder.randomizeEndpoint = optRandomizeEndpoint sessionBuilder.randomizeHostnames = optRandomizeHostnames sessionBuilder.mtu = optMTU - sessionBuilder.xorMask = optXorMask // MARK: Server @@ -938,6 +963,10 @@ extension OpenVPN { } sessionBuilder.routingPolicies = [RoutingPolicy](policies) } + + // MARK: Extra + + sessionBuilder.xorMethod = optXorMethod // diff --git a/Sources/TunnelKitOpenVPNCore/XORMethod.swift b/Sources/TunnelKitOpenVPNCore/XORMethod.swift new file mode 100644 index 0000000..936f961 --- /dev/null +++ b/Sources/TunnelKitOpenVPNCore/XORMethod.swift @@ -0,0 +1,77 @@ +// +// XORMethod.swift +// TunnelKit +// +// Created by Davide De Rosa on 11/4/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import Foundation +import CTunnelKitOpenVPNCore + +extension OpenVPN { + + /// The obfuscation method. + public enum XORMethod: Codable, Equatable { + + /// XORs the bytes in each buffer with the given mask. + case xormask(mask: Data) + + /// XORs each byte with its position in the packet. + case xorptrpos + + /// Reverses the order of bytes in each buffer except for the first (abcde becomes aedcb). + case reverse + + /// Performs several of the above steps (xormask -> xorptrpos -> reverse -> xorptrpos). + case obfuscate(mask: Data) + + /// This method mapped to native enumeration. + public var native: XORMethodNative { + switch self { + case .xormask: + return .mask + + case .xorptrpos: + return .ptrPos + + case .reverse: + return .reverse + + case .obfuscate: + return .obfuscate + } + } + + /// The optionally associated mask. + public var mask: Data? { + switch self { + case .xormask(let mask): + return mask + + case .obfuscate(let mask): + return mask + + default: + return nil + } + } + } +} diff --git a/Sources/TunnelKitOpenVPNProtocol/XORProcessor.swift b/Sources/TunnelKitOpenVPNProtocol/XORProcessor.swift new file mode 100644 index 0000000..e5f64e6 --- /dev/null +++ b/Sources/TunnelKitOpenVPNProtocol/XORProcessor.swift @@ -0,0 +1,100 @@ +// +// XORProcessor.swift +// TunnelKit +// +// Created by Davide De Rosa on 11/4/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import Foundation +import TunnelKitOpenVPNCore + +/// Processes data packets according to a XOR method. +public struct XORProcessor { + private let method: OpenVPN.XORMethod? + + public init(method: OpenVPN.XORMethod?) { + self.method = method + } + + /** + Returns an array of data packets processed according to XOR method. + + - Parameter packets: The array of packets. + - Parameter outbound: Set `true` if packets are outbound, `false` otherwise. + - Returns: The array of packets after XOR processing. + **/ + public func processPackets(_ packets: [Data], outbound: Bool) -> [Data] { + guard let _ = method else { + return packets + } + return packets.map { + processPacket($0, outbound: outbound) + } + } + + /** + Returns a data packet processed according to XOR method. + + - Parameter packets: The packet. + - Parameter outbound: Set `true` if packet is outbound, `false` otherwise. + - Returns: The packet after XOR processing. + **/ + public func processPacket(_ packet: Data, outbound: Bool) -> Data { + guard let method = method else { + return packet + } + switch method { + case .xormask(let mask): + return Self.xormask(packet: packet, mask: mask) + + case .xorptrpos: + return Self.xorptrpos(packet: packet) + + case .reverse: + return Self.reverse(packet: packet) + + case .obfuscate(let mask): + if outbound { + return Self.xormask(packet: Self.xorptrpos(packet: Self.reverse(packet: Self.xorptrpos(packet: packet))), mask: mask) + } else { + return Self.xorptrpos(packet: Self.reverse(packet: Self.xorptrpos(packet: Self.xormask(packet: packet, mask: mask)))) + } + } + } +} + +extension XORProcessor { + private static func xormask(packet: Data, mask: Data) -> Data { + Data(packet.enumerated().map { (index, byte) in + byte ^ [UInt8](mask)[index % mask.count] + }) + } + + private static func xorptrpos(packet: Data) -> Data { + Data(packet.enumerated().map { (index, byte) in + byte ^ UInt8(truncatingIfNeeded: index &+ 1) + }) + } + + private static func reverse(packet: Data) -> Data { + Data(([UInt8](packet))[0..<1] + ([UInt8](packet)[1...]).reversed()) + } +} diff --git a/Tests/TunnelKitOpenVPNTests/ConfigurationParserTests.swift b/Tests/TunnelKitOpenVPNTests/ConfigurationParserTests.swift index 6c82f79..cb343ec 100644 --- a/Tests/TunnelKitOpenVPNTests/ConfigurationParserTests.swift +++ b/Tests/TunnelKitOpenVPNTests/ConfigurationParserTests.swift @@ -127,11 +127,19 @@ class ConfigurationParserTests: XCTestCase { func testXOR() throws { let cfg = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble xormask F"]) XCTAssertNil(cfg.warning) - XCTAssertEqual(cfg.configuration.xorMask, Character("F").asciiValue) - - let cfg2 = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble xormask FFFF"]) + XCTAssertEqual(cfg.configuration.xorMethod, OpenVPN.XORMethod.xormask(mask: Data(repeating: Character("F").asciiValue!, count:1))) + + let cfg2 = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble reverse"]) XCTAssertNil(cfg.warning) - XCTAssertNil(cfg2.configuration.xorMask) + XCTAssertEqual(cfg2.configuration.xorMethod, OpenVPN.XORMethod.reverse) + + let cfg3 = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble xorptrpos"]) + XCTAssertNil(cfg.warning) + XCTAssertEqual(cfg3.configuration.xorMethod, OpenVPN.XORMethod.xorptrpos) + + let cfg4 = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble obfuscate FFFF"]) + XCTAssertNil(cfg.warning) + XCTAssertEqual(cfg4.configuration.xorMethod, OpenVPN.XORMethod.obfuscate(mask: Data(repeating: Character("F").asciiValue!, count:4))) } private func privateTestEncryptedCertificateKey(pkcs: String) throws { diff --git a/Tests/TunnelKitOpenVPNTests/XORTests.swift b/Tests/TunnelKitOpenVPNTests/XORTests.swift new file mode 100644 index 0000000..13a399f --- /dev/null +++ b/Tests/TunnelKitOpenVPNTests/XORTests.swift @@ -0,0 +1,87 @@ +// +// XORTests.swift +// TunnelKitOpenVPNTests +// +// Created by Davide De Rosa on 11/4/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import XCTest +import TunnelKitCore +import TunnelKitOpenVPNProtocol +import CTunnelKitOpenVPNProtocol + +final class XORTests: XCTestCase { + private let mask = Data(hex: "f76dab30") + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testMask() throws { + let processor = XORProcessor(method: .xormask(mask: mask)) + processor.assertReversible(try SecureRandom.data(length: 1000)) + } + + func testPtrPos() throws { + let processor = XORProcessor(method: .xorptrpos) + processor.assertReversible(try SecureRandom.data(length: 1000)) + } + + func testReverse() throws { + let processor = XORProcessor(method: .reverse) + processor.assertReversible(try SecureRandom.data(length: 1000)) + } + + func testObfuscate() throws { + let processor = XORProcessor(method: .obfuscate(mask: mask)) + processor.assertReversible(try SecureRandom.data(length: 1000)) + } + + func testPacketStream() throws { + let data = try SecureRandom.data(length: 10000) + PacketStream.assertReversible(data, method: .none) + PacketStream.assertReversible(data, method: .mask, mask: mask) + PacketStream.assertReversible(data, method: .ptrPos) + PacketStream.assertReversible(data, method: .reverse) + PacketStream.assertReversible(data, method: .obfuscate, mask: mask) + } +} + +private extension XORProcessor { + func assertReversible(_ data: Data) { + let xored = processPacket(data, outbound: true) + XCTAssertEqual(processPacket(xored, outbound: false), data) + } +} + +private extension PacketStream { + static func assertReversible(_ data: Data, method: XORMethodNative, mask: Data? = nil) { + var until = 0 + let outStream = PacketStream.outboundStream(fromPacket: data, xorMethod: method, xorMask: mask) + let inStream = PacketStream.packets(fromInboundStream: outStream, until: &until, xorMethod: method, xorMask: mask) + let originalData = Data(inStream.joined()) + XCTAssertEqual(data.toHex(), originalData.toHex()) + } +}