mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
- 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
356 lines
13 KiB
Swift
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)
|
|
}
|
|
}
|