Files
WerkoutIOS/SharedCore/Sources/SharedCore/TokenSecurity.swift

104 lines
3.5 KiB
Swift

import Foundation
public enum TokenSecurity {
public static let defaultRotationWindow: TimeInterval = 60 * 60
private static let authPrefixes = ["token", "bearer"]
// Basic high-entropy hex token detector for accidental commits.
private static let tokenRegex = try? NSRegularExpression(pattern: "\\b[a-fA-F0-9]{32,}\\b")
public static func containsPotentialHardcodedToken(in text: String) -> Bool {
guard let tokenRegex else {
return false
}
let range = NSRange(location: 0, length: text.utf16.count)
return tokenRegex.firstMatch(in: text, options: [], range: range) != nil
}
public static func isRedactedToken(_ token: String?) -> Bool {
guard let token else { return false }
let upper = token.uppercased()
return upper.contains("REDACTED") || upper.contains("YOUR_TOKEN") || upper.contains("PLACEHOLDER")
}
public static func sanitizeToken(_ token: String?) -> String? {
guard let rawToken = token?.trimmingCharacters(in: .whitespacesAndNewlines),
rawToken.isEmpty == false else {
return nil
}
let normalized = normalizeAuthPrefix(rawToken)
guard normalized.isEmpty == false,
isRedactedToken(normalized) == false else {
return nil
}
return normalized
}
public static func jwtExpiration(_ token: String) -> Date? {
let segments = token.split(separator: ".")
guard segments.count == 3 else {
return nil
}
let payloadSegment = String(segments[1])
guard let payloadData = base64URLDecode(payloadSegment),
let object = try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any],
let exp = object["exp"] as? TimeInterval else {
return nil
}
return Date(timeIntervalSince1970: exp)
}
public static func isExpired(_ token: String?, now: Date = Date()) -> Bool {
guard let token = sanitizeToken(token) else {
return true
}
guard let expiration = jwtExpiration(token) else {
// Non-JWT tokens cannot be locally validated for expiry.
return false
}
return expiration <= now
}
public static func shouldRotate(_ token: String?, now: Date = Date(), rotationWindow: TimeInterval = defaultRotationWindow) -> Bool {
guard let token = sanitizeToken(token),
let expiration = jwtExpiration(token) else {
return false
}
return expiration.timeIntervalSince(now) <= rotationWindow
}
private static func base64URLDecode(_ input: String) -> Data? {
var value = input
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let remainder = value.count % 4
if remainder > 0 {
value.append(String(repeating: "=", count: 4 - remainder))
}
return Data(base64Encoded: value)
}
private static func normalizeAuthPrefix(_ token: String) -> String {
let lowercased = token.lowercased()
for prefix in authPrefixes {
if lowercased == prefix {
return ""
}
let prefixed = "\(prefix) "
if lowercased.hasPrefix(prefixed) {
return String(token.dropFirst(prefixed.count)).trimmingCharacters(in: .whitespacesAndNewlines)
}
}
return token
}
}