Text Encryptor — Swift source
Encrypt or decrypt text with a password using AES-256-GCM. Share the ciphertext safely - only someone with the password can read it.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// text-encryptor — password-based text encryption (PBKDF2 + AES-256-GCM).
//
// Language: Swift 5.9+ (Foundation + CryptoKit + CommonCrypto for PBKDF2)
// Ported from src/lib/text-encryptor.ts
// display source — part of CosmoDev's polyglot tool pages
//
// Format: PBKDF2 (SHA-256, 100k iterations, 16-byte random salt) derives an
// AES-256 key from the password; AES-GCM encrypts with a 12-byte random IV.
// The Base64 output packs salt + IV + ciphertext (+ GCM tag), so every
// encryption is unique and self-contained — decrypt needs only the string and
// the password.
import Foundation
import CryptoKit
import CommonCrypto
let pbkdf2Iterations = 100_000
let saltBytes = 16
let ivBytes = 12
/// Smallest valid packed payload: salt + IV + one AES-GCM block (tag).
let minBytes = saltBytes + ivBytes + 16
let base64Alphabet = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
// MARK: - Base64 codec (pure implementation, mirrors the TS reference)
/// [UInt8] -> canonical Base64.
func bytesToBase64(_ bytes: [UInt8]) -> String {
var out = ""
var i = 0
while i < bytes.count {
let b0 = Int(bytes[i])
let b1 = i + 1 < bytes.count ? Int(bytes[i + 1]) : nil
let b2 = i + 2 < bytes.count ? Int(bytes[i + 2]) : nil
out.append(base64Alphabet[b0 >> 2])
out.append(base64Alphabet[((b0 & 0x03) << 4) | (b1 ?? 0) >> 4])
out.append(b1 == nil ? "=" : base64Alphabet[((b1! & 0x0f) << 2) | (b2 ?? 0) >> 6])
out.append(b2 == nil ? "=" : base64Alphabet[b2! & 0x3f])
i += 3
}
return out
}
/// Base64 -> [UInt8]. Accepts surrounding whitespace; throws on any other
/// non-alphabet character, wrong length, or misplaced padding.
func base64ToBytes(_ b64: String) throws -> [UInt8] {
let clean = b64.filter { !$0.isWhitespace }
if clean.isEmpty { throw TextEncryptorError.invalidBase64("input is empty") }
if clean.count % 4 != 0 { throw TextEncryptorError.invalidBase64("length must be a multiple of 4") }
let chars = Array(clean)
if let bad = chars.first(where: { !base64Alphabet.contains($0) && $0 != "=" }) {
throw TextEncryptorError.invalidBase64("unexpected character \"\(bad)\"")
}
if let badPad = chars.firstIndex(of: "=") {
let tail = chars[(badPad + 1)...]
if badPad < chars.count - 2 || tail.contains(where: { $0 != "=" }) {
throw TextEncryptorError.invalidBase64("misplaced padding")
}
}
var outLength = (clean.count / 4) * 3
if clean.hasSuffix("==") { outLength -= 2 }
else if clean.hasSuffix("=") { outLength -= 1 }
var bytes = [UInt8](repeating: 0, count: outLength)
var p = 0
var i = 0
while i < chars.count {
let c0 = base64Alphabet.firstIndex(of: chars[i])!
let c1 = base64Alphabet.firstIndex(of: chars[i + 1])!
let c2 = chars[i + 2] == "=" ? nil : base64Alphabet.firstIndex(of: chars[i + 2])
let c3 = chars[i + 3] == "=" ? nil : base64Alphabet.firstIndex(of: chars[i + 3])
if p < outLength { bytes[p] = UInt8((c0 << 2) | (c1 >> 4)); p += 1 }
if let c2 = c2, p < outLength { bytes[p] = UInt8(((c1 & 0x0f) << 4) | (c2 >> 2)); p += 1 }
if let c2 = c2, let c3 = c3, p < outLength { bytes[p] = UInt8(((c2 & 0x03) << 6) | c3); p += 1 }
i += 4
}
return bytes
}
// MARK: - Errors
enum TextEncryptorError: Error, CustomStringConvertible {
case nothingToEncrypt
case emptyPassword
case nothingToDecrypt
case invalidBase64(String)
case ciphertextTooShort
case keyDerivationFailed
case decryptionFailed
var description: String {
switch self {
case .nothingToEncrypt: return "Nothing to encrypt: input is empty"
case .emptyPassword: return "Password must not be empty"
case .nothingToDecrypt: return "Nothing to decrypt: input is empty"
case .invalidBase64(let why): return "Invalid Base64: \(why)"
case .ciphertextTooShort: return "Ciphertext too short - not a valid salt + IV + AES-GCM payload"
case .keyDerivationFailed: return "PBKDF2 key derivation failed"
case .decryptionFailed: return "Decryption failed - wrong password or corrupted ciphertext"
}
}
}
// MARK: - Key derivation
/// 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(pbkdf2Iterations),
&derived, derived.count
)
}
}
guard status == kCCSuccess else { throw TextEncryptorError.keyDerivationFailed }
return SymmetricKey(data: Data(derived))
}
// SystemRandomNumberGenerator is backed by a cryptographically secure source
// on Apple platforms, 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) }
}
// MARK: - Public API
/// Encrypt UTF-8 `plaintext` under `password` and return the Base64 string
/// packing salt + IV + AES-256-GCM ciphertext. Throws if either input is empty.
func encryptText(_ plaintext: String, password: String) throws -> String {
guard !plaintext.isEmpty else { throw TextEncryptorError.nothingToEncrypt }
guard !password.isEmpty else { throw TextEncryptorError.emptyPassword }
let salt = randomBytes(saltBytes)
let iv = randomBytes(ivBytes)
let key = try deriveKey(password: password, salt: salt)
let sealed = try AES.GCM.seal(Data(plaintext.utf8), 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 TextEncryptorError.decryptionFailed }
var packed = [UInt8]()
packed.reserveCapacity(saltBytes + ivBytes + combined.count)
packed += salt
packed += iv
packed += [UInt8](combined)
return bytesToBase64(packed)
}
/// Decrypt a Base64 string produced by `encryptText` back to plaintext. Throws
/// on empty input/password, invalid Base64, a payload too short to be one of
/// ours, or a GCM authentication failure (wrong password / tampered data).
func decryptText(_ ciphertext: String, password: String) throws -> String {
let trimmed = ciphertext.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { throw TextEncryptorError.nothingToDecrypt }
guard !password.isEmpty else { throw TextEncryptorError.emptyPassword }
let packed = try base64ToBytes(trimmed)
guard packed.count >= minBytes else { throw TextEncryptorError.ciphertextTooShort }
let salt = Array(packed[0..<saltBytes])
let iv = Array(packed[saltBytes..<(saltBytes + ivBytes)])
let data = Array(packed[(saltBytes + ivBytes)...])
let key = try deriveKey(password: password, salt: salt)
do {
let box = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: Data(iv)),
ciphertext: Data(data.dropLast(16)),
tag: Data(data.suffix(16)))
let plain = try AES.GCM.open(box, using: key)
guard let text = String(data: plain, encoding: .utf8) else {
throw TextEncryptorError.decryptionFailed
}
return text
} catch let error as TextEncryptorError {
throw error
} catch {
throw TextEncryptorError.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 →