vibetunnel/ios/VibeTunnelTests/AuthenticationTests.swift
Peter Steinberger eab1e6c962 feat: Update bundle identifiers and add logging configuration profile
- Updated macOS test bundle IDs to use consistent naming pattern:
  - sh.vibetunnel.vibetunnelTests → sh.vibetunnel.vibetunnel.tests
  - sh.vibetunnel.vibetunnelTests.debug → sh.vibetunnel.vibetunnel.tests.debug
- Updated iOS test bundle ID:
  - sh.vibetunnel.VibeTunnelTests-Mobile → sh.vibetunnel.ios.tests
- Fixed iOS logging to use sh.vibetunnel.ios subsystem consistently
- Created logging configuration profile in apple/logging/ to enable full debug logging
- Configuration profile covers all VibeTunnel subsystems and bundle IDs
- Updated documentation to reflect new bundle identifiers and logging setup
2025-07-30 18:04:32 +02:00

356 lines
13 KiB
Swift

import Foundation
import Testing
@Suite("Authentication and Security Tests", .tags(.critical, .security))
struct AuthenticationTests {
// MARK: - Password Authentication
@Test("Password hashing and validation")
func passwordHashing() {
// Test password requirements
func isValidPassword(_ password: String) -> Bool {
password.count >= 8 &&
password.rangeOfCharacter(from: .uppercaseLetters) != nil &&
password.rangeOfCharacter(from: .lowercaseLetters) != nil &&
password.rangeOfCharacter(from: .decimalDigits) != nil
}
#expect(isValidPassword("Test1234") == true)
#expect(isValidPassword("weak") == false)
#expect(isValidPassword("ALLCAPS123") == false)
#expect(isValidPassword("nocaps123") == false)
#expect(isValidPassword("NoNumbers") == false)
}
@Test("Basic authentication header formatting")
func basicAuthHeader() {
let username = "testuser"
let password = "Test@123"
// Create Basic auth header
let credentials = "\(username):\(password)"
let encodedCredentials = credentials.data(using: .utf8)?.base64EncodedString() ?? ""
let authHeader = "Basic \(encodedCredentials)"
#expect(authHeader.hasPrefix("Basic "))
#expect(!encodedCredentials.isEmpty)
// Decode and verify
if let decodedData = Data(base64Encoded: encodedCredentials),
let decodedString = String(data: decodedData, encoding: .utf8)
{
#expect(decodedString == credentials)
}
}
@Test("Token-based authentication")
func tokenAuth() {
struct AuthToken {
let value: String
let expiresAt: Date
var isExpired: Bool {
Date() > expiresAt
}
var authorizationHeader: String {
"Bearer \(value)"
}
}
let futureDate = Date().addingTimeInterval(3_600) // 1 hour
let pastDate = Date().addingTimeInterval(-3_600) // 1 hour ago
let validToken = AuthToken(value: "valid-token-123", expiresAt: futureDate)
let expiredToken = AuthToken(value: "expired-token-456", expiresAt: pastDate)
#expect(!validToken.isExpired)
#expect(expiredToken.isExpired)
#expect(validToken.authorizationHeader == "Bearer valid-token-123")
}
// MARK: - Session Security
@Test("Session ID generation and validation")
func sessionIdSecurity() {
func generateSessionId() -> String {
// Generate cryptographically secure session ID
var bytes = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
return bytes.map { String(format: "%02x", $0) }.joined()
}
let sessionId1 = generateSessionId()
let sessionId2 = generateSessionId()
// Session IDs should be unique
#expect(sessionId1 != sessionId2)
// Should be 64 characters (32 bytes * 2 hex chars)
#expect(sessionId1.count == 64)
#expect(sessionId2.count == 64)
// Should only contain hex characters
let hexCharacterSet = CharacterSet(charactersIn: "0123456789abcdef")
#expect(sessionId1.rangeOfCharacter(from: hexCharacterSet.inverted) == nil)
}
@Test("Session timeout handling")
func sessionTimeout() {
struct Session {
let id: String
let createdAt: Date
let timeoutInterval: TimeInterval
var isExpired: Bool {
Date().timeIntervalSince(createdAt) > timeoutInterval
}
}
let activeSession = Session(
id: "active-123",
createdAt: Date(),
timeoutInterval: 3_600 // 1 hour
)
let expiredSession = Session(
id: "expired-456",
createdAt: Date().addingTimeInterval(-7_200), // 2 hours ago
timeoutInterval: 3_600 // 1 hour timeout
)
#expect(!activeSession.isExpired)
#expect(expiredSession.isExpired)
}
// MARK: - URL Security
@Test("Secure URL validation")
func secureURLValidation() {
func isSecureURL(_ urlString: String) -> Bool {
guard let url = URL(string: urlString) else { return false }
return url.scheme == "https" || url.scheme == "wss"
}
#expect(isSecureURL("https://example.com") == true)
#expect(isSecureURL("wss://example.com/socket") == true)
#expect(isSecureURL("http://example.com") == false)
#expect(isSecureURL("ws://example.com/socket") == false)
#expect(isSecureURL("ftp://example.com") == false)
#expect(isSecureURL("not-a-url") == false)
}
@Test("URL sanitization")
func uRLSanitization() {
func sanitizeURL(_ urlString: String) -> String? {
// Remove trailing slashes and whitespace
var sanitized = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
if sanitized.hasSuffix("/") {
sanitized = String(sanitized.dropLast())
}
// Validate URL - must have scheme and host
guard let url = URL(string: sanitized),
url.scheme != nil,
url.host != nil else { return nil }
return sanitized
}
#expect(sanitizeURL("https://example.com/") == "https://example.com")
#expect(sanitizeURL(" https://example.com ") == "https://example.com")
#expect(sanitizeURL("https://example.com/path/") == "https://example.com/path")
#expect(sanitizeURL("invalid url") == nil)
}
// MARK: - Certificate Pinning
@Test("Certificate validation logic")
func certificateValidation() {
struct CertificateValidator {
let pinnedCertificates: Set<String> // SHA256 hashes
func isValid(certificateHash: String) -> Bool {
pinnedCertificates.contains(certificateHash)
}
}
let validator = CertificateValidator(pinnedCertificates: [
"abc123def456", // Example hash
"789ghi012jkl" // Another example
])
#expect(validator.isValid(certificateHash: "abc123def456") == true)
#expect(validator.isValid(certificateHash: "unknown-hash") == false)
}
// MARK: - Input Sanitization
@Test("Command injection prevention")
func commandSanitization() {
func sanitizeCommand(_ input: String) -> String {
// Remove potentially dangerous characters
let dangerousCharacters = CharacterSet(charactersIn: ";&|`$(){}[]<>\"'\\")
return input.components(separatedBy: dangerousCharacters).joined(separator: " ")
}
#expect(sanitizeCommand("ls -la") == "ls -la")
#expect(sanitizeCommand("rm -rf /; echo 'hacked'") == "rm -rf / echo hacked ")
#expect(sanitizeCommand("cat /etc/passwd | grep root") == "cat /etc/passwd grep root")
#expect(sanitizeCommand("$(malicious_command)") == " malicious_command ")
}
@Test("Path traversal prevention")
func pathTraversalPrevention() {
func isValidPath(_ path: String, allowedRoot: String) -> Bool {
// Normalize the path
let normalizedPath = (path as NSString).standardizingPath
// Check for path traversal attempts
if normalizedPath.contains("..") {
return false
}
// Ensure path is within allowed root
return normalizedPath.hasPrefix(allowedRoot)
}
let allowedRoot = "/Users/app/documents"
#expect(isValidPath("/Users/app/documents/file.txt", allowedRoot: allowedRoot) == true)
#expect(isValidPath("/Users/app/documents/subfolder/file.txt", allowedRoot: allowedRoot) == true)
#expect(isValidPath("/Users/app/documents/../../../etc/passwd", allowedRoot: allowedRoot) == false)
#expect(isValidPath("/etc/passwd", allowedRoot: allowedRoot) == false)
}
// MARK: - Rate Limiting
@Test("Rate limiting implementation")
func rateLimiting() {
class RateLimiter {
private var requestCounts: [String: (count: Int, resetTime: Date)] = [:]
private let maxRequests: Int
private let windowDuration: TimeInterval
init(maxRequests: Int, windowDuration: TimeInterval) {
self.maxRequests = maxRequests
self.windowDuration = windowDuration
}
func shouldAllowRequest(for identifier: String) -> Bool {
let now = Date()
if let (count, resetTime) = requestCounts[identifier] {
if now > resetTime {
// Window expired, reset
requestCounts[identifier] = (1, now.addingTimeInterval(windowDuration))
return true
} else if count >= maxRequests {
return false
} else {
requestCounts[identifier] = (count + 1, resetTime)
return true
}
} else {
// First request
requestCounts[identifier] = (1, now.addingTimeInterval(windowDuration))
return true
}
}
}
let limiter = RateLimiter(maxRequests: 3, windowDuration: 60)
let clientId = "client-123"
// First 3 requests should be allowed
#expect(limiter.shouldAllowRequest(for: clientId) == true)
#expect(limiter.shouldAllowRequest(for: clientId) == true)
#expect(limiter.shouldAllowRequest(for: clientId) == true)
// 4th request should be blocked
#expect(limiter.shouldAllowRequest(for: clientId) == false)
// Different client should be allowed
#expect(limiter.shouldAllowRequest(for: "other-client") == true)
}
// MARK: - Secure Storage
@Test("Keychain storage security")
func keychainStorage() {
struct KeychainItem {
let service: String
let account: String
let data: Data
let accessGroup: String?
var query: [String: Any] {
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
if let accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
}
return query
}
}
let item = KeychainItem(
service: "sh.vibetunnel.ios",
account: "user-token",
data: "secret-token".data(using: .utf8)!,
accessGroup: nil
)
#expect(item.query[kSecClass as String] as? String == kSecClassGenericPassword as String)
#expect(item.query[kSecAttrService as String] as? String == "sh.vibetunnel.ios")
#expect(item.query[kSecAttrAccount as String] as? String == "user-token")
}
// MARK: - CORS and Origin Validation
@Test("CORS origin validation")
func cORSValidation() {
func isAllowedOrigin(_ origin: String, allowedOrigins: Set<String>) -> Bool {
// Check exact match
if allowedOrigins.contains(origin) {
return true
}
// Check wildcard patterns
for allowed in allowedOrigins {
if allowed == "*" {
return true
}
if allowed.contains("*") {
// Simple wildcard matching: replace * with any subdomain
let pattern = allowed.replacingOccurrences(of: "*", with: "[^.]+")
let regex = try? NSRegularExpression(pattern: "^" + pattern + "$")
if let regex,
regex.firstMatch(in: origin, range: NSRange(origin.startIndex..., in: origin)) != nil
{
return true
}
}
}
return false
}
let allowedOrigins: Set<String> = [
"https://app.vibetunnel.com",
"https://*.vibetunnel.com",
"http://localhost:3000"
]
#expect(isAllowedOrigin("https://app.vibetunnel.com", allowedOrigins: allowedOrigins) == true)
#expect(isAllowedOrigin("https://dev.vibetunnel.com", allowedOrigins: allowedOrigins) == true)
#expect(isAllowedOrigin("http://localhost:3000", allowedOrigins: allowedOrigins) == true)
#expect(isAllowedOrigin("https://evil.com", allowedOrigins: allowedOrigins) == false)
#expect(isAllowedOrigin("http://app.vibetunnel.com", allowedOrigins: allowedOrigins) == false)
}
}