Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Import connections on iPhone. Open a .tablepro file from Files or AirDrop, or use Import Connections in the list menu. Encrypted files prompt for the passphrase, and you choose how to handle duplicates.
- Export connections on iPhone from the list menu and share the .tablepro file. Passwords are left out by default; include them by setting a passphrase that encrypts the file.

### Changed

- Data grid now serves the row count from its existing cache instead of recomputing it on every layout pass, reducing CPU churn while scrolling large result sets.
Expand Down
11 changes: 11 additions & 0 deletions Packages/TableProCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let package = Package(
.library(name: "TableProCoreTypes", targets: ["TableProCoreTypes"]),
.library(name: "TableProPluginKit", targets: ["TableProPluginKit"]),
.library(name: "TableProModels", targets: ["TableProModels"]),
.library(name: "TableProImport", targets: ["TableProImport"]),
.library(name: "TableProDatabase", targets: ["TableProDatabase"]),
.library(name: "TableProQuery", targets: ["TableProQuery"]),
.library(name: "TableProSync", targets: ["TableProSync"]),
Expand All @@ -35,6 +36,11 @@ let package = Package(
dependencies: ["TableProPluginKit", "TableProCoreTypes"],
path: "Sources/TableProModels"
),
.target(
name: "TableProImport",
dependencies: [],
path: "Sources/TableProImport"
),
.target(
name: "TableProDatabase",
dependencies: ["TableProModels", "TableProCoreTypes"],
Expand Down Expand Up @@ -65,6 +71,11 @@ let package = Package(
dependencies: ["TableProModels", "TableProPluginKit"],
path: "Tests/TableProModelsTests"
),
.testTarget(
name: "TableProImportTests",
dependencies: ["TableProImport"],
path: "Tests/TableProImportTests"
),
.testTarget(
name: "TableProDatabaseTests",
dependencies: ["TableProDatabase", "TableProModels"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
//
// ConnectionExportCrypto.swift
// TablePro
//
// AES-256-GCM encryption for connection export files with PBKDF2 key derivation.
//

import CommonCrypto
import CryptoKit
import Foundation

enum ConnectionExportCryptoError: LocalizedError {
public enum ConnectionExportCryptoError: LocalizedError {
case invalidPassphrase
case corruptData
case unsupportedVersion(UInt8)

var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .invalidPassphrase:
return String(localized: "Incorrect passphrase")
Expand All @@ -26,22 +19,21 @@ enum ConnectionExportCryptoError: LocalizedError {
}
}

enum ConnectionExportCrypto {
private static let magic = Data("TPRO".utf8) // 4 bytes
public enum ConnectionExportCrypto {
private static let magic = Data("TPRO".utf8)
private static let currentVersion: UInt8 = 1
private static let saltLength = 32
private static let nonceLength = 12
private static let pbkdf2Iterations: UInt32 = 600_000
private static let keyLength = 32 // AES-256
private static let keyLength = 32

// Header: magic (4) + version (1) + salt (32) + nonce (12) = 49 bytes
private static let headerLength = 4 + 1 + saltLength + nonceLength

static func isEncrypted(_ data: Data) -> Bool {
public static func isEncrypted(_ data: Data) -> Bool {
data.count > headerLength && data.prefix(4) == magic
}

static func encrypt(data: Data, passphrase: String) throws -> Data {
public static func encrypt(data: Data, passphrase: String) throws -> Data {
var salt = Data(count: saltLength)
let saltStatus = salt.withUnsafeMutableBytes { buffer -> OSStatus in
guard let baseAddress = buffer.baseAddress else { return errSecParam }
Expand All @@ -65,7 +57,7 @@ enum ConnectionExportCrypto {
return result
}

static func decrypt(data: Data, passphrase: String) throws -> Data {
public static func decrypt(data: Data, passphrase: String) throws -> Data {
guard data.count > headerLength else {
throw ConnectionExportCryptoError.corruptData
}
Expand Down
Loading
Loading