Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
Merge pull request #6 from LibrePass/newAPI
Browse files Browse the repository at this point in the history
Implemented new API
  • Loading branch information
aeoliux authored Apr 8, 2024
2 parents 2d89990 + a8cee38 commit a673dad
Show file tree
Hide file tree
Showing 16 changed files with 255 additions and 284 deletions.
38 changes: 29 additions & 9 deletions LibrePass.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
3975184DC26DCD2D852A7C08 /* Pods_LibrePass.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A927B022513678BA42B54FCF /* Pods_LibrePass.framework */; };
B23597A32BC415F5000D7A4D /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = B23597A22BC415F5000D7A4D /* Context.swift */; };
B23D44F82BA62C6B00097960 /* OATH.swift in Sources */ = {isa = PBXBuildFile; fileRef = B23D44F72BA62C6B00097960 /* OATH.swift */; };
B25257BB2B91149D0028ABF3 /* LibrePassAccountSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25257BA2B91149D0028ABF3 /* LibrePassAccountSettings.swift */; };
B25257BF2B91341D0028ABF3 /* vaultScreenshot.png in Resources */ = {isa = PBXBuildFile; fileRef = B25257BE2B91341D0028ABF3 /* vaultScreenshot.png */; };
Expand Down Expand Up @@ -36,6 +37,7 @@
/* Begin PBXFileReference section */
2816A09CADE280F72E52A29A /* Pods-LibrePass.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LibrePass.release.xcconfig"; path = "Target Support Files/Pods-LibrePass/Pods-LibrePass.release.xcconfig"; sourceTree = "<group>"; };
A927B022513678BA42B54FCF /* Pods_LibrePass.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LibrePass.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B23597A22BC415F5000D7A4D /* Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = "<group>"; };
B23D44F72BA62C6B00097960 /* OATH.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATH.swift; sourceTree = "<group>"; };
B25257BA2B91149D0028ABF3 /* LibrePassAccountSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrePassAccountSettings.swift; sourceTree = "<group>"; };
B25257BE2B91341D0028ABF3 /* vaultScreenshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vaultScreenshot.png; sourceTree = "<group>"; };
Expand Down Expand Up @@ -85,6 +87,30 @@
name = Frameworks;
sourceTree = "<group>";
};
B23597A42BC419FE000D7A4D /* Context */ = {
isa = PBXGroup;
children = (
B2DD90702B8D10D4003D84B7 /* NetworkMonitor.swift */,
B23597A22BC415F5000D7A4D /* Context.swift */,
);
path = Context;
sourceTree = "<group>";
};
B23597A52BC41A1C000D7A4D /* App */ = {
isa = PBXGroup;
children = (
B253F1D22B83942B00D048F9 /* LibrePassApp.swift */,
B2780F052B8A43C200835EE3 /* LibrePassRegistrationWindow.swift */,
B253F1CE2B83942B00D048F9 /* LibrePassLoginWindow.swift */,
B253F1CD2B83942B00D048F9 /* LibrePassLocalLogin.swift */,
B253F1CF2B83942B00D048F9 /* LibrePassManagerWindow.swift */,
B253F1CC2B83942B00D048F9 /* LibrePassCipherView.swift */,
B25257BA2B91149D0028ABF3 /* LibrePassAccountSettings.swift */,
B28E061F2B8BBD1900F48661 /* Utils.swift */,
);
path = App;
sourceTree = "<group>";
};
B2D444772B87CFF00031A685 /* API */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -126,16 +152,9 @@
B2D91A172B8393C60004561A /* LibrePass */ = {
isa = PBXGroup;
children = (
B23597A52BC41A1C000D7A4D /* App */,
B23597A42BC419FE000D7A4D /* Context */,
B2D444772B87CFF00031A685 /* API */,
B253F1D22B83942B00D048F9 /* LibrePassApp.swift */,
B2780F052B8A43C200835EE3 /* LibrePassRegistrationWindow.swift */,
B253F1CE2B83942B00D048F9 /* LibrePassLoginWindow.swift */,
B253F1CD2B83942B00D048F9 /* LibrePassLocalLogin.swift */,
B253F1CF2B83942B00D048F9 /* LibrePassManagerWindow.swift */,
B253F1CC2B83942B00D048F9 /* LibrePassCipherView.swift */,
B25257BA2B91149D0028ABF3 /* LibrePassAccountSettings.swift */,
B2DD90702B8D10D4003D84B7 /* NetworkMonitor.swift */,
B28E061F2B8BBD1900F48661 /* Utils.swift */,
B253F1E22B8394EE00D048F9 /* LibrePass-Bridging-Header.h */,
B2D91A1C2B8393C70004561A /* Assets.xcassets */,
B2D91A1E2B8393C70004561A /* Preview Content */,
Expand Down Expand Up @@ -291,6 +310,7 @@
B2DD90712B8D10D4003D84B7 /* NetworkMonitor.swift in Sources */,
B253F1DC2B83942B00D048F9 /* LibrePassAPI.swift in Sources */,
B253F1D92B83942B00D048F9 /* LibrePassLoginWindow.swift in Sources */,
B23597A32BC415F5000D7A4D /* Context.swift in Sources */,
B253F1D72B83942B00D048F9 /* LibrePassCipherView.swift in Sources */,
B253F1DF2B83942B00D048F9 /* Crypto.swift in Sources */,
B253F1DE2B83942B00D048F9 /* ApiClient.swift in Sources */,
Expand Down
120 changes: 46 additions & 74 deletions LibrePass/API/LibrePassAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ struct LibrePassClient {
init(apiUrl: String) {
self.client = ApiClient(apiUrl: apiUrl)
self.vault = LibrePassDecryptedVault(lastSync: 0)

if !LibrePassEncryptedVault.isVaultSavedLocally() {
_ = try? LibrePassEncryptedVault(lastSync: 0).saveVault()
}
}

init(credentials: LibrePassCredentialsDatabase, password: String) throws {
Expand All @@ -39,7 +35,6 @@ struct LibrePassClient {

self.sharedKey = SymmetricKey(data: serializedSharedKey)
self.credentialsDatabase = credentials
self.vault.key = self.sharedKey!
}

func getKeys(email: String, password: String, argon2options: Argon2IdOptions) throws -> (Data, Data, Data) {
Expand Down Expand Up @@ -147,113 +142,90 @@ struct LibrePassClient {
}

mutating func fetchCiphers() throws {
self.vault.key = self.sharedKey!

let resp = try self.client.request(path: "/api/cipher", body: nil, method: "GET")
UserDefaults.standard.removeObject(forKey: "vault")
try self.syncVault(request: SyncRequest(lastSyncTimestamp: 0, updated: [], deleted: []))
}

struct SyncRequest: Codable {
var lastSyncTimestamp: Int64
var updated: [LibrePassEncryptedCipher]
var deleted: [String]
}

mutating func syncVault() throws {
var updated: [LibrePassEncryptedCipher] = []
let encryptedVault = try LibrePassEncryptedVault.loadVault()

var cryptedVault = LibrePassEncryptedVault(lastSync: Int64(Date().timeIntervalSince1970))
cryptedVault.vault = try JSONDecoder().decode([LibrePassEncryptedCipher].self, from: resp)
for _ in cryptedVault.vault {
cryptedVault.toSync.append(false)
for (index, element) in encryptedVault.toSync.enumerated() {
if element {
updated.append(encryptedVault.vault[index])
}
}

try cryptedVault.saveVault()

self.vault = try cryptedVault.decryptVault(key: self.sharedKey!)
let syncRequest = SyncRequest(lastSyncTimestamp: encryptedVault.lastSync, updated: updated, deleted: self.vault.idstoDelete)
try self.syncVault(request: syncRequest)
}

mutating func syncVault() throws {
mutating func syncVault(request: SyncRequest) throws {
struct SyncResponse: Codable {
var ids: [String]
var ciphers: [LibrePassEncryptedCipher]
}

let encryptedVault = try LibrePassEncryptedVault.loadVault()
self.vault = try encryptedVault.decryptVault(key: self.sharedKey!)
self.vault = try LibrePassEncryptedVault.loadVault().decryptVault(key: self.sharedKey!)

var syncRequest = request
if syncRequest.lastSyncTimestamp == -2 {
syncRequest.lastSyncTimestamp = self.vault.lastSync
}

if networkMonitor.isConnected {
let resp = try self.client.request(path: "/api/cipher/sync?lastSync=" + String(self.vault.lastSync), body: nil, method: "GET")

let syncRequestJSON = try JSONEncoder().encode(syncRequest)
let resp = try self.client.request(path: "/api/cipher/sync", body: syncRequestJSON, method: "POST")
let updated = try JSONDecoder().decode(SyncResponse.self, from: resp)

var newVaultToSync: [Bool] = []
var newVault: [LibrePassCipher] = []
for (i, j) in self.vault.vault.enumerated() {
if updated.ids.firstIndex(where: { id in j.id == id }) == nil && !self.vault.toSync[i] {
continue
}

if let i2 = updated.ciphers.firstIndex(where: { cipher in cipher.id == j.id }) {
if let last1 = updated.ciphers[i2].lastModified, let last2 = j.lastModified {
if last1 > last2 || !self.vault.toSync[i] {
self.vault.vault[i] = try LibrePassCipher(encCipher: updated.ciphers[i2], key: self.sharedKey!)
self.vault.toSync[i] = false
}
} else if !self.vault.toSync[i] {
self.vault.vault[i] = try LibrePassCipher(encCipher: updated.ciphers[i2], key: self.sharedKey!)
self.vault.toSync[i] = false
}
}

if self.vault.toSync[i] {
if let _ = try? self.put(cipher: self.vault.vault[i]) {
self.vault.toSync[i] = false
}
}

newVault.append(self.vault.vault[i])
newVaultToSync.append(false)
}

for index in stride(from: self.vault.idstoDelete.count - 1, through: 0, by: -1) {
if let _ = try? self.delete(id: self.vault.idstoDelete[index]) {
if let _ = try? self.vault.remove(id: self.vault.idstoDelete[index], save: true) {
self.vault.idstoDelete.remove(at: index)
for id in updated.ids {
if let newCipher = updated.ciphers.first(where: { $0.id == id }) {
if let cipher = self.vault.vault.first(where: { $0.id == id }), let last1 = cipher.lastModified, let last2 = newCipher.lastModified, last1 > last2 {
continue
}

try self.vault.addOrReplace(cipher: try LibrePassCipher(encCipher: newCipher, key: self.sharedKey!), toSync: false, save: false)
}
}

for encCipher in updated.ciphers {
if self.vault.vault.firstIndex(where: { cipher in encCipher.id == cipher.id }) == nil {
newVault.append(try LibrePassCipher(encCipher: encCipher, key: self.sharedKey!))
newVaultToSync.append(false)
for cipher in self.vault.vault {
if updated.ids.first(where: { $0 == cipher.id }) == nil {
try self.vault.remove(cipher: cipher, save: false)
}
}

self.vault.vault = newVault
self.vault.toSync = newVaultToSync
self.vault.lastSync = Int64(Date().timeIntervalSince1970)
try self.vault.encryptVault().saveVault()
}
}

mutating func put(encryptedCipher: LibrePassEncryptedCipher) throws {
try self.put(cipher: LibrePassCipher(encCipher: encryptedCipher, key: self.sharedKey!))
if networkMonitor.isConnected {
try self.syncVault(request: SyncRequest(lastSyncTimestamp: -2, updated: [encryptedCipher], deleted: []))
} else {
try self.vault.addOrReplace(cipher: LibrePassCipher(encCipher: encryptedCipher, key: self.sharedKey!), toSync: true, save: true)
}
}

mutating func put(cipher: LibrePassCipher) throws {
let encrypted = try LibrePassEncryptedCipher(cipher: cipher, key: self.sharedKey!)

var toSync = false
if networkMonitor.isConnected {
let req = try JSONEncoder().encode(encrypted)

_ = try self.client.request(path: "/api/cipher", body: req, method: "PUT")
} else {
toSync = true
}

try self.vault.addOrReplace(cipher: cipher, toSync: toSync, save: true)
try self.put(encryptedCipher: encrypted)
}

mutating func delete(id: String) throws {
if networkMonitor.isConnected {
_ = try self.client.request(path: "/api/cipher/" + id, body: nil, method: "DELETE")
try self.syncVault(request: SyncRequest(lastSyncTimestamp: -2, updated: [], deleted: [id]))
} else {
self.vault.idstoDelete.append(id)
try self.vault.remove(id: id, save: true)
}

try self.vault.remove(id: id, save: true)
}

mutating func updateCredentials(email: String, password: String, passwordHint: String, oldSharedKey: String) throws {
Expand Down Expand Up @@ -321,7 +293,7 @@ struct LibrePassClient {
var code: String
}

let (oldPrivateData, oldPublicData, oldSharedData) = try self.getKeys(email: self.credentialsDatabase!.email, password: password, argon2options: self.credentialsDatabase!.argon2idParams)
let (_, oldPublicData, oldSharedData) = try self.getKeys(email: self.credentialsDatabase!.email, password: password, argon2options: self.credentialsDatabase!.argon2idParams)

if dataToHexString(data: oldPublicData) != self.credentialsDatabase!.publicKey {
throw LibrePassApiErrors.WithMessage(message: "Invalid credentials")
Expand Down
28 changes: 14 additions & 14 deletions LibrePass/API/LibrePassCipher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class LibrePassCipher: Codable {
var collection: String?
var favorite: Bool = false
var rePrompt: Bool = false
var version: Int = 1
var version: Int?
var created: Int64?
var lastModified: Int64?

Expand All @@ -43,8 +43,13 @@ class LibrePassCipher: Codable {
}
}

convenience init(encCipher: LibrePassEncryptedCipher, key: SymmetricKey) throws {
self.init(id: encCipher.id, owner: encCipher.owner, type: CipherType.Card)
init(encCipher: LibrePassEncryptedCipher, key: SymmetricKey) throws {
self.id = encCipher.id
self.owner = encCipher.owner
guard let type = LibrePassCipher.CipherType(rawValue: encCipher.type) else {
throw LibrePassApiErrors.WithMessage(message: "Invalid cipher type")
}
self.type = type
self.collection = encCipher.collection
self.favorite = encCipher.favorite
self.rePrompt = encCipher.rePrompt
Expand All @@ -53,26 +58,21 @@ class LibrePassCipher: Codable {
self.lastModified = encCipher.lastModified

let decoder = JSONDecoder()
switch encCipher.type {
case 0:
switch self.type {
case .Login:
self.loginData = try decoder.decode(CipherLoginData.self, from: aesGcmDecrypt(data: hexStringToData(string: encCipher.protectedData)!, key: key))
self.type = CipherType.Login
break
case 1:
case .SecureNote:
self.secureNoteData = try decoder.decode(CipherSecureNoteData.self, from: aesGcmDecrypt(data: hexStringToData(string: encCipher.protectedData)!, key: key))
self.type = CipherType.SecureNote
break
case 2:
case .Card:
self.cardData = try decoder.decode(CipherCardData.self, from: aesGcmDecrypt(data: hexStringToData(string: encCipher.protectedData)!, key: key))
self.type = CipherType.Card
break
default:
break
}
}

enum CipherType: Codable {
case Login
enum CipherType: Int, Codable {
case Login = 0
case SecureNote
case Card
}
Expand Down
6 changes: 2 additions & 4 deletions LibrePass/API/LibrePassEncryptedCipher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,23 @@ class LibrePassEncryptedCipher: Codable {
var collection: String?
var favorite: Bool
var rePrompt: Bool
var version: Int
var version: Int?
var created: Int64?
var lastModified: Int64?

init(cipher: LibrePassCipher, key: SymmetricKey) throws {
self.id = cipher.id
self.owner = cipher.owner

self.type = cipher.type.rawValue
switch cipher.type {
case .Login:
self.type = 0
self.protectedData = dataToHexString(data: try aesGcmEncrypt(data: JSONEncoder().encode(cipher.loginData!), key: key))
break
case .SecureNote:
self.type = 1
self.protectedData = dataToHexString(data: try aesGcmEncrypt(data: JSONEncoder().encode(cipher.secureNoteData!), key: key))
break
case .Card:
self.type = 2
self.protectedData = dataToHexString(data: try aesGcmEncrypt(data: JSONEncoder().encode(cipher.cardData!), key: key))
break
}
Expand Down
8 changes: 6 additions & 2 deletions LibrePass/API/Vault.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ struct LibrePassEncryptedVault: Codable {
var lastSync: Int64

static func loadVault() throws -> Self {
if !isVaultSavedLocally() {
try LibrePassEncryptedVault(lastSync: 0).saveVault()
}

let vaultJson = UserDefaults.standard.data(forKey: "vault")!

return try JSONDecoder().decode(Self.self, from: vaultJson)
Expand All @@ -27,7 +31,7 @@ struct LibrePassEncryptedVault: Codable {
}

func decryptVault(key: SymmetricKey) throws -> LibrePassDecryptedVault {
var ciphers: LibrePassDecryptedVault = LibrePassDecryptedVault(toSync: self.toSync, idstoDelete: self.idsToDelete, lastSync: self.lastSync, key: key)
var ciphers: LibrePassDecryptedVault = LibrePassDecryptedVault(toSync: [], idstoDelete: self.idsToDelete, lastSync: self.lastSync, key: key)
for (i, encCipher) in self.vault.enumerated() {
try ciphers.addOrReplace(cipher: try LibrePassCipher(encCipher: encCipher, key: key), toSync: self.toSync[i], save: false)
}
Expand All @@ -48,7 +52,7 @@ struct LibrePassDecryptedVault {
var key: SymmetricKey?

mutating func addOrReplace(cipher: LibrePassCipher, toSync: Bool, save: Bool) throws {
if let idx = self.vault.firstIndex(where: { ciph in ciph.id == cipher.id }) {
if let idx = self.vault.firstIndex(where: { cipher.id == $0.id }) {
self.vault[idx] = cipher
self.toSync[idx] = toSync
} else {
Expand Down
Loading

0 comments on commit a673dad

Please sign in to comment.