Add Complete XOR Patch Functionality (#255)

Co-authored-by: Davide De Rosa <keeshux@gmail.com>
This commit is contained in:
Tejas Mehta 2022-11-06 11:46:10 -05:00 committed by GitHub
parent e225ca15ff
commit 5ecd732cc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 567 additions and 97 deletions

View File

@ -6,12 +6,18 @@ on:
- "master" - "master"
paths-ignore: paths-ignore:
- "*.md" - "*.md"
pull_request:
types: [ opened, synchronize ]
concurrency:
group: ${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs: jobs:
run_tests: run_tests:
name: Run tests name: Run tests
runs-on: macos-12 runs-on: macos-12
timeout-minutes: 10 timeout-minutes: 5
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: maxim-lobanov/setup-xcode@v1 - uses: maxim-lobanov/setup-xcode@v1

View File

@ -24,7 +24,7 @@
"repositoryURL": "https://github.com/passepartoutvpn/wireguard-apple", "repositoryURL": "https://github.com/passepartoutvpn/wireguard-apple",
"state": { "state": {
"branch": null, "branch": null,
"revision": "b4f74b7bcba9004a1852e615298f9cbc68fb7f67", "revision": "d3b8f1ac6f3361d69bd3daf8aee3c43012c6ec0b",
"version": "1.0.16" "version": "1.0.16"
} }
} }

View File

@ -161,6 +161,11 @@ let package = Package(
name: "TunnelKitCoreTests", name: "TunnelKitCoreTests",
dependencies: [ dependencies: [
"TunnelKitCore" "TunnelKitCore"
],
exclude: [
"RandomTests.swift",
"RawPerformanceTests.swift",
"RoutingTests.swift"
]), ]),
.testTarget( .testTarget(
name: "TunnelKitOpenVPNTests", name: "TunnelKitOpenVPNTests",
@ -169,6 +174,10 @@ let package = Package(
"TunnelKitOpenVPNAppExtension", "TunnelKitOpenVPNAppExtension",
"TunnelKitLZO" "TunnelKitLZO"
], ],
exclude: [
"DataPathPerformanceTests.swift",
"EncryptionPerformanceTests.swift"
],
resources: [ resources: [
.process("Resources") .process("Resources")
]), ]),

View File

@ -46,10 +46,20 @@ TunnelKit can parse .ovpn configuration files. Below are a few details worth men
#### Non-standard #### Non-standard
- Single-byte XOR masking - XOR-patch functionality:
- Via `--scramble xormask <character>` - Multi-byte XOR Masking
- XOR all incoming and outgoing bytes by the ASCII value of the character argument - Via `--scramble xormask <passphrase>`
- See [Tunnelblick website][about-tunnelblick-xor] for more details - 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 <passphrase>`
- Performs a combination of the three above (specifically `xormask <passphrase>` -> `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 #### Unsupported
@ -221,6 +231,7 @@ A custom TunnelKit license, e.g. for use in proprietary software, may be negotia
- [SURFnet][ppl-surfnet] - [SURFnet][ppl-surfnet]
- [SwiftyBeaver][dep-swiftybeaver-repo] - Copyright (c) 2015 Sebastian Kreutzberger - [SwiftyBeaver][dep-swiftybeaver-repo] - Copyright (c) 2015 Sebastian Kreutzberger
- [XMB5][ppl-xmb5] for the [XOR patch][ppl-xmb5-xor] - Copyright (c) 2020 Sam Foxman - [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 - [eduVPN][ppl-eduvpn] for the convenient WireGuardKitGo script
### OpenVPN ### OpenVPN
@ -264,6 +275,8 @@ Website: [passepartoutvpn.app][about-website]
[ppl-surfnet]: https://www.surf.nl/en/about-surf/subsidiaries/surfnet [ppl-surfnet]: https://www.surf.nl/en/about-surf/subsidiaries/surfnet
[ppl-xmb5]: https://github.com/XMB5 [ppl-xmb5]: https://github.com/XMB5
[ppl-xmb5-xor]: https://github.com/passepartoutvpn/tunnelkit/pull/170 [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 [ppl-eduvpn]: https://github.com/eduvpn/apple
[about-tunnelblick-xor]: https://tunnelblick.net/cOpenvpn_xorpatch.html [about-tunnelblick-xor]: https://tunnelblick.net/cOpenvpn_xorpatch.html
[about-pr-bitcode]: https://github.com/passepartoutvpn/tunnelkit/issues/51 [about-pr-bitcode]: https://github.com/passepartoutvpn/tunnelkit/issues/51

View File

@ -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 <http://www.gnu.org/licenses/>.
//
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, XORMethodNative) {
XORMethodNativeNone,
XORMethodNativeMask,
XORMethodNativePtrPos,
XORMethodNativeReverse,
XORMethodNativeObfuscate
};

View File

@ -24,23 +24,16 @@
// //
#import "PacketStream.h" #import "PacketStream.h"
#import "XOR.h"
static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t); static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t);
@implementation PacketStream @implementation PacketStream
+ (void)memcpyXor:(uint8_t *)dst src:(NSData *)src xorMask:(uint8_t)xorMask + (NSArray<NSData *> *)packetsFromInboundStream:(NSData *)stream
{ until:(NSInteger *)until
if (xorMask != 0) { xorMethod:(XORMethodNative)xorMethod
for (int i = 0; i < src.length; ++i) { xorMask:(NSData *)xorMask
dst[i] = ((uint8_t *)(src.bytes))[i] ^ xorMask;
}
return;
}
memcpy(dst, src.bytes, src.length);
}
+ (NSArray<NSData *> *)packetsFromStream:(NSData *)stream until:(NSInteger *)until xorMask:(uint8_t)xorMask
{ {
NSInteger ni = 0; NSInteger ni = 0;
NSMutableArray<NSData *> *parsed = [[NSMutableArray alloc] init]; NSMutableArray<NSData *> *parsed = [[NSMutableArray alloc] init];
@ -54,11 +47,7 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t);
} }
NSData *packet = [stream subdataWithRange:NSMakeRange(start, packlen)]; NSData *packet = [stream subdataWithRange:NSMakeRange(start, packlen)];
uint8_t* packetBytes = (uint8_t*) packet.bytes; uint8_t* packetBytes = (uint8_t*) packet.bytes;
if (xorMask != 0) { xor_memcpy(packetBytes, packet, xorMethod, xorMask, false);
for (int i = 0; i < packet.length; i++) {
packetBytes[i] ^= xorMask;
}
}
[parsed addObject:packet]; [parsed addObject:packet];
ni = end; ni = end;
} }
@ -68,19 +57,23 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t);
return parsed; 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)]; NSMutableData *raw = [[NSMutableData alloc] initWithLength:(PacketStreamHeaderLength + packet.length)];
uint8_t *ptr = raw.mutableBytes; uint8_t *ptr = raw.mutableBytes;
*(uint16_t *)ptr = CFSwapInt16HostToBig(packet.length); *(uint16_t *)ptr = CFSwapInt16HostToBig(packet.length);
ptr += PacketStreamHeaderLength; ptr += PacketStreamHeaderLength;
[PacketStream memcpyXor:ptr src:packet xorMask:xorMask]; xor_memcpy(ptr, packet, xorMethod, xorMask, true);
return raw; return raw;
} }
+ (NSData *)streamFromPackets:(NSArray<NSData *> *)packets xorMask:(uint8_t)xorMask + (NSData *)outboundStreamFromPackets:(NSArray<NSData *> *)packets
xorMethod:(XORMethodNative)xorMethod
xorMask:(NSData *)xorMask
{ {
NSInteger streamLength = 0; NSInteger streamLength = 0;
for (NSData *p in packets) { for (NSData *p in packets) {
@ -92,7 +85,7 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t);
for (NSData *packet in packets) { for (NSData *packet in packets) {
*(uint16_t *)ptr = CFSwapInt16HostToBig(packet.length); *(uint16_t *)ptr = CFSwapInt16HostToBig(packet.length);
ptr += PacketStreamHeaderLength; ptr += PacketStreamHeaderLength;
[PacketStream memcpyXor:ptr src:packet xorMask:xorMask]; xor_memcpy(ptr, packet, xorMethod, xorMask, true);
ptr += packet.length; ptr += packet.length;
} }
return raw; return raw;

View File

@ -24,14 +24,24 @@
// //
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "XORMethodNative.h"
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@interface PacketStream : NSObject @interface PacketStream : NSObject
+ (NSArray<NSData *> *)packetsFromStream:(NSData *)stream until:(NSInteger *)until xorMask:(uint8_t)xorMask; + (NSArray<NSData *> *)packetsFromInboundStream:(NSData *)stream
+ (NSData *)streamFromPacket:(NSData *)packet xorMask:(uint8_t)xorMask; until:(NSInteger *)until
+ (NSData *)streamFromPackets:(NSArray<NSData *> *)packets xorMask:(uint8_t)xorMask; xorMethod:(XORMethodNative)xorMethod
xorMask:(nullable NSData *)xorMask;
+ (NSData *)outboundStreamFromPacket:(NSData *)packet
xorMethod:(XORMethodNative)xorMethod
xorMask:(nullable NSData *)xorMask;
+ (NSData *)outboundStreamFromPackets:(NSArray<NSData *> *)packets
xorMethod:(XORMethodNative)xorMethod
xorMask:(nullable NSData *)xorMask;
@end @end

View File

@ -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 <http://www.gnu.org/licenses/>.
//
#import <Foundation/Foundation.h>
#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;
}
}

View File

@ -32,7 +32,8 @@ public protocol LinkProducer {
/** /**
Returns a `LinkInterface`. 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
} }

View File

@ -50,7 +50,4 @@ public protocol LinkInterface: IOInterface {
/// The number of packets that this interface is able to bufferize. /// The number of packets that this interface is able to bufferize.
var packetBufferSize: Int { get } var packetBufferSize: Int { get }
/// A byte to xor all packet payloads with.
var xorMask: UInt8 { get }
} }

View File

@ -27,6 +27,7 @@ import Foundation
import NetworkExtension import NetworkExtension
import TunnelKitCore import TunnelKitCore
import TunnelKitAppExtension import TunnelKitAppExtension
import TunnelKitOpenVPNCore
import CTunnelKitOpenVPNProtocol import CTunnelKitOpenVPNProtocol
class NETCPLink: LinkInterface { class NETCPLink: LinkInterface {
@ -34,12 +35,15 @@ class NETCPLink: LinkInterface {
private let maxPacketSize: Int private let maxPacketSize: Int
let xorMask: UInt8 private let xorMethod: OpenVPN.XORMethod?
init(impl: NWTCPConnection, maxPacketSize: Int? = nil, xorMask: UInt8?) { private let xorMask: Data?
init(impl: NWTCPConnection, maxPacketSize: Int? = nil, xorMethod: OpenVPN.XORMethod?) {
self.impl = impl self.impl = impl
self.maxPacketSize = maxPacketSize ?? (512 * 1024) self.maxPacketSize = maxPacketSize ?? (512 * 1024)
self.xorMask = xorMask ?? 0 self.xorMethod = xorMethod
xorMask = xorMethod?.mask
} }
// MARK: LinkInterface // MARK: LinkInterface
@ -81,7 +85,12 @@ class NETCPLink: LinkInterface {
var newBuffer = buffer var newBuffer = buffer
newBuffer.append(contentsOf: data) newBuffer.append(contentsOf: data)
var until = 0 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..<newBuffer.count) newBuffer = newBuffer.subdata(in: until..<newBuffer.count)
self.loopReadPackets(queue, newBuffer, handler) self.loopReadPackets(queue, newBuffer, handler)
@ -91,14 +100,22 @@ class NETCPLink: LinkInterface {
} }
func writePacket(_ packet: Data, completionHandler: ((Error?) -> Void)?) { func writePacket(_ packet: Data, completionHandler: ((Error?) -> 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 impl.write(stream) { (error) in
completionHandler?(error) completionHandler?(error)
} }
} }
func writePackets(_ packets: [Data], completionHandler: ((Error?) -> Void)?) { 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 impl.write(stream) { (error) in
completionHandler?(error) completionHandler?(error)
} }
@ -106,7 +123,8 @@ class NETCPLink: LinkInterface {
} }
extension NETCPSocket: LinkProducer { extension NETCPSocket: LinkProducer {
public func link(xorMask: UInt8?) -> LinkInterface { public func link(userObject: Any?) -> LinkInterface {
return NETCPLink(impl: impl, maxPacketSize: nil, xorMask: xorMask) let xorMethod = userObject as? OpenVPN.XORMethod
return NETCPLink(impl: impl, maxPacketSize: nil, xorMethod: xorMethod)
} }
} }

View File

@ -27,18 +27,20 @@ import Foundation
import NetworkExtension import NetworkExtension
import TunnelKitCore import TunnelKitCore
import TunnelKitAppExtension import TunnelKitAppExtension
import TunnelKitOpenVPNCore
import TunnelKitOpenVPNProtocol
class NEUDPLink: LinkInterface { class NEUDPLink: LinkInterface {
private let impl: NWUDPSession private let impl: NWUDPSession
private let maxDatagrams: Int private let maxDatagrams: Int
let xorMask: UInt8 private let xor: XORProcessor
init(impl: NWUDPSession, maxDatagrams: Int? = nil, xorMask: UInt8?) { init(impl: NWUDPSession, maxDatagrams: Int? = nil, xorMethod: OpenVPN.XORMethod?) {
self.impl = impl self.impl = impl
self.maxDatagrams = maxDatagrams ?? 200 self.maxDatagrams = maxDatagrams ?? 200
self.xorMask = xorMask ?? 0 xor = XORProcessor(method: xorMethod)
} }
// MARK: LinkInterface // MARK: LinkInterface
@ -68,12 +70,8 @@ class NEUDPLink: LinkInterface {
return return
} }
var packetsToUse: [Data]? var packetsToUse: [Data]?
if let packets = packets, self.xorMask != 0 { if let packets = packets {
packetsToUse = packets.map { packet in packetsToUse = self.xor.processPackets(packets, outbound: false)
Data(bytes: packet.map { $0 ^ self.xorMask }, count: packet.count)
}
} else {
packetsToUse = packets
} }
queue.sync { queue.sync {
handler(packetsToUse, error) handler(packetsToUse, error)
@ -82,26 +80,14 @@ class NEUDPLink: LinkInterface {
} }
func writePacket(_ packet: Data, completionHandler: ((Error?) -> Void)?) { func writePacket(_ packet: Data, completionHandler: ((Error?) -> Void)?) {
var dataToUse: Data let dataToUse = xor.processPacket(packet, outbound: true)
if xorMask != 0 {
dataToUse = Data(bytes: packet.map { $0 ^ xorMask }, count: packet.count)
} else {
dataToUse = packet
}
impl.writeDatagram(dataToUse) { error in impl.writeDatagram(dataToUse) { error in
completionHandler?(error) completionHandler?(error)
} }
} }
func writePackets(_ packets: [Data], completionHandler: ((Error?) -> Void)?) { func writePackets(_ packets: [Data], completionHandler: ((Error?) -> Void)?) {
var packetsToUse: [Data] let packetsToUse = xor.processPackets(packets, outbound: true)
if xorMask != 0 {
packetsToUse = packets.map { packet in
Data(bytes: packet.map { $0 ^ xorMask }, count: packet.count)
}
} else {
packetsToUse = packets
}
impl.writeMultipleDatagrams(packetsToUse) { error in impl.writeMultipleDatagrams(packetsToUse) { error in
completionHandler?(error) completionHandler?(error)
} }
@ -109,7 +95,8 @@ class NEUDPLink: LinkInterface {
} }
extension NEUDPSocket: LinkProducer { extension NEUDPSocket: LinkProducer {
public func link(xorMask: UInt8?) -> LinkInterface { public func link(userObject: Any?) -> LinkInterface {
return NEUDPLink(impl: impl, maxDatagrams: nil, xorMask: xorMask) let xorMethod = userObject as? OpenVPN.XORMethod
return NEUDPLink(impl: impl, maxDatagrams: nil, xorMethod: xorMethod)
} }
} }

View File

@ -408,10 +408,10 @@ extension OpenVPNTunnelProvider: GenericSocketDelegate {
return return
} }
if session.canRebindLink() { if session.canRebindLink() {
session.rebindLink(producer.link(xorMask: cfg.configuration.xorMask)) session.rebindLink(producer.link(userObject: cfg.configuration.xorMethod))
reasserting = false reasserting = false
} else { } else {
session.setLink(producer.link(xorMask: cfg.configuration.xorMask)) session.setLink(producer.link(userObject: cfg.configuration.xorMethod))
} }
} }

View File

@ -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. /// The number of seconds after which a renegotiation should be initiated. If `nil`, the client will never initiate a renegotiation.
public var renegotiatesAfter: TimeInterval? public var renegotiatesAfter: TimeInterval?
/// A byte to xor all packet payloads with.
public var xorMask: UInt8?
// MARK: Client // MARK: Client
/// The list of server endpoints. /// The list of server endpoints.
@ -314,6 +311,11 @@ extension OpenVPN {
/// Server settings that must not be pulled. /// Server settings that must not be pulled.
public var noPullMask: [PullMask]? public var noPullMask: [PullMask]?
// MARK: Extra
/// The method to follow in regards to the XOR patch.
public var xorMethod: XORMethod?
/** /**
Creates a `ConfigurationBuilder`. Creates a `ConfigurationBuilder`.
@ -348,7 +350,6 @@ extension OpenVPN {
keepAliveInterval: keepAliveInterval, keepAliveInterval: keepAliveInterval,
keepAliveTimeout: keepAliveTimeout, keepAliveTimeout: keepAliveTimeout,
renegotiatesAfter: renegotiatesAfter, renegotiatesAfter: renegotiatesAfter,
xorMask: xorMask,
remotes: remotes, remotes: remotes,
checksEKU: checksEKU, checksEKU: checksEKU,
checksSANHost: checksSANHost, checksSANHost: checksSANHost,
@ -376,7 +377,8 @@ extension OpenVPN {
proxyAutoConfigurationURL: proxyAutoConfigurationURL, proxyAutoConfigurationURL: proxyAutoConfigurationURL,
proxyBypassDomains: proxyBypassDomains, proxyBypassDomains: proxyBypassDomains,
routingPolicies: routingPolicies, routingPolicies: routingPolicies,
noPullMask: noPullMask noPullMask: noPullMask,
xorMethod: xorMethod
) )
} }
} }
@ -434,9 +436,6 @@ extension OpenVPN {
/// - Seealso: `ConfigurationBuilder.renegotiatesAfter` /// - Seealso: `ConfigurationBuilder.renegotiatesAfter`
public let renegotiatesAfter: TimeInterval? public let renegotiatesAfter: TimeInterval?
/// - Seealso: `ConfigurationBuilder.xorMask`
public let xorMask: UInt8?
/// - Seealso: `ConfigurationBuilder.remotes` /// - Seealso: `ConfigurationBuilder.remotes`
public let remotes: [Endpoint]? public let remotes: [Endpoint]?
@ -521,6 +520,9 @@ extension OpenVPN {
/// - Seealso: `ConfigurationBuilder.noPullMask` /// - Seealso: `ConfigurationBuilder.noPullMask`
public let noPullMask: [PullMask]? public let noPullMask: [PullMask]?
/// - Seealso: `ConfigurationBuilder.xorMethod`
public let xorMethod: XORMethod?
// MARK: Shortcuts // MARK: Shortcuts
public var fallbackCipher: Cipher { public var fallbackCipher: Cipher {
@ -597,7 +599,6 @@ extension OpenVPN.Configuration {
builder.keepAliveInterval = keepAliveInterval builder.keepAliveInterval = keepAliveInterval
builder.keepAliveTimeout = keepAliveTimeout builder.keepAliveTimeout = keepAliveTimeout
builder.renegotiatesAfter = renegotiatesAfter builder.renegotiatesAfter = renegotiatesAfter
builder.xorMask = xorMask
builder.remotes = remotes builder.remotes = remotes
builder.checksEKU = checksEKU builder.checksEKU = checksEKU
builder.checksSANHost = checksSANHost builder.checksSANHost = checksSANHost
@ -626,6 +627,7 @@ extension OpenVPN.Configuration {
builder.proxyBypassDomains = proxyBypassDomains builder.proxyBypassDomains = proxyBypassDomains
builder.routingPolicies = routingPolicies builder.routingPolicies = routingPolicies
builder.noPullMask = noPullMask builder.noPullMask = noPullMask
builder.xorMethod = xorMethod
return builder return builder
} }
} }

View File

@ -65,8 +65,6 @@ extension OpenVPN {
static let renegSec = NSRegularExpression("^reneg-sec +\\d+") static let renegSec = NSRegularExpression("^reneg-sec +\\d+")
static let xorMask = NSRegularExpression("^scramble +xormask +.$")
static let blockBegin = NSRegularExpression("^<[\\w\\-]+>") static let blockBegin = NSRegularExpression("^<[\\w\\-]+>")
static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>") static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>")
@ -121,6 +119,10 @@ extension OpenVPN {
static let routeNoPull = NSRegularExpression("^route-nopull") static let routeNoPull = NSRegularExpression("^route-nopull")
// MARK: Extra
static let xorInfo = NSRegularExpression("^scramble +(xormask|xorptrpos|reverse|obfuscate)[\\s]?([^\\s]+)?")
// MARK: Unsupported // MARK: Unsupported
// static let fragment = NSRegularExpression("^fragment +\\d+") // static let fragment = NSRegularExpression("^fragment +\\d+")
@ -266,7 +268,6 @@ extension OpenVPN {
var optKeepAliveSeconds: TimeInterval? var optKeepAliveSeconds: TimeInterval?
var optKeepAliveTimeoutSeconds: TimeInterval? var optKeepAliveTimeoutSeconds: TimeInterval?
var optRenegotiateAfterSeconds: TimeInterval? var optRenegotiateAfterSeconds: TimeInterval?
var optXorMask: UInt8?
// //
var optDefaultProto: SocketType? var optDefaultProto: SocketType?
var optDefaultPort: UInt16? var optDefaultPort: UInt16?
@ -294,6 +295,8 @@ extension OpenVPN {
var optProxyBypass: [String]? var optProxyBypass: [String]?
var optRedirectGateway: Set<RedirectGateway>? var optRedirectGateway: Set<RedirectGateway>?
var optRouteNoPull: Bool? var optRouteNoPull: Bool?
//
var optXorMethod: XORMethod?
log.verbose("Configuration file:") log.verbose("Configuration file:")
for line in lines { for line in lines {
@ -518,13 +521,6 @@ extension OpenVPN {
} }
optRenegotiateAfterSeconds = TimeInterval(arg) optRenegotiateAfterSeconds = TimeInterval(arg)
} }
Regex.xorMask.enumerateSpacedArguments(in: line) {
isHandled = true
if $0.count != 2 {
return
}
optXorMask = Character($0[1]).asciiValue
}
// MARK: Client // MARK: Client
@ -713,6 +709,36 @@ extension OpenVPN {
optRouteNoPull = true 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 { if let error = unsupportedError {
@ -821,7 +847,6 @@ extension OpenVPN {
sessionBuilder.randomizeEndpoint = optRandomizeEndpoint sessionBuilder.randomizeEndpoint = optRandomizeEndpoint
sessionBuilder.randomizeHostnames = optRandomizeHostnames sessionBuilder.randomizeHostnames = optRandomizeHostnames
sessionBuilder.mtu = optMTU sessionBuilder.mtu = optMTU
sessionBuilder.xorMask = optXorMask
// MARK: Server // MARK: Server
@ -939,6 +964,10 @@ extension OpenVPN {
sessionBuilder.routingPolicies = [RoutingPolicy](policies) sessionBuilder.routingPolicies = [RoutingPolicy](policies)
} }
// MARK: Extra
sessionBuilder.xorMethod = optXorMethod
// //
return Result( return Result(

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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())
}
}

View File

@ -127,11 +127,19 @@ class ConfigurationParserTests: XCTestCase {
func testXOR() throws { func testXOR() throws {
let cfg = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble xormask F"]) let cfg = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble xormask F"])
XCTAssertNil(cfg.warning) XCTAssertNil(cfg.warning)
XCTAssertEqual(cfg.configuration.xorMask, Character("F").asciiValue) XCTAssertEqual(cfg.configuration.xorMethod, OpenVPN.XORMethod.xormask(mask: Data(repeating: Character("F").asciiValue!, count:1)))
let cfg2 = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble xormask FFFF"]) let cfg2 = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble reverse"])
XCTAssertNil(cfg.warning) 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 { private func privateTestEncryptedCertificateKey(pkcs: String) throws {

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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())
}
}