mirror of
https://github.com/bitwarden/android.git
synced 2024-12-26 02:48:29 +03:00
[EC-835] Add kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly on Watch Keychain (#2250)
* EC-835 Added in the Watch app keychain accessible when passcode set this device only and when the passcode is set to signal the iPhone to trigger a sync on opening the watch app * EC-835 Embed LocalAuthentication framework into the watch app to fix no such module when importing it * EC-835 Changed approach to check if Watch has passcode enabled by using Keychain accessible kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly instead of LAContext * EC-835 Fix weird error saying unassigned local variable on the CI compiler. It seems it doesn't realize of the full condition
This commit is contained in:
parent
4347c2f81d
commit
e72932cbaa
13 changed files with 225 additions and 78 deletions
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||||
using Bit.App.Services;
|
using Bit.App.Services;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using WatchConnectivity;
|
using WatchConnectivity;
|
||||||
|
|
||||||
|
@ -11,12 +12,16 @@ namespace Bit.iOS.Core.Services
|
||||||
{
|
{
|
||||||
public class WatchDeviceService : BaseWatchDeviceService
|
public class WatchDeviceService : BaseWatchDeviceService
|
||||||
{
|
{
|
||||||
|
const string ACTION_MESSAGE_KEY = "actionMessage";
|
||||||
|
const string TRIGGER_SYNC_ACTION_KEY = "triggerSync";
|
||||||
|
|
||||||
public WatchDeviceService(ICipherService cipherService,
|
public WatchDeviceService(ICipherService cipherService,
|
||||||
IEnvironmentService environmentService,
|
IEnvironmentService environmentService,
|
||||||
IStateService stateService,
|
IStateService stateService,
|
||||||
IVaultTimeoutService vaultTimeoutService)
|
IVaultTimeoutService vaultTimeoutService)
|
||||||
: base(cipherService, environmentService, stateService, vaultTimeoutService)
|
: base(cipherService, environmentService, stateService, vaultTimeoutService)
|
||||||
{
|
{
|
||||||
|
WCSessionManager.SharedManager.OnMessagedReceived += OnMessagedReceived;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsConnected => WCSessionManager.SharedManager.IsSessionActivated;
|
public override bool IsConnected => WCSessionManager.SharedManager.IsSessionActivated;
|
||||||
|
@ -44,5 +49,17 @@ namespace Bit.iOS.Core.Services
|
||||||
{
|
{
|
||||||
WCSessionManager.SharedManager.StartSession();
|
WCSessionManager.SharedManager.StartSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnMessagedReceived(WCSession session, Dictionary<string, object> data)
|
||||||
|
{
|
||||||
|
if (data != null
|
||||||
|
&&
|
||||||
|
data.TryGetValue(ACTION_MESSAGE_KEY, out var action)
|
||||||
|
&&
|
||||||
|
action as string == TRIGGER_SYNC_ACTION_KEY)
|
||||||
|
{
|
||||||
|
SyncDataToWatchAsync().FireAndForget();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
@ -21,6 +22,25 @@ namespace Bit.iOS.Core.Utilities
|
||||||
var NSKeys = dict.Keys.Select(x => keyConverter(x)).ToArray();
|
var NSKeys = dict.Keys.Select(x => keyConverter(x)).ToArray();
|
||||||
return NSDictionary<KTo, VTo>.FromObjectsAndKeys(NSValues, NSKeys, NSKeys.Count());
|
return NSDictionary<KTo, VTo>.FromObjectsAndKeys(NSValues, NSKeys, NSKeys.Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, object> ToDictionary(this NSDictionary<NSString, NSObject> nsDict)
|
||||||
|
{
|
||||||
|
return nsDict.ToDictionary(v => v?.ToString() as object);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, object> ToDictionary(this NSDictionary<NSString, NSObject> nsDict, Func<NSObject, object> valueTransformer)
|
||||||
|
{
|
||||||
|
return nsDict.ToDictionary(k => k.ToString(), v => valueTransformer(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<KTo, VTo> ToDictionary<KFrom, VFrom, KTo, VTo>(this NSDictionary<KFrom, VFrom> nsDict, Func<KFrom, KTo> keyConverter, Func<VFrom, VTo> valueConverter)
|
||||||
|
where KFrom : NSObject
|
||||||
|
where VFrom : NSObject
|
||||||
|
{
|
||||||
|
var keys = nsDict.Keys.Select(k => keyConverter(k)).ToArray();
|
||||||
|
var values = nsDict.Values.Select(v => valueConverter(v)).ToArray();
|
||||||
|
return keys.Zip(values, (k, v) => new { Key = k, Value = v })
|
||||||
|
.ToDictionary(x => x.Key, x => x.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.iOS.Core.Utilities;
|
using Bit.iOS.Core.Utilities;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using ObjCRuntime;
|
||||||
|
|
||||||
namespace WatchConnectivity
|
namespace WatchConnectivity
|
||||||
{
|
{
|
||||||
|
@ -15,19 +19,18 @@ namespace WatchConnectivity
|
||||||
private static readonly WCSessionManager sharedManager = new WCSessionManager();
|
private static readonly WCSessionManager sharedManager = new WCSessionManager();
|
||||||
private static WCSession session = WCSession.IsSupported ? WCSession.DefaultSession : null;
|
private static WCSession session = WCSession.IsSupported ? WCSession.DefaultSession : null;
|
||||||
|
|
||||||
public static string Device = "Phone";
|
public event WCSessionReceiveDataHandler OnApplicationContextUpdated;
|
||||||
|
public event WCSessionReceiveDataHandler OnMessagedReceived;
|
||||||
public event WCSessionReceiveDataHandler ApplicationContextUpdated;
|
public delegate void WCSessionReceiveDataHandler(WCSession session, Dictionary<string, object> data);
|
||||||
public event WCSessionReceiveDataHandler MessagedReceived;
|
|
||||||
public delegate void WCSessionReceiveDataHandler(WCSession session, Dictionary<string, object> applicationContext);
|
|
||||||
|
|
||||||
|
WCSessionUserInfoTransfer _transf;
|
||||||
|
|
||||||
private WCSession validSession
|
private WCSession validSession
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Paired status:{(session.Paired ? '✓' : '✗')}\n");
|
Debug.WriteLine($"Paired status:{(session.Paired ? '✓' : '✗')}\n");
|
||||||
Console.WriteLine($"Watch App Installed status:{(session.WatchAppInstalled ? '✓' : '✗')}\n");
|
Debug.WriteLine($"Watch App Installed status:{(session.WatchAppInstalled ? '✓' : '✗')}\n");
|
||||||
return (session.Paired && session.WatchAppInstalled) ? session : null;
|
return (session.Paired && session.WatchAppInstalled) ? session : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,25 +65,14 @@ namespace WatchConnectivity
|
||||||
{
|
{
|
||||||
session.Delegate = this;
|
session.Delegate = this;
|
||||||
session.ActivateSession();
|
session.ActivateSession();
|
||||||
Console.WriteLine($"Started Watch Connectivity Session on {Device}");
|
Debug.WriteLine($"Started Watch Connectivity Session");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SessionReachabilityDidChange(WCSession session)
|
public override void SessionReachabilityDidChange(WCSession session)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Watch connectivity Reachable:{(session.Reachable ? '✓' : '✗')} from {Device}");
|
Debug.WriteLine($"Watch connectivity Reachable:{(session.Reachable ? '✓' : '✗')}");
|
||||||
// handle session reachability change
|
|
||||||
if (session.Reachable)
|
|
||||||
{
|
|
||||||
// great! continue on with Interactive Messaging
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// 😥 prompt the user to unlock their iOS device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Application Context Methods
|
|
||||||
|
|
||||||
public void SendBackgroundHighPriorityMessage(Dictionary<string, object> applicationContext)
|
public void SendBackgroundHighPriorityMessage(Dictionary<string, object> applicationContext)
|
||||||
{
|
{
|
||||||
|
@ -90,27 +82,24 @@ namespace WatchConnectivity
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sendSuccessfully = validSession.UpdateApplicationContext(applicationContext.ToNSDictionary(), out var error);
|
var sendSuccessfully = validSession.UpdateApplicationContext(applicationContext.ToNSDictionary(), out var error);
|
||||||
if (sendSuccessfully)
|
if (sendSuccessfully)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Sent App Context from {Device} \nPayLoad: {applicationContext.ToNSDictionary().ToString()} \n");
|
Debug.WriteLine($"Sent App Context \nPayLoad: {applicationContext.ToNSDictionary().ToString()} \n");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Error Updating Application Context: {error.LocalizedDescription}");
|
Debug.WriteLine($"Error Updating Application Context: {error.LocalizedDescription}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Exception Updating Application Context: {ex.Message}");
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
WCSessionUserInfoTransfer _transf;
|
|
||||||
public void SendBackgroundFifoHighPriorityMessage(Dictionary<string, object> message)
|
public void SendBackgroundFifoHighPriorityMessage(Dictionary<string, object> message)
|
||||||
{
|
{
|
||||||
if(validSession is null || validSession.ActivationState != WCSessionActivationState.Activated)
|
if(validSession is null || validSession.ActivationState != WCSessionActivationState.Activated)
|
||||||
|
@ -120,11 +109,10 @@ namespace WatchConnectivity
|
||||||
|
|
||||||
_transf?.Cancel();
|
_transf?.Cancel();
|
||||||
|
|
||||||
Console.WriteLine("Started transferring user info");
|
Debug.WriteLine("Started transferring user info");
|
||||||
|
|
||||||
_transf = session.TransferUserInfo(message.ToNSDictionary());
|
_transf = session.TransferUserInfo(message.ToNSDictionary());
|
||||||
|
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -133,53 +121,41 @@ namespace WatchConnectivity
|
||||||
{
|
{
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
}
|
}
|
||||||
Console.WriteLine("Finished transferring user info");
|
Debug.WriteLine("Finished transferring user info");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Error transferring user info " + ex);
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//session.SendMessage(dic,
|
|
||||||
// (dd) =>
|
|
||||||
// {
|
|
||||||
// Console.WriteLine(dd?.ToString());
|
|
||||||
// },
|
|
||||||
// error =>
|
|
||||||
// {
|
|
||||||
// Console.WriteLine(error?.ToString());
|
|
||||||
// }
|
|
||||||
//);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void DidReceiveApplicationContext(WCSession session, NSDictionary<NSString, NSObject> applicationContext)
|
public override void DidReceiveApplicationContext(WCSession session, NSDictionary<NSString, NSObject> applicationContext)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Receiving Message on {Device}");
|
Debug.WriteLine($"Receiving Message");
|
||||||
if (ApplicationContextUpdated != null)
|
if (OnApplicationContextUpdated != null)
|
||||||
{
|
{
|
||||||
var keys = applicationContext.Keys.Select(k => k.ToString()).ToArray();
|
var keys = applicationContext.Keys.Select(k => k.ToString()).ToArray();
|
||||||
var values = applicationContext.Values.Select(v => JsonConvert.DeserializeObject(v.ToString())).ToArray();
|
var values = applicationContext.Values.Select(v => JsonConvert.DeserializeObject(v.ToString())).ToArray();
|
||||||
var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v })
|
var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v })
|
||||||
.ToDictionary(x => x.Key, x => x.Value);
|
.ToDictionary(x => x.Key, x => x.Value);
|
||||||
|
|
||||||
ApplicationContextUpdated(session, dictionary);
|
OnApplicationContextUpdated(session, dictionary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override void DidReceiveMessage(WCSession session, NSDictionary<NSString, NSObject> message)
|
public override void DidReceiveMessage(WCSession session, NSDictionary<NSString, NSObject> message)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Receiving Message on {Device}");
|
Debug.WriteLine($"Receiving Message");
|
||||||
|
|
||||||
var keys = message.Keys.Select(k => k.ToString()).ToArray();
|
OnMessagedReceived?.Invoke(session, message.ToDictionary());
|
||||||
var values = message.Values.Select(v => v?.ToString() as object).ToArray();
|
|
||||||
var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v })
|
|
||||||
.ToDictionary(x => x.Key, x => x.Value);
|
|
||||||
|
|
||||||
MessagedReceived?.Invoke(session, dictionary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
public override void DidReceiveMessage(WCSession session, NSDictionary<NSString, NSObject> message, WCSessionReplyHandler replyHandler)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Receiving Message");
|
||||||
|
|
||||||
|
OnMessagedReceived?.Invoke(session, message.ToDictionary());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,37 @@ final class KeychainHelper {
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
|
func hasDeviceOwnerAuth() -> Bool {
|
||||||
|
let query: [String:Any] = [
|
||||||
|
kSecClass as String : kSecClassGenericPassword,
|
||||||
|
kSecAttrAccount as String : UUID().uuidString,
|
||||||
|
kSecAttrAccessible as String: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
|
||||||
|
kSecValueData as String: "hasPass".data(using: String.Encoding.utf8)!,
|
||||||
|
kSecReturnAttributes as String : kCFBooleanTrue as Any
|
||||||
|
]
|
||||||
|
|
||||||
|
var dataTypeRef: AnyObject?
|
||||||
|
var status = withUnsafeMutablePointer(to: &dataTypeRef) {
|
||||||
|
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == errSecItemNotFound {
|
||||||
|
let createStatus = SecItemAdd(query as CFDictionary, nil)
|
||||||
|
guard createStatus == errSecSuccess else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
status = withUnsafeMutablePointer(to: &dataTypeRef) {
|
||||||
|
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard status == errSecSuccess else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func read<T>(_ key: String, _ type: T.Type) -> T? where T : Codable {
|
func read<T>(_ key: String, _ type: T.Type) -> T? where T : Codable {
|
||||||
guard let data = read(key) else {
|
guard let data = read(key) else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -16,7 +47,7 @@ final class KeychainHelper {
|
||||||
let item = try JSONDecoder().decode(type, from: data)
|
let item = try JSONDecoder().decode(type, from: data)
|
||||||
return item
|
return item
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure("Fail to decode item for keychain: \(error)")
|
Log.e("Fail to decode item for keychain: \(error)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +59,7 @@ final class KeychainHelper {
|
||||||
save(data, key)
|
save(data, key)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure("Fail to encode item for keychain: \(error)")
|
Log.e("Fail to encode item for keychain: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +88,7 @@ final class KeychainHelper {
|
||||||
kSecClass: kSecClassGenericPassword,
|
kSecClass: kSecClassGenericPassword,
|
||||||
kSecAttrService: genericService,
|
kSecAttrService: genericService,
|
||||||
kSecAttrAccount: key,
|
kSecAttrAccount: key,
|
||||||
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
|
kSecAttrAccessible: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
|
||||||
] as CFDictionary
|
] as CFDictionary
|
||||||
|
|
||||||
let status = SecItemAdd(query, nil)
|
let status = SecItemAdd(query, nil)
|
||||||
|
|
|
@ -7,3 +7,4 @@
|
||||||
"SetUpBitwardenToViewItemsContainingVerificationCodes"="Set up Bitwarden to view items containing verification codes";
|
"SetUpBitwardenToViewItemsContainingVerificationCodes"="Set up Bitwarden to view items containing verification codes";
|
||||||
"Search"="Search";
|
"Search"="Search";
|
||||||
"NoItemsFound"="No items found";
|
"NoItemsFound"="No items found";
|
||||||
|
"SetUpAppleWatchPasscodeInOrderToUseBitwarden"="Set up Apple Watch passcode in order to use Bitwarden";
|
||||||
|
|
|
@ -6,6 +6,7 @@ class StateService {
|
||||||
let HAS_RUN_BEFORE_KEY = "has_run_before_key"
|
let HAS_RUN_BEFORE_KEY = "has_run_before_key"
|
||||||
let CURRENT_STATE_KEY = "current_state_key"
|
let CURRENT_STATE_KEY = "current_state_key"
|
||||||
let CURRENT_USER_KEY = "current_user_key"
|
let CURRENT_USER_KEY = "current_user_key"
|
||||||
|
let LACKED_DEVICE_OWNER_AUTH_LAST_TIME_KEY = "lacked_device_owner_auth_last_time_key"
|
||||||
// let TIMEOUT_MINUTES_KEY = "timeout_minutes_key"
|
// let TIMEOUT_MINUTES_KEY = "timeout_minutes_key"
|
||||||
// let TIMEOUT_ACTION_KEY = "timeout_action_key"
|
// let TIMEOUT_ACTION_KEY = "timeout_action_key"
|
||||||
|
|
||||||
|
@ -66,6 +67,15 @@ class StateService {
|
||||||
setUser(user: nil)
|
setUser(user: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lackedDeviceOwnerAuthLastTime : Bool {
|
||||||
|
get {
|
||||||
|
return UserDefaults.standard.bool(forKey: LACKED_DEVICE_OWNER_AUTH_LAST_TIME_KEY)
|
||||||
|
}
|
||||||
|
set(newValue) {
|
||||||
|
UserDefaults.standard.set(newValue, forKey: LACKED_DEVICE_OWNER_AUTH_LAST_TIME_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// var vaultTimeoutInMinutes: Int? {
|
// var vaultTimeoutInMinutes: Int? {
|
||||||
// guard let timeoutData = KeychainHelper.standard.read(TIMEOUT_MINUTES_KEY),
|
// guard let timeoutData = KeychainHelper.standard.read(TIMEOUT_MINUTES_KEY),
|
||||||
// let strData = String(data: timeoutData, encoding: .utf8),
|
// let strData = String(data: timeoutData, encoding: .utf8),
|
||||||
|
|
|
@ -8,6 +8,7 @@ enum BWState : Int, Codable {
|
||||||
case need2FAItem = 4
|
case need2FAItem = 4
|
||||||
case syncing = 5
|
case syncing = 5
|
||||||
// case needUnlock = 6
|
// case needUnlock = 6
|
||||||
|
case needDeviceOwnerAuth = 7
|
||||||
|
|
||||||
var isDestructive: Bool {
|
var isDestructive: Bool {
|
||||||
return self == .needSetup || self == .needLogin || self == .needPremium || self == .need2FAItem
|
return self == .needSetup || self == .needLogin || self == .needPremium || self == .need2FAItem
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// This is basic Queue implementation that uses an array internally.
|
||||||
|
/// For the current use is enough, If intended to use with many items please improve it
|
||||||
|
/// following https://nitinagam17.medium.com/data-structure-in-swift-queue-part-5-985601071606 Linked list or Two stacks approach
|
||||||
|
struct ArrayQueue<T> : CustomStringConvertible {
|
||||||
|
private var elements: [T] = []
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
var isEmpty: Bool {
|
||||||
|
elements.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var peek: T? {
|
||||||
|
elements.first
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
if isEmpty {
|
||||||
|
return "Queue empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "---- Queue -----\n"
|
||||||
|
+ elements.map({"\($0)"}).joined(separator: " -> ")
|
||||||
|
+ "---- Queue end -----"
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func enqueue(_ value: T) {
|
||||||
|
elements.append(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func dequeue() -> T? {
|
||||||
|
isEmpty ? nil : elements.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,8 @@ class BWStateViewModel : ObservableObject{
|
||||||
isLoading = true
|
isLoading = true
|
||||||
case .need2FAItem:
|
case .need2FAItem:
|
||||||
text = "Add2FactorAutenticationToAnItemToViewVerificationCodes"
|
text = "Add2FactorAutenticationToAnItemToViewVerificationCodes"
|
||||||
|
case .needDeviceOwnerAuth:
|
||||||
|
text = "SetUpAppleWatchPasscodeInOrderToUseBitwarden"
|
||||||
default:
|
default:
|
||||||
text = ""
|
text = ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,10 @@ class CipherListViewModel : ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkStateAndFetch(_ state: BWState? = nil) {
|
func checkStateAndFetch(_ state: BWState? = nil) {
|
||||||
|
guard checkDeviceOwnerAuth() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
StateService.shared.checkIntegrity()
|
StateService.shared.checkIntegrity()
|
||||||
|
|
||||||
user = StateService.shared.getUser()
|
user = StateService.shared.getUser()
|
||||||
|
@ -88,4 +92,20 @@ class CipherListViewModel : ObservableObject {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkDeviceOwnerAuth() -> Bool {
|
||||||
|
guard KeychainHelper.standard.hasDeviceOwnerAuth() else {
|
||||||
|
currentState = .needDeviceOwnerAuth
|
||||||
|
showingSheet = true
|
||||||
|
StateService.shared.lackedDeviceOwnerAuthLastTime = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if StateService.shared.lackedDeviceOwnerAuthLastTime {
|
||||||
|
watchConnectivityManager.triggerSync()
|
||||||
|
StateService.shared.lackedDeviceOwnerAuthLastTime = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,11 @@ final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
let watchConnectivitySubject = CurrentValueSubject<WatchConnectivityMessage, Error>(WatchConnectivityMessage(state: nil))
|
let watchConnectivitySubject = CurrentValueSubject<WatchConnectivityMessage, Error>(WatchConnectivityMessage(state: nil))
|
||||||
|
|
||||||
private let kMessageKey = "message"
|
private let WATCH_DTO_APP_CONTEXT_KEY = "watchDto"
|
||||||
private let kCipherDataKey = "watchDto"
|
private let TRIGGER_SYNC_ACTION_KEY = "triggerSync"
|
||||||
|
private let ACTION_MESSAGE_KEY = "actionMessage"
|
||||||
|
|
||||||
|
var messageQueue = ArrayQueue<[String : Any]>()
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
@ -27,8 +30,13 @@ final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||||
return WCSession.default.isCompanionAppInstalled && WCSession.default.activationState == .activated
|
return WCSession.default.isCompanionAppInstalled && WCSession.default.activationState == .activated
|
||||||
}
|
}
|
||||||
|
|
||||||
func send(_ message: String) {
|
func triggerSync() {
|
||||||
|
send([ACTION_MESSAGE_KEY : TRIGGER_SYNC_ACTION_KEY])
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(_ message: [String : Any]) {
|
||||||
guard WCSession.default.activationState == .activated else {
|
guard WCSession.default.activationState == .activated else {
|
||||||
|
messageQueue.enqueue(message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +44,7 @@ final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
WCSession.default.sendMessage([kMessageKey : message], replyHandler: nil) { error in
|
WCSession.default.sendMessage(message) { error in
|
||||||
Log.e("Cannot send message: \(String(describing: error))")
|
Log.e("Cannot send message: \(String(describing: error))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,19 +60,30 @@ extension WatchConnectivityManager: WCSessionDelegate {
|
||||||
func session(_ session: WCSession,
|
func session(_ session: WCSession,
|
||||||
activationDidCompleteWith activationState: WCSessionActivationState,
|
activationDidCompleteWith activationState: WCSessionActivationState,
|
||||||
error: Error?) {
|
error: Error?) {
|
||||||
|
guard !messageQueue.isEmpty, activationState == .activated else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
repeat {
|
||||||
|
send(messageQueue.dequeue()!)
|
||||||
|
} while !messageQueue.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
|
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
|
||||||
// in order for the delivery to be faster the time is added to the key to make each application context update have a different key
|
// in order for the delivery to be faster the time is added to the key to make each application context update have a different key
|
||||||
// and update faster
|
// and update faster
|
||||||
let watchDtoKey = applicationContext.keys.first { k in
|
let watchDtoKey = applicationContext.keys.first { k in
|
||||||
k.starts(with: kCipherDataKey)
|
k.starts(with: WATCH_DTO_APP_CONTEXT_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let dtoKey = watchDtoKey, let serializedDto = applicationContext[dtoKey] as? String else {
|
guard let dtoKey = watchDtoKey, let serializedDto = applicationContext[dtoKey] as? String else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard KeychainHelper.standard.hasDeviceOwnerAuth() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
guard let json = try! JSONSerialization.jsonObject(with: serializedDto.data(using: .utf8)!, options: [.fragmentsAllowed]) as? String else {
|
guard let json = try! JSONSerialization.jsonObject(with: serializedDto.data(using: .utf8)!, options: [.fragmentsAllowed]) as? String else {
|
||||||
return
|
return
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
1B15615B28B7F3D900610B9B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15615A28B7F3D900610B9B /* Preview Assets.xcassets */; };
|
1B15615B28B7F3D900610B9B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15615A28B7F3D900610B9B /* Preview Assets.xcassets */; };
|
||||||
1B15616C28B81A2200610B9B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15616B28B81A2200610B9B /* Cipher.swift */; };
|
1B15616C28B81A2200610B9B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15616B28B81A2200610B9B /* Cipher.swift */; };
|
||||||
1B15616E28B81A4300610B9B /* WatchConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */; };
|
1B15616E28B81A4300610B9B /* WatchConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */; };
|
||||||
|
1B5849A7294D1C020055286B /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5849A6294D1C020055286B /* Queue.swift */; };
|
||||||
1B59EC5729007DEE00A8718D /* BitwardenDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC5529007DEE00A8718D /* BitwardenDB.xcdatamodeld */; };
|
1B59EC5729007DEE00A8718D /* BitwardenDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC5529007DEE00A8718D /* BitwardenDB.xcdatamodeld */; };
|
||||||
1B59EC592900801500A8718D /* StringEncryptionTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */; };
|
1B59EC592900801500A8718D /* StringEncryptionTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */; };
|
||||||
1B59EC5C2900BB3400A8718D /* CryptoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC5B2900BB3400A8718D /* CryptoService.swift */; };
|
1B59EC5C2900BB3400A8718D /* CryptoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC5B2900BB3400A8718D /* CryptoService.swift */; };
|
||||||
|
@ -134,6 +135,8 @@
|
||||||
1B15615D28B7F3D900610B9B /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = "<group>"; };
|
1B15615D28B7F3D900610B9B /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = "<group>"; };
|
||||||
1B15616B28B81A2200610B9B /* Cipher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cipher.swift; sourceTree = "<group>"; };
|
1B15616B28B81A2200610B9B /* Cipher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cipher.swift; sourceTree = "<group>"; };
|
||||||
1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnectivityManager.swift; sourceTree = "<group>"; };
|
1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnectivityManager.swift; sourceTree = "<group>"; };
|
||||||
|
1B5849A6294D1C020055286B /* Queue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = "<group>"; };
|
||||||
|
1B5849A92950BC860055286B /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS9.1.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
1B59EC5629007DEE00A8718D /* BitwardenDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = BitwardenDB.xcdatamodel; sourceTree = "<group>"; };
|
1B59EC5629007DEE00A8718D /* BitwardenDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = BitwardenDB.xcdatamodel; sourceTree = "<group>"; };
|
||||||
1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringEncryptionTransformer.swift; sourceTree = "<group>"; };
|
1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringEncryptionTransformer.swift; sourceTree = "<group>"; };
|
||||||
1B59EC5B2900BB3400A8718D /* CryptoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoService.swift; sourceTree = "<group>"; };
|
1B59EC5B2900BB3400A8718D /* CryptoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoService.swift; sourceTree = "<group>"; };
|
||||||
|
@ -219,6 +222,7 @@
|
||||||
1BD291BE292D0E6F0004F33F /* JsonDecoderExtensions.swift */,
|
1BD291BE292D0E6F0004F33F /* JsonDecoderExtensions.swift */,
|
||||||
1BD291C0292E7E690004F33F /* ErrorExtensions.swift */,
|
1BD291C0292E7E690004F33F /* ErrorExtensions.swift */,
|
||||||
1B5F5E39293F9D6F009B5FCC /* ViewExtensions.swift */,
|
1B5F5E39293F9D6F009B5FCC /* ViewExtensions.swift */,
|
||||||
|
1B5849A6294D1C020055286B /* Queue.swift */,
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -230,6 +234,7 @@
|
||||||
1B15614128B7F3D700610B9B /* bitwarden WatchKit App */,
|
1B15614128B7F3D700610B9B /* bitwarden WatchKit App */,
|
||||||
1B15614C28B7F3D800610B9B /* bitwarden WatchKit Extension */,
|
1B15614C28B7F3D800610B9B /* bitwarden WatchKit Extension */,
|
||||||
1B15613028B7F3D400610B9B /* Products */,
|
1B15613028B7F3D400610B9B /* Products */,
|
||||||
|
1B5849A82950BC860055286B /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
@ -305,6 +310,14 @@
|
||||||
path = "Preview Content";
|
path = "Preview Content";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
1B5849A82950BC860055286B /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1B5849A92950BC860055286B /* LocalAuthentication.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
1B59EC5429007D7300A8718D /* Entities */ = {
|
1B59EC5429007D7300A8718D /* Entities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -583,6 +596,7 @@
|
||||||
1BD291C1292E7E690004F33F /* ErrorExtensions.swift in Sources */,
|
1BD291C1292E7E690004F33F /* ErrorExtensions.swift in Sources */,
|
||||||
1B14DF37291186D900EA43F1 /* EmptyStateViewModifier.swift in Sources */,
|
1B14DF37291186D900EA43F1 /* EmptyStateViewModifier.swift in Sources */,
|
||||||
1BD291B52924047C0004F33F /* BWStateViewModel.swift in Sources */,
|
1BD291B52924047C0004F33F /* BWStateViewModel.swift in Sources */,
|
||||||
|
1B5849A7294D1C020055286B /* Queue.swift in Sources */,
|
||||||
1B15616E28B81A4300610B9B /* WatchConnectivityManager.swift in Sources */,
|
1B15616E28B81A4300610B9B /* WatchConnectivityManager.swift in Sources */,
|
||||||
1B5AFF0329196C81004478F9 /* ColorUtils.swift in Sources */,
|
1B5AFF0329196C81004478F9 /* ColorUtils.swift in Sources */,
|
||||||
1B59EC612900C48E00A8718D /* KeychainHelper.swift in Sources */,
|
1B59EC612900C48E00A8718D /* KeychainHelper.swift in Sources */,
|
||||||
|
|
|
@ -10,8 +10,9 @@ final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||||
static let shared = WatchConnectivityManager()
|
static let shared = WatchConnectivityManager()
|
||||||
@Published var notificationMessage: NotificationMessage? = nil
|
@Published var notificationMessage: NotificationMessage? = nil
|
||||||
|
|
||||||
private let kMessageKey = "message"
|
private let WATCH_DTO_APP_CONTEXT_KEY = "watchDto"
|
||||||
private let kCipherDataKey = "cipherData"
|
private let TRIGGER_SYNC_ACTION_KEY = "triggerSync"
|
||||||
|
private let ACTION_MESSAGE_KEY = "actionMessage"
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
@ -40,7 +41,7 @@ final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
WCSession.default.sendMessage([kMessageKey : message], replyHandler: nil) { error in
|
WCSession.default.sendMessage([ACTION_MESSAGE_KEY : TRIGGER_SYNC_ACTION_KEY], replyHandler: nil) { error in
|
||||||
print("Cannot send message: \(String(describing: error))")
|
print("Cannot send message: \(String(describing: error))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +53,7 @@ extension WatchConnectivityManager: WCSessionDelegate {
|
||||||
self?.notificationMessage = NotificationMessage(text: "testing this didReceiveMessage")
|
self?.notificationMessage = NotificationMessage(text: "testing this didReceiveMessage")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let notificationText = message[kMessageKey] as? String {
|
if let notificationText = message[ACTION_MESSAGE_KEY] as? String {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.notificationMessage = NotificationMessage(text: notificationText)
|
self?.notificationMessage = NotificationMessage(text: notificationText)
|
||||||
}
|
}
|
||||||
|
@ -96,7 +97,7 @@ extension WatchConnectivityManager: WCSessionDelegate {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.notificationMessage = NotificationMessage(text: "testing this didReceiveApplicationContext")
|
self?.notificationMessage = NotificationMessage(text: "testing this didReceiveApplicationContext")
|
||||||
}
|
}
|
||||||
if let notificationText = applicationContext[kCipherDataKey] as? String {
|
if let notificationText = applicationContext[WATCH_DTO_APP_CONTEXT_KEY] as? String {
|
||||||
|
|
||||||
// let decoder = JSONDecoder()
|
// let decoder = JSONDecoder()
|
||||||
// do {
|
// do {
|
||||||
|
|
Loading…
Reference in a new issue