Skip to content

File Encryptor — Swift source

Encrypt or decrypt any file with AES-256-GCM in your browser. Password-based, zero server contact. Drag, drop, done.

This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.

// file-encryptor — password-based AES-256-GCM file encryption.
//
// Language: Swift 5.9+ (Foundation + CryptoKit + CommonCrypto for PBKDF2)
// Ported from src/lib/file-encryptor.ts
// display source — part of CosmoDev's polyglot tool pages
//
// Wire format:  salt (16 B) || IV (12 B) || AES-256-GCM ciphertext + tag.
// The 256-bit key is derived from the password with PBKDF2-SHA256 and a fresh
// random salt per encryption, so the same file + password never encrypts to
// the same bytes, and the password itself is never stored or derivable from
// the output.

import Foundation
import CryptoKit
import CommonCrypto

let saltLength = 16
let ivLength = 12
let iterations = 100_000

// GCM appends a 16-byte auth tag to the ciphertext; the smallest possible
// encrypted payload is therefore salt + IV + tag = 44 bytes.
let tagLength = 16
let minLength = saltLength + ivLength + tagLength

enum FileEncryptorError: Error, CustomStringConvertible {
    case emptyPassword
    case emptyInput
    case tooShort
    case keyDerivationFailed
    case decryptionFailed

    var description: String {
        switch self {
        case .emptyPassword: return "Password must not be empty."
        case .emptyInput: return "Input data is empty - nothing to encrypt."
        case .tooShort:
            return "Input is too short to be an encrypted file (needs at least \(minLength) bytes: salt + IV + auth tag)."
        case .keyDerivationFailed: return "PBKDF2 key derivation failed."
        case .decryptionFailed: return "Decryption failed: wrong password or corrupted file."
        }
    }
}

/// PBKDF2-SHA256 (100k iterations) -> a CryptoKit AES-256 key.
func deriveKey(password: String, salt: [UInt8]) throws -> SymmetricKey {
    let passwordBytes = Array(password.utf8)
    var derived = [UInt8](repeating: 0, count: 32)
    let status = salt.withUnsafeBufferPointer { saltPtr in
        passwordBytes.withUnsafeBufferPointer { pwPtr in
            CCKeyDerivationPBKDF(
                CCPBKDFAlgorithm(kCCPBKDF2),
                pwPtr.baseAddress, passwordBytes.count,
                saltPtr.baseAddress, salt.count,
                CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
                UInt32(iterations),
                &derived, derived.count
            )
        }
    }
    guard status == kCCSuccess else { throw FileEncryptorError.keyDerivationFailed }
    return SymmetricKey(data: Data(derived))
}

// SystemRandomNumberGenerator is backed by a cryptographically secure source
// on Apple platforms (arc4random / CSPRNG), so it stands in for crypto.getRandomValues.
private func randomBytes(_ count: Int) -> [UInt8] {
    var rng = SystemRandomNumberGenerator()
    return (0..<count).map { _ in UInt8.random(in: .min ... .max, using: &rng) }
}

/// Encrypt `data` under `password`. Returns salt || IV || ciphertext+tag.
func encryptFile(_ data: [UInt8], password: String) throws -> [UInt8] {
    guard !password.isEmpty else { throw FileEncryptorError.emptyPassword }
    guard !data.isEmpty else { throw FileEncryptorError.emptyInput }
    let salt = randomBytes(saltLength)
    let iv = randomBytes(ivLength)
    let key = try deriveKey(password: password, salt: salt)
    let sealed = try AES.GCM.seal(Data(data), using: key, nonce: AES.GCM.Nonce(data: Data(iv)))
    // combined = ciphertext || 16-byte GCM tag (never nil: a nonce was supplied).
    guard let combined = sealed.combined else { throw FileEncryptorError.decryptionFailed }
    var out = [UInt8]()
    out.reserveCapacity(saltLength + ivLength + combined.count)
    out += salt
    out += iv
    out += [UInt8](combined)
    return out
}

/// Decrypt a payload produced by `encryptFile`. Throws when the password is
/// wrong or the payload was corrupted/tampered (GCM auth-tag failure).
func decryptFile(_ data: [UInt8], password: String) throws -> [UInt8] {
    guard !password.isEmpty else { throw FileEncryptorError.emptyPassword }
    guard data.count >= minLength else { throw FileEncryptorError.tooShort }
    let salt = Array(data[0..<saltLength])
    let iv = Array(data[saltLength..<(saltLength + ivLength)])
    let ciphertext = Array(data[(saltLength + ivLength)...])
    let key = try deriveKey(password: password, salt: salt)
    do {
        let box = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: Data(iv)),
                                        ciphertext: Data(ciphertext.dropLast(tagLength)),
                                        tag: Data(ciphertext.suffix(tagLength)))
        let plain = try AES.GCM.open(box, using: key)
        return [UInt8](plain)
    } catch {
        // A GCM auth-tag failure means the key did not match (wrong password) or
        // the payload was modified after encryption.
        throw FileEncryptorError.decryptionFailed
    }
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →