Skip to content

Commit d5ce515

Browse files
committed
Add remote commands via APNS for Loop users
1 parent 1ce2edf commit d5ce515

25 files changed

+2912
-462
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@ fastlane/test_output
8080
fastlane/FastlaneRunner
8181

8282
LoopFollowConfigOverride.xcconfig
83+
.history

Config.xcconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
unique_id = ${DEVELOPMENT_TEAM}
77

88
//Version (DEFAULT)
9-
LOOP_FOLLOW_MARKETING_VERSION = 2.8.10
9+
LOOP_FOLLOW_MARKETING_VERSION = 2.8.11

LoopFollow.xcodeproj/project.pbxproj

Lines changed: 66 additions & 33 deletions
Large diffs are not rendered by default.

LoopFollow.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LoopFollow/Helpers/NightscoutUtils.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,21 @@
22
// NightscoutUtils.swift
33
// Created by bjorkert.
44

5+
import CommonCrypto
56
import Foundation
67

8+
extension String {
9+
var sha1: String {
10+
let data = Data(utf8)
11+
var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
12+
data.withUnsafeBytes {
13+
_ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
14+
}
15+
let hexBytes = digest.map { String(format: "%02hhx", $0) }
16+
return hexBytes.joined()
17+
}
18+
}
19+
720
class NightscoutUtils {
821
enum NightscoutError: Error, LocalizedError {
922
case emptyAddress
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// LoopFollow
2+
// TOTPGenerator.swift
3+
// Created by codebymini.
4+
5+
import CommonCrypto
6+
import Foundation
7+
8+
enum TOTPGenerator {
9+
/// Generates a TOTP code from a base32 secret
10+
/// - Parameter secret: The base32 encoded secret
11+
/// - Returns: A 6-digit TOTP code as a string
12+
static func generateTOTP(secret: String) -> String {
13+
// Decode base32 secret
14+
let decodedSecret = base32Decode(secret)
15+
16+
// Get current time in 30-second intervals
17+
let timeInterval = Int(Date().timeIntervalSince1970)
18+
let timeStep = 30
19+
let counter = timeInterval / timeStep
20+
21+
// Convert counter to 8-byte big-endian data
22+
var counterData = Data()
23+
for i in 0 ..< 8 {
24+
counterData.append(UInt8((counter >> (56 - i * 8)) & 0xFF))
25+
}
26+
27+
// Generate HMAC-SHA1
28+
let key = Data(decodedSecret)
29+
let hmac = generateHMACSHA1(key: key, data: counterData)
30+
31+
// Get the last 4 bits of the HMAC
32+
let offset = Int(hmac.withUnsafeBytes { $0.last! } & 0x0F)
33+
34+
// Extract 4 bytes starting at the offset
35+
let hmacData = Data(hmac)
36+
let codeBytes = hmacData.subdata(in: offset ..< (offset + 4))
37+
38+
// Convert to integer and get last 6 digits
39+
let code = codeBytes.withUnsafeBytes { bytes in
40+
let value = bytes.load(as: UInt32.self).bigEndian
41+
return Int(value & 0x7FFF_FFFF) % 1_000_000
42+
}
43+
44+
return String(format: "%06d", code)
45+
}
46+
47+
/// Extracts OTP from various URL formats
48+
/// - Parameter urlString: The URL string to parse
49+
/// - Returns: The OTP code as a string, or nil if not found
50+
static func extractOTPFromURL(_ urlString: String) -> String? {
51+
guard let url = URL(string: urlString),
52+
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
53+
else {
54+
return nil
55+
}
56+
57+
// Check for TOTP format (otpauth://)
58+
if url.scheme == "otpauth" {
59+
if let secretItem = components.queryItems?.first(where: { $0.name == "secret" }),
60+
let secret = secretItem.value
61+
{
62+
return generateTOTP(secret: secret)
63+
}
64+
}
65+
66+
// Check for regular OTP format
67+
if let otpItem = components.queryItems?.first(where: { $0.name == "otp" }) {
68+
return otpItem.value
69+
}
70+
71+
return nil
72+
}
73+
74+
/// Decodes a base32 string to bytes
75+
/// - Parameter string: The base32 encoded string
76+
/// - Returns: Array of decoded bytes
77+
private static func base32Decode(_ string: String) -> [UInt8] {
78+
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
79+
var result: [UInt8] = []
80+
var buffer = 0
81+
var bitsLeft = 0
82+
83+
for char in string.uppercased() {
84+
guard let index = alphabet.firstIndex(of: char) else { continue }
85+
let value = alphabet.distance(from: alphabet.startIndex, to: index)
86+
87+
buffer = (buffer << 5) | value
88+
bitsLeft += 5
89+
90+
while bitsLeft >= 8 {
91+
bitsLeft -= 8
92+
result.append(UInt8((buffer >> bitsLeft) & 0xFF))
93+
}
94+
}
95+
96+
return result
97+
}
98+
99+
/// Generates HMAC-SHA1 for the given key and data
100+
/// - Parameters:
101+
/// - key: The key to use for HMAC
102+
/// - data: The data to hash
103+
/// - Returns: The HMAC-SHA1 result as Data
104+
private static func generateHMACSHA1(key: Data, data: Data) -> Data {
105+
var hmac = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
106+
key.withUnsafeBytes { keyBytes in
107+
data.withUnsafeBytes { dataBytes in
108+
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), keyBytes.baseAddress, key.count, dataBytes.baseAddress, data.count, &hmac)
109+
}
110+
}
111+
return Data(hmac)
112+
}
113+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// LoopFollow
2+
// SimpleQRCodeScannerView.swift
3+
// Created by codebymini.
4+
5+
import AVFoundation
6+
import SwiftUI
7+
8+
struct SimpleQRCodeScannerView: UIViewControllerRepresentable {
9+
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
10+
var parent: SimpleQRCodeScannerView
11+
12+
init(parent: SimpleQRCodeScannerView) {
13+
self.parent = parent
14+
}
15+
16+
func metadataOutput(_: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from _: AVCaptureConnection) {
17+
if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
18+
metadataObject.type == .qr,
19+
let stringValue = metadataObject.stringValue
20+
{
21+
parent.completion(.success(stringValue))
22+
parent.presentationMode.wrappedValue.dismiss()
23+
}
24+
}
25+
}
26+
27+
@Environment(\.presentationMode) var presentationMode
28+
var completion: (Result<String, Error>) -> Void
29+
30+
func makeCoordinator() -> Coordinator {
31+
Coordinator(parent: self)
32+
}
33+
34+
func makeUIViewController(context: Context) -> UIViewController {
35+
let controller = UIViewController()
36+
let session = AVCaptureSession()
37+
38+
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return controller }
39+
guard let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else { return controller }
40+
if session.canAddInput(videoInput) {
41+
session.addInput(videoInput)
42+
}
43+
44+
let metadataOutput = AVCaptureMetadataOutput()
45+
if session.canAddOutput(metadataOutput) {
46+
session.addOutput(metadataOutput)
47+
metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
48+
metadataOutput.metadataObjectTypes = [.qr]
49+
}
50+
51+
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
52+
previewLayer.frame = controller.view.layer.bounds
53+
previewLayer.videoGravity = .resizeAspectFill
54+
controller.view.layer.addSublayer(previewLayer)
55+
56+
session.startRunning()
57+
return controller
58+
}
59+
60+
func updateUIViewController(_: UIViewController, context _: Context) {}
61+
}

LoopFollow/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
<string>This app requires access to contacts to update a contact image with real-time blood glucose information.</string>
5858
<key>NSFaceIDUsageDescription</key>
5959
<string>This app requires Face ID for secure authentication.</string>
60+
<key>NSCameraUsageDescription</key>
61+
<string>Used for scanning QR codes for remote authentication</string>
6062
<key>NSHumanReadableCopyright</key>
6163
<string></string>
6264
<key>UIApplicationSceneManifest</key>

LoopFollow/Nightscout/NightscoutSettingsViewModel.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,12 @@ class NightscoutSettingsViewModel: ObservableObject {
9696
}
9797

9898
func checkNightscoutStatus() {
99-
NightscoutUtils.verifyURLAndToken { error, _, nsWriteAuth, nsAdminAuth in
99+
NightscoutUtils.verifyURLAndToken { [weak self] error, _, nsWriteAuth, nsAdminAuth in
100100
DispatchQueue.main.async {
101101
Storage.shared.nsWriteAuth.value = nsWriteAuth
102102
Storage.shared.nsAdminAuth.value = nsAdminAuth
103103

104-
self.updateStatusLabel(error: error)
104+
self?.updateStatusLabel(error: error)
105105
}
106106
}
107107
}

LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)