Skip to content
Open
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
45 changes: 16 additions & 29 deletions Modules/IdCardLib/Sources/IdCardLib/Utilities/SecureData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,30 @@
*/

import Foundation
import Darwin // for memset_s
import Synchronization
import Darwin // memset_s

/// Holds sensitive bytes and reliably zeroes them on deinit.
public final class SecureData: @unchecked Sendable {
private var storage: Data
/// Holds sensitive bytes and reliably zeroes them on `secureZero()` / `deinit`.
public final class SecureData: Sendable {
private let storage: Mutex<Data>

public init(_ bytes: [UInt8]) {
self.storage = Data(bytes)
}

public init(_ data: Data) {
self.storage = data
}
public init(_ bytes: [UInt8]) { storage = Mutex(Data(bytes)) }
public init(_ data: Data) { storage = Mutex(data) }

deinit { secureZero() }

/// Mutating read-only access to the underlying bytes.
func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
try storage.withUnsafeBytes(body)
/// Scoped read-only access to the underlying bytes
func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) -> R) -> R {
let snapshot = storage.withLock { $0 }
return snapshot.withUnsafeBytes(body)
}

/// Mutating access when you need to write into the buffer.
func withUnsafeMutableBytes<R>(_ body: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R {
try storage.withUnsafeMutableBytes(body)
}

public var count: Int { storage.count }

/// Explicitly wipe now (also runs on deinit).
/// Explicitly wipe now (also runs on `deinit`)
public func secureZero() {
guard storage.count > 0 else { return }
storage.withUnsafeMutableBytes { buf in
_ = memset_s(buf.baseAddress, buf.count, 0, buf.count)
storage.withLock { data in
guard !data.isEmpty else { return }
data.withUnsafeMutableBytes { _ = memset_s($0.baseAddress, $0.count, 0, $0.count) }
data.removeAll(keepingCapacity: false)
}
storage.removeAll(keepingCapacity: false)
}

/// If you need a temporary `Data` view (try to avoid).
func asData() -> Data { storage }
}
100 changes: 100 additions & 0 deletions Modules/IdCardLib/Tests/IdCardLibTests/Utilities/SecureDataTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2017 - 2026 Riigi Infosüsteemi Amet
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/

import Foundation
import Testing

@testable import IdCardLib

struct SecureDataTests {

private func bytes(of secureData: SecureData) -> [UInt8] {
secureData.withUnsafeBytes { Array($0) }
}

@Test
func init_fromByteArray_exposesSameBytes() {
let input: [UInt8] = [1, 2, 3, 4, 5, 6]
let secureData = SecureData(input)

#expect(bytes(of: secureData) == input)
}

@Test
func init_fromData_exposesSameBytes() {
let input = Data([0xAA, 0xBB, 0xCC])
let secureData = SecureData(input)

#expect(bytes(of: secureData) == Array(input))
}

@Test
func init_empty_exposesNoBytes() {
let secureData = SecureData([])

#expect(bytes(of: secureData).isEmpty)
}

@Test
func init_preservesNullAndAllByteValues() {
let input: [UInt8] = [0x00, 0x30, 0xFF, 0x00, 0x01]
let secureData = SecureData(input)

#expect(bytes(of: secureData) == input)
}

@Test
func secureZero_clearsTheBuffer() {
let secureData = SecureData([1, 2, 3, 4])

secureData.secureZero()

#expect(bytes(of: secureData).isEmpty)
}

@Test
func secureZero_staysEmptyWhenCalledTwice() {
let secureData = SecureData([9, 9, 9])

secureData.secureZero()
secureData.secureZero()

#expect(bytes(of: secureData).isEmpty)
}

@Test
func secureZero_staysEmptyWhenAlreadyEmpty() {
let secureData = SecureData([])

secureData.secureZero()

#expect(bytes(of: secureData).isEmpty)
}

@Test
func withUnsafeBytes_returnsValueFromClosure() {
let secureData = SecureData([2, 4, 6, 8])

let sum = secureData.withUnsafeBytes { raw in
raw.reduce(0) { $0 + Int($1) }
}

#expect(sum == 20)
}
}
5 changes: 5 additions & 0 deletions RIADigiDoc/Domain/NFC/OperationChangePin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ public class OperationChangePin: NFCOperationBase, OperationChangePinProtocol {
}

do {
defer {
currentPin.secureZero()
newPin.secureZero()
}

updateAlertMessage(step: 1)
OperationChangePin.logger().info("NFC: Setting up NFC connection for PIN change...")
let tag = try await self.connection.setup(session, tags: tags)
Expand Down
2 changes: 2 additions & 0 deletions RIADigiDoc/Domain/NFC/OperationDecrypt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ public class OperationDecrypt: NFCOperationBase, OperationDecryptProtocol {
OperationDecrypt.logger().info("NFC: Checks complete starting decryption")

do {
defer { pin1Number.secureZero() }

updateAlertMessage(step: 1)
let tag = try await connection.setup(session, tags: tags)
updateAlertMessage(step: 2)
Expand Down
2 changes: 2 additions & 0 deletions RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ public class OperationReadCertAndSign: NFCOperationBase, OperationReadCertAndSig
OperationReadCertAndSign.logger().info("NFC: Checks complete starting signing")

do {
defer { pin2Number.secureZero() }

updateAlertMessage(step: 1)
let tag = try await connection.setup(session, tags: tags)

Expand Down
5 changes: 5 additions & 0 deletions RIADigiDoc/Domain/NFC/OperationUnblockPin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public class OperationUnblockPin: NFCOperationBase, OperationUnblockPinProtocol
return
}
do {
defer {
puk.secureZero()
newPin.secureZero()
}

updateAlertMessage(step: 1)
let tag = try await self.connection.setup(session, tags: tags)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ struct IdCardView: View {
@Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled
@Environment(\.openURL) private var openURL
@Environment(\.dismiss) private var dismiss
@Environment(\.scenePhase) private var scenePhase
@Environment(LanguageSettings.self) private var languageSettings
@Environment(NavigationPathManager.self) private var pathManager

Expand Down Expand Up @@ -395,6 +396,11 @@ struct IdCardView: View {
pinNumber.removeAll()
cancelIdCardAction()
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
cancelIdCardAction()
}
}
}

private func handleCardError() async {
Expand Down
20 changes: 14 additions & 6 deletions RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ struct NFCView: View {
@Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled
@Environment(\.dismiss) private var dismiss
@Environment(\.openURL) private var openURL
@Environment(\.scenePhase) private var scenePhase
@Environment(LanguageSettings.self) private var languageSettings
@Environment(NavigationPathManager.self) private var pathManager

Expand Down Expand Up @@ -267,6 +268,17 @@ struct NFCView: View {
.onDisappear {
cancelSigning()
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
resetPinState()
}
}
}

private func resetPinState() {
pinNumber.removeAll()
isActionEnabled = viewModel
.isActionEnabled(canNumber: canNumber, pinNumber: pinNumber, pinType: pinType)
}

func saveInputData() {
Expand Down Expand Up @@ -317,17 +329,13 @@ struct NFCView: View {
}

private func cancelDecrypt() {
pinNumber.isEmpty ? () : (pinNumber.removeAll())
isActionEnabled = viewModel
.isActionEnabled(canNumber: canNumber, pinNumber: pinNumber, pinType: pinType)
resetPinState()
taskDecrypt?.cancel()
taskDecrypt = nil
}

private func cancelSigning() {
pinNumber.isEmpty ? () : (pinNumber.removeAll())
isActionEnabled = viewModel
.isActionEnabled(canNumber: canNumber, pinNumber: pinNumber, pinType: pinType)
resetPinState()
taskSign?.cancel()
taskSign = nil
}
Expand Down
6 changes: 6 additions & 0 deletions RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import CommonsLib
struct MyEidPinChangeView: View {
@Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled
@Environment(\.dismiss) private var dismiss
@Environment(\.scenePhase) private var scenePhase
@Environment(LanguageSettings.self) private var languageSettings

@AccessibilityFocusState private var flowTitleFocused: Bool
Expand Down Expand Up @@ -295,6 +296,11 @@ struct MyEidPinChangeView: View {
stepTitleFocused = true
}
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
viewModel.clearSensitiveDataOnBackground()
}
}
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ struct FloatingLabelTextField: View {
onDone()
}
)
.privacySensitive(isSecure)
.toolbar { keyboardToolbar }
.accessibilitySortPriority(sortPriority)
.accessibilityLabel(Text(verbatim: title))
Expand Down
5 changes: 5 additions & 0 deletions RIADigiDoc/ViewModel/MyEid/MyEidPinChangeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable {
isSuccess = false
}

func clearSensitiveDataOnBackground() {
clearPinCodes()
resetErrors()
}

func handleConfirmStepError() {
if step == .confirm && !verifyRepeatedCode() && !isSuccess {
inputErrorMessage = "PIN repeat error"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ protocol MyEidPinChangeViewModelProtocol: Sendable {

func submit(nfcStringsUtil: NFCSessionStringsUtil) async
func resetErrors()
func clearSensitiveDataOnBackground()

func verifyNewCode()
func verifyRepeatedCode() -> Bool
Expand Down
Loading