Skip to content

Commit

Permalink
[BITAU-122] [BITAU-133] Add encryption to/from shared the CoreData st…
Browse files Browse the repository at this point in the history
…ore (#938)
  • Loading branch information
brant-livefront committed Sep 20, 2024
1 parent b1a16a0 commit 6a820f4
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 78 deletions.
20 changes: 2 additions & 18 deletions AuthenticatorBridgeKit/AuthenticatorBridgeItemDataModel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

/// A struct for storing information about items that are shared between the Bitwarden and Authenticator apps.
/// A struct for storing **encrypted** information about items that are shared between the Bitwarden
/// and Authenticator apps.
///
public struct AuthenticatorBridgeItemDataModel: Codable, Equatable {
// MARK: Properties
Expand All @@ -19,21 +20,4 @@ public struct AuthenticatorBridgeItemDataModel: Codable, Equatable {

/// The username of the Bitwarden account that owns this iteam.
public let username: String?

/// Initialize an `AuthenticatorBridgeItemDataModel` with the values provided.
///
/// - Parameters:
/// - favorite: Bool indicating if this item is a favorite.
/// - id: The unique id of the item.
/// - name: The name of the item.
/// - totpKey: The TOTP key used to generate codes.
/// - username: The username of the Bitwarden account that owns this iteam.
///
public init(favorite: Bool, id: String, name: String, totpKey: String?, username: String?) {
self.favorite = favorite
self.id = id
self.name = name
self.totpKey = totpKey
self.username = username
}
}
40 changes: 40 additions & 0 deletions AuthenticatorBridgeKit/AuthenticatorBridgeItemDataView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

/// A struct for storing **unencrypted** information about items that are shared between the Bitwarden
/// and Authenticator apps.
///
public struct AuthenticatorBridgeItemDataView: Codable, Equatable {
// MARK: Properties

/// Bool indicating if this item is a favorite.
public let favorite: Bool

/// The unique id of the item.
public let id: String

/// The name of the item.
public let name: String

/// The TOTP key used to generate codes.
public let totpKey: String?

/// The username of the Bitwarden account that owns this iteam.
public let username: String?

/// Initialize an `AuthenticatorBridgeItemDataView` with the values provided.
///
/// - Parameters:
/// - favorite: Bool indicating if this item is a favorite.
/// - id: The unique id of the item.
/// - name: The name of the item.
/// - totpKey: The TOTP key used to generate codes.
/// - username: The username of the Bitwarden account that owns this item.
///
public init(favorite: Bool, id: String, name: String, totpKey: String?, username: String?) {
self.favorite = favorite
self.id = id
self.name = name
self.totpKey = totpKey
self.username = username
}
}
34 changes: 23 additions & 11 deletions AuthenticatorBridgeKit/AuthenticatorBridgeItemService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ public protocol AuthenticatorBridgeItemService {
///
/// - Parameter userId: the id of the user for which to fetch items.
///
func fetchAllForUserId(_ userId: String) async throws -> [AuthenticatorBridgeItemDataModel]
func fetchAllForUserId(_ userId: String) async throws -> [AuthenticatorBridgeItemDataView]

/// Inserts the list of items into the store for the given userId.
///
/// - Parameters:
/// - items: The list of `AuthenticatorBridgeItemDataModel` to be inserted into the store.
/// - userId: the id of the user for which to insert the items.
///
func insertItems(_ items: [AuthenticatorBridgeItemDataModel],
func insertItems(_ items: [AuthenticatorBridgeItemDataView],
forUserId userId: String) async throws

/// Deletes all existing items for a given user and inserts new items for the list of items provided.
Expand All @@ -33,7 +33,7 @@ public protocol AuthenticatorBridgeItemService {
/// - items: The new items to be inserted into the store
/// - userId: The userId of the items to be removed and then replaces with items.
///
func replaceAllItems(with items: [AuthenticatorBridgeItemDataModel],
func replaceAllItems(with items: [AuthenticatorBridgeItemDataView],
forUserId userId: String) async throws
}

Expand All @@ -42,6 +42,9 @@ public protocol AuthenticatorBridgeItemService {
public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemService {
// MARK: Properties

/// Cryptography service for encrypting/decrypting items.
let cryptoService: SharedCryptographyService

/// The CoreData store for working with shared data.
let dataStore: AuthenticatorBridgeDataStore

Expand All @@ -53,10 +56,14 @@ public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemServi
/// Initialize a `DefaultAuthenticatorBridgeItemService`
///
/// - Parameters:
/// - cryptoService: Cryptography service for encrypting/decrypting items.
/// - dataStore: The CoreData store for working with shared data
/// - sharedKeychainRepository: The keychain repository for working with the shared key.
///
init(dataStore: AuthenticatorBridgeDataStore, sharedKeychainRepository: SharedKeychainRepository) {
init(cryptoService: SharedCryptographyService,
dataStore: AuthenticatorBridgeDataStore,
sharedKeychainRepository: SharedKeychainRepository) {
self.cryptoService = cryptoService
self.dataStore = dataStore
self.sharedKeychainRepository = sharedKeychainRepository
}
Expand All @@ -75,13 +82,13 @@ public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemServi
///
/// - Parameter userId: the id of the user for which to fetch items.
///
public func fetchAllForUserId(_ userId: String) async throws -> [AuthenticatorBridgeItemDataModel] {
public func fetchAllForUserId(_ userId: String) async throws -> [AuthenticatorBridgeItemDataView] {
let fetchRequest = AuthenticatorBridgeItemData.fetchByUserIdRequest(userId: userId)
let result = try dataStore.backgroundContext.fetch(fetchRequest)

return result.compactMap { data in
let encryptedItems = result.compactMap { data in
data.model
}
return try await cryptoService.decryptAuthenticatorItems(encryptedItems)
}

/// Inserts the list of items into the store for the given userId.
Expand All @@ -90,10 +97,11 @@ public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemServi
/// - items: The list of `AuthenticatorBridgeItemDataModel` to be inserted into the store.
/// - userId: the id of the user for which to insert the items.
///
public func insertItems(_ items: [AuthenticatorBridgeItemDataModel],
public func insertItems(_ items: [AuthenticatorBridgeItemDataView],
forUserId userId: String) async throws {
let encryptedItems = try await cryptoService.encryptAuthenticatorItems(items)
try await dataStore.executeBatchInsert(
AuthenticatorBridgeItemData.batchInsertRequest(objects: items, userId: userId)
AuthenticatorBridgeItemData.batchInsertRequest(objects: encryptedItems, userId: userId)
)
}

Expand All @@ -103,10 +111,14 @@ public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemServi
/// - items: The new items to be inserted into the store
/// - userId: The userId of the items to be removed and then replaces with items.
///
public func replaceAllItems(with items: [AuthenticatorBridgeItemDataModel],
public func replaceAllItems(with items: [AuthenticatorBridgeItemDataView],
forUserId userId: String) async throws {
let encryptedItems = try await cryptoService.encryptAuthenticatorItems(items)
let deleteRequest = AuthenticatorBridgeItemData.deleteByUserIdRequest(userId: userId)
let insertRequest = try AuthenticatorBridgeItemData.batchInsertRequest(objects: items, userId: userId)
let insertRequest = try AuthenticatorBridgeItemData.batchInsertRequest(
objects: encryptedItems,
userId: userId
)
try await dataStore.executeBatchReplace(
deleteRequest: deleteRequest,
insertRequest: insertRequest
Expand Down
143 changes: 143 additions & 0 deletions AuthenticatorBridgeKit/SharedCryptographyService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import CryptoKit
import Foundation

// MARK: - SharedCryptographyService

/// A service for handling encrypting/decrypting items to be shared between the main
/// Bitwarden app and the Authenticator app.
///
public protocol SharedCryptographyService: AnyObject {
/// Takes an array of `AuthenticatorBridgeItemDataModel` with encrypted data and
/// returns the list with each member decrypted.
///
/// - Parameter items: The encrypted array of items to be decrypted
/// - Returns: the array of items with their data decrypted
/// - Throws: AuthenticatorKeychainServiceError.keyNotFound if the Authenticator
/// key is not in the shared repository.
///
func decryptAuthenticatorItems(
_ items: [AuthenticatorBridgeItemDataModel]
) async throws -> [AuthenticatorBridgeItemDataView]

/// Takes an array of `AuthenticatorBridgeItemDataModel` with decrypted data and
/// returns the list with each member encrypted.
///
/// - Parameter items: The decrypted array of items to be encrypted
/// - Returns: the array of items with their data encrypted
/// - Throws: AuthenticatorKeychainServiceError.keyNotFound if the Authenticator
/// key is not in the shared repository.
///
func encryptAuthenticatorItems(
_ items: [AuthenticatorBridgeItemDataView]
) async throws -> [AuthenticatorBridgeItemDataModel]
}

/// A concrete implementation of the `SharedCryptographyService` protocol.
///
public class DefaultAuthenticatorCryptographyService: SharedCryptographyService {
// MARK: Properties

/// the `SharedKeyRepository` to obtain the shared Authenticator
/// key to use in encrypting/decrypting
private let sharedKeychainRepository: SharedKeychainRepository

// MARK: Initialization

/// Initialize a `DefaultAuthenticatorCryptographyService`
///
/// - Parameter sharedKeychainRepository: the `SharedKeyRepository` to obtain the shared Authenticator
/// key to use in encrypting/decrypting
///
public init(sharedKeychainRepository: SharedKeychainRepository) {
self.sharedKeychainRepository = sharedKeychainRepository
}

// MARK: Methods

public func decryptAuthenticatorItems(
_ items: [AuthenticatorBridgeItemDataModel]
) async throws -> [AuthenticatorBridgeItemDataView] {
let key = try await sharedKeychainRepository.getAuthenticatorKey()
let symmetricKey = SymmetricKey(data: key)

return items.map { item in
AuthenticatorBridgeItemDataView(
favorite: item.favorite,
id: item.id,
name: (try? decrypt(item.name, withKey: symmetricKey)) ?? "",
totpKey: try? decrypt(item.totpKey, withKey: symmetricKey),
username: try? decrypt(item.username, withKey: symmetricKey)
)
}
}

public func encryptAuthenticatorItems(
_ items: [AuthenticatorBridgeItemDataView]
) async throws -> [AuthenticatorBridgeItemDataModel] {
let key = try await sharedKeychainRepository.getAuthenticatorKey()
let symmetricKey = SymmetricKey(data: key)

return items.map { item in
AuthenticatorBridgeItemDataModel(
favorite: item.favorite,
id: item.id,
name: encrypt(item.name, withKey: symmetricKey) ?? "",
totpKey: encrypt(item.totpKey, withKey: symmetricKey),
username: encrypt(item.username, withKey: symmetricKey)
)
}
}

/// Decrypts a string given a key.
///
/// - Parameters:
/// - string: The string to decrypt.
/// - key: The key to decrypt with.
/// - Returns: A decrypted string, or `nil` if the passed-in string was not encoded in Base64.
///
private func decrypt(_ string: String?, withKey key: SymmetricKey) throws -> String? {
guard let string, !string.isEmpty, let data = Data(base64Encoded: string) else {
return nil
}
let encryptedSealedBox = try AES.GCM.SealedBox(
combined: data
)
let decryptedBox = try AES.GCM.open(
encryptedSealedBox,
using: key
)
return String(data: decryptedBox, encoding: .utf8)
}

/// Encrypt a string with the given key.
///
/// - Parameters:
/// - plainText: The string to encrypt
/// - key: The key to use to encrypt the string
/// - Returns: An encrypted string or `nil` if the string was nil
///
private func encrypt(_ plainText: String?, withKey key: SymmetricKey) -> String? {
guard let plainText else {
return nil
}

let nonce = randomData(lengthInBytes: 12)

let plainData = plainText.data(using: .utf8)
let sealedData = try? AES.GCM.seal(plainData!, using: key, nonce: AES.GCM.Nonce(data: nonce))
return sealedData?.combined?.base64EncodedString()
}

/// Generate random data of the length specified
///
/// - Parameter lengthInBytes: the length of the random data to generate
/// - Returns: random `Data` of the length in bytes requested.
///
private func randomData(lengthInBytes: Int) -> Data {
var data = Data(count: lengthInBytes)
_ = data.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, lengthInBytes, bytes.baseAddress!)
}
return data
}
}
Loading

0 comments on commit 6a820f4

Please sign in to comment.