Skip to content
Merged
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
33 changes: 33 additions & 0 deletions .swiftpm/SilentKey-Package.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"configurations" : [
{
"id" : "86DBA0CF-7C5C-4C77-B1DA-42F46A4BD664",
"name" : "Test Scheme Action",
"options" : {

}
}
],
"defaultOptions" : {
"performanceAntipatternCheckerEnabled" : true,
"targetForVariableExpansion" : {
"containerPath" : "container:",
"identifier" : "SilentKey",
"name" : "SilentKey"
}
},
"testTargets" : [
{
"parallelizable" : true,
"selectedTags" : {
"includeXCTests" : true
},
"target" : {
"containerPath" : "container:",
"identifier" : "SilentKeyTests",
"name" : "SilentKeyTests"
}
}
],
"version" : 1
}
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ let package = Package(
.executableTarget(
name: "SilentKeyApp",
dependencies: ["SilentKeyCore"],
path: "Sources/SilentKeyApp"
path: "Sources/SilentKeyApp",
resources: [
.process("Resources")
]
),

// MARK: - Core Library
Expand Down
42 changes: 28 additions & 14 deletions Sources/Core/Crypto/EncryptionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,17 @@ public actor EncryptionManager {

private init() {}

/// Dérive une clé depuis un mot de passe
/// - Parameter password: Mot de passe maître
/// - Returns: Clé symétrique dérivée, ou nil en cas d'échec
public func deriveKey(from password: String) async throws -> SymmetricKey? {
/// Dérive une clé depuis un mot de passe et un sel
/// - Parameters:
/// - password: Mot de passe maître
/// - salt: Sel cryptographique
/// - Returns: Clé symétrique dérivée
public func deriveKey(from password: String, salt: Data) async throws -> SymmetricKey {
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deriveKey method signature changed from returning an optional SymmetricKey? to throwing and returning non-optional SymmetricKey. This is a breaking API change that will cause compilation errors in any code calling this method. All call sites need to be updated to handle the throwing behavior.

Copilot uses AI. Check for mistakes.
guard !password.isEmpty else {
return nil
throw KeyDerivationService.KeyDerivationError.invalidPassword
}

do {
// Générer ou récupérer le sel (pour démo, on génère un nouveau sel)
// En production, le sel devrait être stocké de manière persistante
let salt = try KeyDerivationService.generateSalt()
let masterKey = try KeyDerivationService.deriveMasterKey(from: password, salt: salt)
return masterKey
} catch {
return nil
}
return try KeyDerivationService.deriveMasterKey(from: password, salt: salt)
}

/// Chiffre un item codable
Expand All @@ -55,4 +49,24 @@ public actor EncryptionManager {
let decoder = JSONDecoder()
return try decoder.decode(type, from: plainData)
}

/// Chiffre des données avec un nouveau sel et retourne un conteneur sécurisé
public func encryptWithNewSalt(_ data: Data, password: String) async throws -> EncryptedContainer {
let salt = try KeyDerivationService.generateSalt()
let key = try await deriveKey(from: password, salt: salt)
let ciphertext = try AESEncryptionService.encrypt(data, with: key)
return EncryptedContainer(salt: salt, ciphertext: ciphertext)
}

/// Déchiffre un conteneur sécurisé avec un mot de passe
public func decryptContainer(_ container: EncryptedContainer, password: String) async throws -> Data {
let key = try await deriveKey(from: password, salt: container.salt)
return try AESEncryptionService.decrypt(container.ciphertext, with: key)
}
}

/// Conteneur pour données chiffrées avec leur sel
public struct EncryptedContainer: Codable {
public let salt: Data
public let ciphertext: Data
}
126 changes: 126 additions & 0 deletions Sources/Core/Models/CertificateModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// CertificateModels.swift
// SilentKey
//
// Models for SSL/TLS certificates and other digital certs
//

import Foundation

public struct CertificateSecret: SecretItemProtocol, EncryptableSecret, ExportableSecret {
public let id: UUID
public var title: String
public var notes: String?
public var tags: Set<String>
public let createdAt: Date
public var modifiedAt: Date
public var isFavorite: Bool

public var certificateName: String
public var issuer: String
public var expirationDate: Date?
public var encryptedFields: [String: Data]

public var category: SecretCategory {
.certificate
}

public var iconName: String {
category.icon
}

public init(
id: UUID = UUID(),
title: String,
certificateName: String,
issuer: String,
expirationDate: Date? = nil,
notes: String? = nil,
tags: Set<String> = [],
isFavorite: Bool = false
) {
self.id = id
self.title = title
self.certificateName = certificateName
self.issuer = issuer
self.expirationDate = expirationDate
self.notes = notes
self.tags = tags
self.createdAt = Date()
self.modifiedAt = Date()
self.isFavorite = isFavorite
self.encryptedFields = [:]
}

public func encryptedData() throws -> Data {
try JSONEncoder().encode(self)
}

public func validate() throws {
guard !title.isEmpty else { throw ValidationError.emptyTitle }
guard !certificateName.isEmpty else { throw ValidationError.emptyCertName }
}

public func searchableText() -> String {
[title, certificateName, issuer, notes ?? ""].joined(separator: " ")
}

public mutating func encrypt(field: String, value: String, using key: Data) throws {
guard let data = value.data(using: .utf8) else { throw CertificateError.encryptionFailed }
encryptedFields[field] = data
modifiedAt = Date()
}

public func decrypt(field: String, using key: Data) throws -> String {
guard let data = encryptedFields[field] else { throw CertificateError.fieldNotFound }
guard let value = String(data: data, encoding: .utf8) else { throw CertificateError.decryptionFailed }
return value
}

public var supportedExportFormats: [ExportFormat] {
[.json, .encrypted]
}

public func export(format: ExportFormat) throws -> Data {
switch format {
case .json: return try JSONEncoder().encode(self)
case .encrypted: return try encryptedData()
default: throw CertificateError.unsupportedFormat
}
}

public enum ValidationError: Error {
case emptyTitle
case emptyCertName
}
}

public enum CertificateError: Error {
case encryptionFailed
case decryptionFailed
case fieldNotFound
case unsupportedFormat
}

extension CertificateSecret: SecretTemplate {
public static var templateName: String { "Certificate" }
public static var templateDescription: String { "SSL/TLS and other digital certificates" }
public static var requiredFields: [FieldDefinition] {
[
FieldDefinition(name: "title", displayName: "Name", type: .text, isSecure: false, placeholder: "Server Cert", validationPattern: nil),
FieldDefinition(name: "certificateName", displayName: "Certificate Common Name", type: .text, isSecure: false, placeholder: "example.com", validationPattern: nil)
]
}
public static var optionalFields: [FieldDefinition] {
[
FieldDefinition(name: "issuer", displayName: "Issuer", type: .text, isSecure: false, placeholder: "Let's Encrypt", validationPattern: nil)
]
}
public static func create(from fields: [String: Any]) throws -> CertificateSecret {
guard let title = fields["title"] as? String,
let certName = fields["certificateName"] as? String else {
throw CertificateError.encryptionFailed
}
return CertificateSecret(title: title, certificateName: certName, issuer: (fields["issuer"] as? String) ?? "Unknown")
}
}
124 changes: 124 additions & 0 deletions Sources/Core/Models/PasswordModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//
// PasswordModels.swift
// SilentKey
//
// Models for passwords and login credentials
//

import Foundation

public struct PasswordSecret: SecretItemProtocol, EncryptableSecret, ExportableSecret {
public let id: UUID
public var title: String
public var notes: String?
public var tags: Set<String>
public let createdAt: Date
public var modifiedAt: Date
public var isFavorite: Bool

public var username: String
public var url: String?
public var encryptedFields: [String: Data]

public var category: SecretCategory {
.password
}

public var iconName: String {
category.icon
}

public init(
id: UUID = UUID(),
title: String,
username: String,
url: String? = nil,
notes: String? = nil,
tags: Set<String> = [],
isFavorite: Bool = false
) {
self.id = id
self.title = title
self.username = username
self.url = url
self.notes = notes
self.tags = tags
self.createdAt = Date()
self.modifiedAt = Date()
self.isFavorite = isFavorite
self.encryptedFields = [:]
}

public func encryptedData() throws -> Data {
try JSONEncoder().encode(self)
}

public func validate() throws {
guard !title.isEmpty else { throw ValidationError.emptyTitle }
guard !username.isEmpty else { throw ValidationError.emptyUsername }
}

public func searchableText() -> String {
[title, username, url ?? "", notes ?? ""].joined(separator: " ")
}

public mutating func encrypt(field: String, value: String, using key: Data) throws {
guard let data = value.data(using: .utf8) else { throw PasswordError.encryptionFailed }
encryptedFields[field] = data
modifiedAt = Date()
}

public func decrypt(field: String, using key: Data) throws -> String {
guard let data = encryptedFields[field] else { throw PasswordError.fieldNotFound }
guard let value = String(data: data, encoding: .utf8) else { throw PasswordError.decryptionFailed }
return value
}

public var supportedExportFormats: [ExportFormat] {
[.json, .encrypted, .csv]
}

public func export(format: ExportFormat) throws -> Data {
switch format {
case .json: return try JSONEncoder().encode(self)
case .encrypted: return try encryptedData()
default: throw PasswordError.unsupportedFormat
}
}

public enum ValidationError: Error {
case emptyTitle
case emptyUsername
}
}

public enum PasswordError: Error {
case encryptionFailed
case decryptionFailed
case fieldNotFound
case unsupportedFormat
}

extension PasswordSecret: SecretTemplate {
public static var templateName: String { "Password" }
public static var templateDescription: String { "Standard login credentials" }
public static var requiredFields: [FieldDefinition] {
[
FieldDefinition(name: "title", displayName: "Name", type: .text, isSecure: false, placeholder: "My Account", validationPattern: nil),
FieldDefinition(name: "username", displayName: "Username/Email", type: .text, isSecure: false, placeholder: "user@example.com", validationPattern: nil),
FieldDefinition(name: "password", displayName: "Password", type: .password, isSecure: true, placeholder: "Password", validationPattern: nil)
]
}
public static var optionalFields: [FieldDefinition] {
[
FieldDefinition(name: "url", displayName: "Website URL", type: .url, isSecure: false, placeholder: "https://example.com", validationPattern: nil)
]
}
public static func create(from fields: [String: Any]) throws -> PasswordSecret {
guard let title = fields["title"] as? String,
let username = fields["username"] as? String else {
throw PasswordError.encryptionFailed
}
return PasswordSecret(title: title, username: username, url: fields["url"] as? String)
}
}
Loading
Loading