diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..b2f95bd
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @eu-digital-identity-wallet/niscy-admins
\ No newline at end of file
diff --git a/.github/workflows/dependencycheck.yml b/.github/workflows/dependencycheck.yml
new file mode 100644
index 0000000..1861cb8
--- /dev/null
+++ b/.github/workflows/dependencycheck.yml
@@ -0,0 +1,15 @@
+name: SCA - Dependency-Check Caller
+on:
+ push:
+ branches-ignore:
+ - 'dependabot/*'
+ workflow_dispatch:
+
+jobs:
+
+ SCA_caller:
+ uses: eu-digital-identity-wallet/eudi-infra-ci/.github/workflows/sca.yml@main
+ secrets:
+ NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
+ DOJO_TOKEN: ${{ secrets.DOJO_TOKEN }}
+ DOJO_URL: ${{ secrets.DOJO_URL }}
\ No newline at end of file
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 8fa091f..fa7644b 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -3,17 +3,23 @@
name: Documentation
-on: [push]
+on:
+ push:
+ branches:
+ - main
jobs:
build:
- runs-on: macos-latest
+ runs-on: macos-latest-xlarge
steps:
+ - uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: latest
- uses: swift-actions/setup-swift@v1
- name: Get swift version
- run: swift --version # Swift 5.8
+ run: swift --version
- uses: actions/checkout@v3
- name: Fix Up Private GitHub URLs
# Add personal access token to all private repo URLs
diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml
new file mode 100644
index 0000000..a32a740
--- /dev/null
+++ b/.github/workflows/gitleaks.yml
@@ -0,0 +1,14 @@
+name: Secret Scanning - Gitleaks Caller
+on:
+ push:
+ branches-ignore:
+ - 'dependabot/*'
+ workflow_dispatch:
+
+jobs:
+
+ Secret_Scanning_caller:
+ uses: eu-digital-identity-wallet/eudi-infra-ci/.github/workflows/secretscanning.yml@main
+ secrets:
+ DOJO_TOKEN: ${{ secrets.DOJO_TOKEN }}
+ DOJO_URL: ${{ secrets.DOJO_URL }}
\ No newline at end of file
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
new file mode 100644
index 0000000..2c7fbdb
--- /dev/null
+++ b/.github/workflows/sonar.yml
@@ -0,0 +1,17 @@
+name: SAST - SonarCloud Caller
+on:
+ push:
+ branches-ignore:
+ - 'dependabot/*'
+ pull_request_target:
+ workflow_dispatch:
+
+jobs:
+
+ SAST_caller:
+ uses: eu-digital-identity-wallet/eudi-infra-ci/.github/workflows/sast_action.yml@main
+ secrets:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ DOJO_TOKEN: ${{ secrets.DOJO_TOKEN }}
+ DOJO_URL: ${{ secrets.DOJO_URL }}
\ No newline at end of file
diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml
index 355a97b..9af38a5 100644
--- a/.github/workflows/swift.yml
+++ b/.github/workflows/swift.yml
@@ -1,23 +1,24 @@
-# This workflow will build a Swift project
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
+name: Swift Build
-name: Swift
-
-on: [push]
+on:
+ push:
+ branches-ignore:
+ - 'dependabot/*'
+ pull_request_target:
+ workflow_dispatch:
jobs:
build:
-
- runs-on: macos-latest
+ runs-on: macos-latest-xlarge
steps:
- - uses: swift-actions/setup-swift@v1
+ - uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: latest-stable
+ - uses: swift-actions/setup-swift@v2
- name: Get swift version
- run: swift --version # Swift 5.8
- - uses: actions/checkout@v3
- - name: Fix Up Private GitHub URLs
- # Add personal access token to all private repo URLs
- run: find . -type f \( -name 'Package.swift' -o -name 'Package.resolved' \) -exec sed -i '' "s/https:\/\/github.com\/eu-digital-identity-wallet/https:\/\/${{ secrets.USER_NAME }}:${{ secrets.USER_GITHUB_TOKEN }}@github.com\/eu-digital-identity-wallet/g" {} \;
+ run: swift --version
+ - uses: actions/checkout@v4
- name: Build
run: swift build
- name: Run tests
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/EudiWalletKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/EudiWalletKit.xcscheme
new file mode 100644
index 0000000..697c8d0
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/EudiWalletKit.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/EudiWalletKitTest.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/EudiWalletKitTest.xcscheme
new file mode 100644
index 0000000..206486e
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/EudiWalletKitTest.xcscheme
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Documentation/Reference/README.md b/Documentation/Reference/README.md
index 713dfff..7096bc6 100644
--- a/Documentation/Reference/README.md
+++ b/Documentation/Reference/README.md
@@ -2,43 +2,39 @@
## Protocols
-- [DataStorageService](protocols/DataStorageService.md)
- [PresentationService](protocols/PresentationService.md)
## Structs
- [DocElementsViewModel](structs/DocElementsViewModel.md)
- [ElementViewModel](structs/ElementViewModel.md)
+- [WalletError](structs/WalletError.md)
## Classes
- [BlePresentationService](classes/BlePresentationService.md)
-- [DataSampleStorageService](classes/DataSampleStorageService.md)
+- [EudiWallet](classes/EudiWallet.md)
- [FaultPresentationService](classes/FaultPresentationService.md)
-- [KeyChainStorageService](classes/KeyChainStorageService.md)
+- [OpenId4VCIService](classes/OpenId4VCIService.md)
- [OpenId4VpService](classes/OpenId4VpService.md)
- [PresentationSession](classes/PresentationSession.md)
-- [UserWallet](classes/UserWallet.md)
+- [StorageManager](classes/StorageManager.md)
## Enums
- [DataFormat](enums/DataFormat.md)
- [FlowType](enums/FlowType.md)
-- [TransferStatus](enums/TransferStatus.md)
+- [OpenId4VCIError](enums/OpenId4VCIError.md)
+- [StorageType](enums/StorageType.md)
## Extensions
- [Array](extensions/Array.md)
- [BlePresentationService](extensions/BlePresentationService.md)
-- [PresentationSession](extensions/PresentationSession.md)
+- [String](extensions/String.md)
## Typealiases
- [RequestItems](typealiases/RequestItems.md)
-## Methods
-
-- [fluttenItemViewModels(__valid_)](methods/fluttenItemViewModels(__valid_).md)
-- [nsItemsToViewModels(______)](methods/nsItemsToViewModels(______).md)
-
-This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs) on 2023-10-25 22:10:55 +0000
\ No newline at end of file
+This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs) on 2024-05-15 12:20:03 +0000
diff --git a/Documentation/Reference/classes/BlePresentationService.md b/Documentation/Reference/classes/BlePresentationService.md
index 9de49ba..70eb70c 100644
--- a/Documentation/Reference/classes/BlePresentationService.md
+++ b/Documentation/Reference/classes/BlePresentationService.md
@@ -5,101 +5,76 @@
**Contents**
- [Properties](#properties)
- - `bleServerTransfer`
- `status`
- - `continuationQrCode`
- - `continuationRequest`
- - `continuationResponse`
- - `handleSelected`
- - `deviceEngagement`
- - `request`
- `flow`
- [Methods](#methods)
- `init(parameters:)`
- - `generateQRCode()`
+ - `startQrEngagement()`
- `receiveRequest()`
- - `sendResponse(userAccepted:itemsToSend:)`
+ - `sendResponse(userAccepted:itemsToSend:onSuccess:)`
```swift
-class BlePresentationService : PresentationService
+public class BlePresentationService : PresentationService
```
-## Properties
-### `bleServerTransfer`
-
-```swift
-var bleServerTransfer: MdocGattServer
-```
+Implements proximity attestation presentation with QR to BLE data transfer
+Implementation is based on the ISO/IEC 18013-5 specification
+## Properties
### `status`
```swift
-var status: TransferStatus = .initializing
-```
-
-### `continuationQrCode`
-
-```swift
-var continuationQrCode: CheckedContinuation?
+public var status: TransferStatus = .initializing
```
-### `continuationRequest`
-
-```swift
-var continuationRequest: CheckedContinuation<[String: Any], Error>?
-```
-
-### `continuationResponse`
+### `flow`
```swift
-var continuationResponse: CheckedContinuation?
+public var flow: FlowType
```
-### `handleSelected`
+## Methods
+### `init(parameters:)`
```swift
-var handleSelected: ((Bool, RequestItems?) -> Void)?
+public init(parameters: [String: Any]) throws
```
-### `deviceEngagement`
+### `startQrEngagement()`
```swift
-var deviceEngagement: Data?
+public func startQrEngagement() async throws -> String?
```
-### `request`
+Generate device engagement QR code
+The holder app should present the returned code to the verifier
+- Returns: The image data for the QR code
-```swift
-var request: [String: Any]?
-```
-
-### `flow`
+### `receiveRequest()`
```swift
-var flow: FlowType
+public func receiveRequest() async throws -> [String: Any]
```
-## Methods
-### `init(parameters:)`
+ Receive request via BLE
-```swift
-public init(parameters: [String: Any]) throws
-```
+- Returns: The requested items.
-### `generateQRCode()`
+### `sendResponse(userAccepted:itemsToSend:onSuccess:)`
```swift
-public func generateQRCode() async throws -> Data?
+public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)? ) async throws
```
-### `receiveRequest()`
+Send response via BLE
-```swift
-public func receiveRequest() async throws -> [String: Any]
-```
+- Parameters:
+ - userAccepted: True if user accepted to send the response
+ - itemsToSend: The selected items to send organized in document types and namespaces
-### `sendResponse(userAccepted:itemsToSend:)`
+#### Parameters
-```swift
-public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws
-```
+| Name | Description |
+| ---- | ----------- |
+| userAccepted | True if user accepted to send the response |
+| itemsToSend | The selected items to send organized in document types and namespaces |
\ No newline at end of file
diff --git a/Documentation/Reference/classes/DataSampleStorageService.md b/Documentation/Reference/classes/DataSampleStorageService.md
deleted file mode 100644
index ade5803..0000000
--- a/Documentation/Reference/classes/DataSampleStorageService.md
+++ /dev/null
@@ -1,126 +0,0 @@
-**CLASS**
-
-# `DataSampleStorageService`
-
-**Contents**
-
-- [Properties](#properties)
- - `euPidModel`
- - `isoMdlModel`
- - `sampleData`
- - `pidLoaded`
- - `mdlLoaded`
- - `debugDisplay`
- - `logger`
- - `hasData`
- - `defaultId`
-- [Methods](#methods)
- - `init()`
- - `getDoc(i:)`
- - `removeDoc(i:)`
- - `loadSampleData(force:)`
- - `loadDocument(id:)`
- - `saveDocument(id:value:)`
- - `deleteDocument(id:)`
-
-```swift
-public class DataSampleStorageService: ObservableObject, DataStorageService
-```
-
-## Properties
-### `euPidModel`
-
-```swift
-@Published public var euPidModel: EuPidModel?
-```
-
-### `isoMdlModel`
-
-```swift
-@Published public var isoMdlModel: IsoMdlModel?
-```
-
-### `sampleData`
-
-```swift
-var sampleData: Data?
-```
-
-### `pidLoaded`
-
-```swift
-@AppStorage("pidLoaded") public var pidLoaded: Bool = false
-```
-
-### `mdlLoaded`
-
-```swift
-@AppStorage("mdlLoaded") public var mdlLoaded: Bool = false
-```
-
-### `debugDisplay`
-
-```swift
-@AppStorage("DebugDisplay") var debugDisplay: Bool = false
-```
-
-### `logger`
-
-```swift
-let logger: Logger
-```
-
-### `hasData`
-
-```swift
-public var hasData: Bool
-```
-
-### `defaultId`
-
-```swift
-public static var defaultId: String = "EUDI_sample_data"
-```
-
-## Methods
-### `init()`
-
-```swift
-public init()
-```
-
-### `getDoc(i:)`
-
-```swift
-public func getDoc(i: Int) -> MdocDecodable?
-```
-
-### `removeDoc(i:)`
-
-```swift
-public func removeDoc(i: Int)
-```
-
-### `loadSampleData(force:)`
-
-```swift
-public func loadSampleData(force: Bool = false)
-```
-
-### `loadDocument(id:)`
-
-```swift
-public func loadDocument(id: String) throws -> Data
-```
-
-### `saveDocument(id:value:)`
-
-```swift
-public func saveDocument(id: String, value: inout Data) throws
-```
-
-### `deleteDocument(id:)`
-
-```swift
-public func deleteDocument(id: String) throws
-```
diff --git a/Documentation/Reference/classes/EudiWallet.md b/Documentation/Reference/classes/EudiWallet.md
new file mode 100644
index 0000000..39fc44a
--- /dev/null
+++ b/Documentation/Reference/classes/EudiWallet.md
@@ -0,0 +1,284 @@
+**CLASS**
+
+# `EudiWallet`
+
+**Contents**
+
+- [Properties](#properties)
+ - `storage`
+ - `standard`
+ - `userAuthenticationRequired`
+ - `trustedReaderCertificates`
+ - `deviceAuthMethod`
+ - `verifierApiUri`
+ - `openID4VciIssuerUrl`
+ - `openID4VciClientId`
+ - `openID4VciRedirectUri`
+ - `useSecureEnclave`
+- [Methods](#methods)
+ - `init(storageType:serviceName:accessGroup:trustedReaderCertificates:userAuthenticationRequired:verifierApiUri:openID4VciIssuerUrl:openID4VciClientId:openID4VciRedirectUri:)`
+ - `issueDocument(docType:format:)`
+ - `beginIssueDocument(id:privateKeyType:)`
+ - `endIssueDocument(_:)`
+ - `loadDocuments()`
+ - `deleteDocuments()`
+ - `loadSampleData(sampleDataFiles:)`
+ - `prepareServiceDataParameters(docType:dataFormat:)`
+ - `beginPresentation(flow:docType:dataFormat:)`
+ - `beginPresentation(service:)`
+ - `authorizedAction(action:disabled:dismiss:localizedReason:)`
+
+```swift
+public final class EudiWallet: ObservableObject
+```
+
+User wallet implementation
+
+## Properties
+### `storage`
+
+```swift
+public private(set) var storage: StorageManager
+```
+
+Storage manager instance
+
+### `standard`
+
+```swift
+public static private(set) var standard: EudiWallet = EudiWallet()
+```
+
+Instance of the wallet initialized with default parameters
+
+### `userAuthenticationRequired`
+
+```swift
+public var userAuthenticationRequired: Bool
+```
+
+Whether user authentication via biometrics or passcode is required before sending user data
+
+### `trustedReaderCertificates`
+
+```swift
+public var trustedReaderCertificates: [Data]?
+```
+
+Trusted root certificates to validate the reader authentication certificate included in the proximity request
+
+### `deviceAuthMethod`
+
+```swift
+public var deviceAuthMethod: DeviceAuthMethod = .deviceMac
+```
+
+Method to perform mdoc authentication (MAC or signature). Defaults to device MAC
+
+### `verifierApiUri`
+
+```swift
+public var verifierApiUri: String?
+```
+
+OpenID4VP verifier api URL (used for preregistered clients)
+
+### `openID4VciIssuerUrl`
+
+```swift
+public var openID4VciIssuerUrl: String?
+```
+
+OpenID4VCI issuer url
+
+### `openID4VciClientId`
+
+```swift
+public var openID4VciClientId: String?
+```
+
+OpenID4VCI client id
+
+### `openID4VciRedirectUri`
+
+```swift
+public var openID4VciRedirectUri: String = "eudi-openid4ci://authorize/"
+```
+
+OpenID4VCI redirect URI. Defaults to "eudi-openid4ci://authorize/"
+
+### `useSecureEnclave`
+
+```swift
+public var useSecureEnclave: Bool
+```
+
+Use iPhone Secure Enclave to protect keys and perform cryptographic operations. Defaults to true (if available)
+
+## Methods
+### `init(storageType:serviceName:accessGroup:trustedReaderCertificates:userAuthenticationRequired:verifierApiUri:openID4VciIssuerUrl:openID4VciClientId:openID4VciRedirectUri:)`
+
+```swift
+public init(storageType: StorageType = .keyChain, serviceName: String = "eudiw", accessGroup: String? = nil, trustedReaderCertificates: [Data]? = nil, userAuthenticationRequired: Bool = true, verifierApiUri: String? = nil, openID4VciIssuerUrl: String? = nil, openID4VciClientId: String? = nil, openID4VciRedirectUri: String? = nil)
+```
+
+Initialize a wallet instance. All parameters are optional.
+
+### `issueDocument(docType:format:)`
+
+```swift
+@discardableResult public func issueDocument(docType: String, format: DataFormat = .cbor) async throws -> WalletStorage.Document
+```
+
+Issue a document with the given docType using OpenId4Vci protocol
+
+If ``userAuthenticationRequired`` is true, user authentication is required. The authentication prompt message has localisation key "issue_document"
+ - Parameters:
+ - docType: Document type
+ - format: Optional format type. Defaults to cbor
+- Returns: The document issued. It is saved in storage.
+
+### `beginIssueDocument(id:privateKeyType:saveToStorage:)`
+
+```swift
+public func beginIssueDocument(id: String, privateKeyType: PrivateKeyType = .secureEnclaveP256, saveToStorage: Bool = true) async throws -> IssueRequest
+```
+
+Begin issuing a document by generating an issue request
+
+- Parameters:
+ - id: Document identifier
+ - issuer: Issuer function
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| id | Document identifier |
+| issuer | Issuer function |
+
+### `endIssueDocument(_:)`
+
+```swift
+public func endIssueDocument(_ issued: WalletStorage.Document) throws
+```
+
+End issuing by saving the issuing document (and its private key) in storage
+- Parameter issued: The issued document
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| issued | The issued document |
+
+### `loadDocuments()`
+
+```swift
+@discardableResult public func loadDocuments() async throws -> [WalletStorage.Document]?
+```
+
+Load documents from storage
+
+Calls ``storage`` loadDocuments
+- Returns: An array of ``WalletStorage.Document`` objects
+
+### `deleteDocuments()`
+
+```swift
+public func deleteDocuments() async throws
+```
+
+Delete all documents from storage
+
+Calls ``storage`` loadDocuments
+- Returns: An array of ``WalletStorage.Document`` objects
+
+### `loadSampleData(sampleDataFiles:)`
+
+```swift
+public func loadSampleData(sampleDataFiles: [String]? = nil) async throws
+```
+
+Load sample data from json files
+
+The mdoc data are stored in wallet storage as documents
+- Parameter sampleDataFiles: Names of sample files provided in the app bundle
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| sampleDataFiles | Names of sample files provided in the app bundle |
+
+### `prepareServiceDataParameters(docType:dataFormat:)`
+
+```swift
+public func prepareServiceDataParameters(docType: String? = nil, dataFormat: DataFormat = .cbor ) throws -> [String : Any]
+```
+
+Prepare Service Data Parameters
+- Parameters:
+ - docType: docType of documents to present (optional)
+ - dataFormat: Exchanged data ``Format`` type
+- Returns: A data dictionary that can be used to initialize a presentation service
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| docType | docType of documents to present (optional) |
+| dataFormat | Exchanged data `Format` type |
+
+### `beginPresentation(flow:docType:dataFormat:)`
+
+```swift
+public func beginPresentation(flow: FlowType, docType: String? = nil, dataFormat: DataFormat = .cbor) -> PresentationSession
+```
+
+Begin attestation presentation to a verifier
+- Parameters:
+ - flow: Presentation ``FlowType`` instance
+ - docType: DocType of documents to present (optional)
+ - dataFormat: Exchanged data ``Format`` type
+- Returns: A presentation session instance,
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| flow | Presentation `FlowType` instance |
+| docType | DocType of documents to present (optional) |
+| dataFormat | Exchanged data `Format` type |
+
+### `beginPresentation(service:)`
+
+```swift
+public func beginPresentation(service: any PresentationService) -> PresentationSession
+```
+
+Begin attestation presentation to a verifier
+- Parameters:
+ - service: A ``PresentationService`` instance
+ - docType: DocType of documents to present (optional)
+ - dataFormat: Exchanged data ``Format`` type
+- Returns: A presentation session instance,
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| service | A `PresentationService` instance |
+| docType | DocType of documents to present (optional) |
+| dataFormat | Exchanged data `Format` type |
+
+### `authorizedAction(action:disabled:dismiss:localizedReason:)`
+
+```swift
+public static func authorizedAction(action: () async throws -> T, disabled: Bool, dismiss: () -> Void, localizedReason: String) async throws -> T?
+```
+
+Perform an action after user authorization via TouchID/FaceID/Passcode
+- Parameters:
+ - dismiss: Action to perform if the user cancels authorization
+ - action: Action to perform after user authorization
diff --git a/Documentation/Reference/classes/FaultPresentationService.md b/Documentation/Reference/classes/FaultPresentationService.md
index 4453038..c73470f 100644
--- a/Documentation/Reference/classes/FaultPresentationService.md
+++ b/Documentation/Reference/classes/FaultPresentationService.md
@@ -7,57 +7,66 @@
- [Properties](#properties)
- `status`
- `flow`
- - `error`
- [Methods](#methods)
+ - `init(msg:)`
- `init(error:)`
- - `generateQRCode()`
+ - `startQrEngagement()`
- `receiveRequest()`
- - `sendResponse(userAccepted:itemsToSend:)`
+ - `sendResponse(userAccepted:itemsToSend:onSuccess:)`
```swift
-class FaultPresentationService: PresentationService
+public class FaultPresentationService: PresentationService
```
+Fault presentation service. Used to communicate error state to the user
+
## Properties
### `status`
```swift
-var status: TransferStatus = .error
+public var status: TransferStatus = .error
```
### `flow`
```swift
-var flow: FlowType = .ble
+public var flow: FlowType = .other
```
-### `error`
+## Methods
+### `init(msg:)`
```swift
-var error: Error
+public init(msg: String)
```
-## Methods
### `init(error:)`
```swift
-init(error: Error)
+public init(error: Error)
```
-### `generateQRCode()`
+### `startQrEngagement()`
```swift
-func generateQRCode() async throws -> Data?
+public func startQrEngagement() async throws -> String?
```
### `receiveRequest()`
```swift
-func receiveRequest() async throws -> [String : Any]
+public func receiveRequest() async throws -> [String : Any]
```
-### `sendResponse(userAccepted:itemsToSend:)`
+### `sendResponse(userAccepted:itemsToSend:onSuccess:)`
```swift
-func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws
+public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)?) async throws
```
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| userAccepted | True if user accepted to send the response |
+| itemsToSend | The selected items to send organized in document types and namespaces (see `RequestItems`) |
\ No newline at end of file
diff --git a/Documentation/Reference/classes/KeyChainStorageService.md b/Documentation/Reference/classes/KeyChainStorageService.md
deleted file mode 100644
index 22e3154..0000000
--- a/Documentation/Reference/classes/KeyChainStorageService.md
+++ /dev/null
@@ -1,117 +0,0 @@
-**CLASS**
-
-# `KeyChainStorageService`
-
-**Contents**
-
-- [Properties](#properties)
- - `defaultId`
- - `vcService`
- - `accessGroup`
- - `itemTypeCode`
-- [Methods](#methods)
- - `init()`
- - `loadDocument(id:)`
- - `saveDocument(id:value:)`
- - `deleteDocument(id:)`
-
-```swift
-public class KeyChainStorageService: DataStorageService
-```
-
-## Properties
-### `defaultId`
-
-```swift
-public static var defaultId: String = "eudiw"
-```
-
-### `vcService`
-
-```swift
-var vcService = "eudiw"
-```
-
-### `accessGroup`
-
-```swift
-var accessGroup: String?
-```
-
-### `itemTypeCode`
-
-```swift
-var itemTypeCode: Int?
-```
-
-## Methods
-### `init()`
-
-```swift
-public init()
-```
-
-### `loadDocument(id:)`
-
-```swift
-public func loadDocument(id: String) throws -> Data
-```
-
-Gets the secret with the id passed in parameter
-- Parameters:
- - id: The Id of the secret
- - itemTypeCode: the item type code for the secret
- - accessGroup: the access group for the secret
-- Returns: The secret
-
-#### Parameters
-
-| Name | Description |
-| ---- | ----------- |
-| id | The Id of the secret |
-| itemTypeCode | the item type code for the secret |
-| accessGroup | the access group for the secret |
-
-### `saveDocument(id:value:)`
-
-```swift
-public func saveDocument(id: String, value: inout Data) throws
-```
-
-Save the secret to keychain
-Note: the value passed in will be zeroed out after the secret is saved
-- Parameters:
- - id: The Id of the secret
- - itemTypeCode: The secret type code (4 chars)
- - accessGroup: The access group to use to save secret.
- - value: The value of the secret
-
-#### Parameters
-
-| Name | Description |
-| ---- | ----------- |
-| id | The Id of the secret |
-| itemTypeCode | The secret type code (4 chars) |
-| accessGroup | The access group to use to save secret. |
-| value | The value of the secret |
-
-### `deleteDocument(id:)`
-
-```swift
-public func deleteDocument(id: String) throws
-```
-
-Delete the secret from keychain
-Note: the value passed in will be zeroed out after the secret is deleted
-- Parameters:
- - id: The Id of the secret
- - itemTypeCode: The secret type code (4 chars)
- - accessGroup: The access group of the secret.
-
-#### Parameters
-
-| Name | Description |
-| ---- | ----------- |
-| id | The Id of the secret |
-| itemTypeCode | The secret type code (4 chars) |
-| accessGroup | The access group of the secret. |
\ No newline at end of file
diff --git a/Documentation/Reference/classes/OpenId4VCIService.md b/Documentation/Reference/classes/OpenId4VCIService.md
new file mode 100644
index 0000000..7d79f4d
--- /dev/null
+++ b/Documentation/Reference/classes/OpenId4VCIService.md
@@ -0,0 +1,42 @@
+**CLASS**
+
+# `OpenId4VCIService`
+
+**Contents**
+
+- [Methods](#methods)
+ - `issueDocument(docType:format:useSecureEnclave:)`
+ - `presentationAnchor(for:)`
+
+```swift
+public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContextProviding
+```
+
+## Methods
+### `issueDocument(docType:format:useSecureEnclave:)`
+
+```swift
+public func issueDocument(docType: String, format: DataFormat, useSecureEnclave: Bool = true) async throws -> Data
+```
+
+Issue a document with the given `docType` using OpenId4Vci protocol
+- Parameters:
+ - docType: the docType of the document to be issued
+ - format: format of the exchanged data
+ - useSecureEnclave: use secure enclave to protect the private key
+- Returns: The data of the document
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| docType | the docType of the document to be issued |
+| format | format of the exchanged data |
+| useSecureEnclave | use secure enclave to protect the private key |
+
+### `presentationAnchor(for:)`
+
+```swift
+public func presentationAnchor(for session: ASWebAuthenticationSession)
+-> ASPresentationAnchor
+```
diff --git a/Documentation/Reference/classes/OpenId4VpService.md b/Documentation/Reference/classes/OpenId4VpService.md
index d3045a8..d20b141 100644
--- a/Documentation/Reference/classes/OpenId4VpService.md
+++ b/Documentation/Reference/classes/OpenId4VpService.md
@@ -6,130 +6,71 @@
- [Properties](#properties)
- `status`
- - `openid4VPlink`
- - `docs`
- - `iaca`
- - `devicePrivateKey`
- - `logger`
- - `presentationDefinition`
- - `resolvedRequestData`
- - `siopOpenId4Vp`
- `flow`
- - `walletConf`
- [Methods](#methods)
- - `init(parameters:qrCode:)`
- - `generateQRCode()`
+ - `init(parameters:qrCode:openId4VpVerifierApiUri:)`
+ - `startQrEngagement()`
- `receiveRequest()`
- - `sendResponse(userAccepted:itemsToSend:)`
- - `parsePresentationDefinition(_:)`
+ - `sendResponse(userAccepted:itemsToSend:onSuccess:)`
```swift
-class OpenId4VpService: PresentationService
+public class OpenId4VpService: PresentationService
```
+Implements remote attestation presentation to online verifier
+Implementation is based on the OpenID4VP – Draft 18 specification
+
## Properties
### `status`
```swift
-var status: TransferStatus = .initialized
-```
-
-### `openid4VPlink`
-
-```swift
-var openid4VPlink: String
-```
-
-### `docs`
-
-```swift
-var docs: [DeviceResponse]!
-```
-
-### `iaca`
-
-```swift
-var iaca: [SecCertificate]!
-```
-
-### `devicePrivateKey`
-
-```swift
-var devicePrivateKey: CoseKeyPrivate!
-```
-
-### `logger`
-
-```swift
-var logger = Logger(label: "OpenId4VpService")
+public var status: TransferStatus = .initialized
```
-### `presentationDefinition`
-
-```swift
-var presentationDefinition: PresentationDefinition?
-```
-
-### `resolvedRequestData`
-
-```swift
-var resolvedRequestData: ResolvedRequestData?
-```
-
-### `siopOpenId4Vp`
+### `flow`
```swift
-var siopOpenId4Vp: SiopOpenID4VP!
+public var flow: FlowType
```
-### `flow`
+## Methods
+### `init(parameters:qrCode:openId4VpVerifierApiUri:)`
```swift
-var flow: FlowType
+public init(parameters: [String: Any], qrCode: Data, openId4VpVerifierApiUri: String?) throws
```
-### `walletConf`
+### `startQrEngagement()`
```swift
-static var walletConf: WalletOpenId4VPConfiguration? = {
- let VERIFIER_API = ProcessInfo.processInfo.environment["VERIFIER_API"] ?? "http://localhost:8080"
- let verifierMetaData = PreregisteredClient(clientId: "Verifier", jarSigningAlg: JWSAlgorithm(.RS256), jwkSetSource: WebKeySource.fetchByReference(url: URL(string: "\(VERIFIER_API)/wallet/public-keys.json")!))
- guard let rsaPrivateKey = try? KeyController.generateRSAPrivateKey(), let privateKey = try? KeyController.generateECDHPrivateKey(),
- let rsaPublicKey = try? KeyController.generateRSAPublicKey(from: rsaPrivateKey) else { return nil }
- guard let rsaJWK = try? RSAPublicKey(publicKey: rsaPublicKey, additionalParameters: ["use": "sig", "kid": UUID().uuidString, "alg": "RS256"]) else { return nil }
- guard let keySet = try? WebKeySet(jwk: rsaJWK) else { return nil }
- var res = WalletOpenId4VPConfiguration(subjectSyntaxTypesSupported: [], preferredSubjectSyntaxType: .jwkThumbprint, decentralizedIdentifier: try! DecentralizedIdentifier(rawValue: "did:example:123"), idTokenTTL: 10 * 60, presentationDefinitionUriSupported: true, signingKey: privateKey, signingKeySet: keySet, supportedClientIdSchemes: [.preregistered(clients: [verifierMetaData.clientId: verifierMetaData])], vpFormatsSupported: [])
- return res
-}()
+public func startQrEngagement() async throws -> String?
```
-## Methods
-### `init(parameters:qrCode:)`
+### `receiveRequest()`
```swift
-init(parameters: [String: Any], qrCode: Data) throws
+public func receiveRequest() async throws -> [String: Any]
```
-### `generateQRCode()`
+ Receive request from an openid4vp URL
-```swift
-func generateQRCode() async throws -> Data?
-```
+- Returns: The requested items.
-### `receiveRequest()`
+### `sendResponse(userAccepted:itemsToSend:onSuccess:)`
```swift
-func receiveRequest() async throws -> [String: Any]
+public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)?) async throws
```
-### `sendResponse(userAccepted:itemsToSend:)`
+Send response via openid4vp
-```swift
-func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws
-```
+- Parameters:
+ - userAccepted: True if user accepted to send the response
+ - itemsToSend: The selected items to send organized in document types and namespaces
-### `parsePresentationDefinition(_:)`
+#### Parameters
-```swift
-func parsePresentationDefinition(_ presentationDefinition: PresentationDefinition) -> RequestItems?
-```
+| Name | Description |
+| ---- | ----------- |
+| userAccepted | True if user accepted to send the response |
+| itemsToSend | The selected items to send organized in document types and namespaces |
\ No newline at end of file
diff --git a/Documentation/Reference/classes/PresentationSession.md b/Documentation/Reference/classes/PresentationSession.md
index 57ecc3d..f250871 100644
--- a/Documentation/Reference/classes/PresentationSession.md
+++ b/Documentation/Reference/classes/PresentationSession.md
@@ -6,107 +6,151 @@
- [Properties](#properties)
- `presentationService`
- - `readerCertIsserMessage`
+ - `readerCertIssuer`
- `readerCertValidationMessage`
- - `errorMessage`
- - `selectedRequestItems`
+ - `readerCertIssuerValid`
+ - `uiError`
+ - `disclosedDocuments`
- `status`
- - `flow`
- - `handleSelected`
- `deviceEngagement`
- - `notAvailable`
- [Methods](#methods)
- - `init(presentationService:)`
- - `decodeRequest(_:)`
- - `didFinishedWithError(_:)`
+ - `init(presentationService:userAuthenticationRequired:)`
- `makeError(str:)`
+ - `makeError(code:str:)`
+ - `startQrEngagement()`
+ - `receiveRequest()`
+ - `sendResponse(userAccepted:itemsToSend:onCancel:onSuccess:)`
```swift
public class PresentationSession: ObservableObject
```
+Presentation session
+
+This class wraps the ``PresentationService`` instance, providing bindable fields to a SwifUI view
+
## Properties
### `presentationService`
```swift
-var presentationService: any PresentationService
+public var presentationService: any PresentationService
```
-### `readerCertIsserMessage`
+### `readerCertIssuer`
```swift
-@Published public var readerCertIsserMessage: String?
+@Published public var readerCertIssuer: String?
```
+Reader certificate issuer (only for BLE flow wih verifier using reader authentication)
+
### `readerCertValidationMessage`
```swift
@Published public var readerCertValidationMessage: String?
```
-### `errorMessage`
+Reader certificate validation message (only for BLE transfer wih verifier using reader authentication)
+
+### `readerCertIssuerValid`
```swift
-@Published public var errorMessage: String = ""
+@Published public var readerCertIssuerValid: Bool?
```
-### `selectedRequestItems`
+Reader certificate issuer is valid (only for BLE transfer wih verifier using reader authentication)
+
+### `uiError`
```swift
-@Published public var selectedRequestItems: [DocElementsViewModel] = []
+@Published public var uiError: WalletError?
```
-### `status`
+Error message when the ``status`` is in the error state.
+
+### `disclosedDocuments`
```swift
-@Published public var status: TransferStatus = .initializing
+@Published public var disclosedDocuments: [DocElementsViewModel] = []
```
-### `flow`
+Request items selected by the user to be sent to verifier.
+
+### `status`
```swift
-public var flow: FlowType
+@Published public var status: TransferStatus = .initializing
```
-### `handleSelected`
+Status of the data transfer.
+
+### `deviceEngagement`
```swift
-public var handleSelected: ((Bool, RequestItems?) -> Void)?
+@Published public var deviceEngagement: String?
```
-### `deviceEngagement`
+Device engagement data (QR data for the BLE flow)
+
+## Methods
+### `init(presentationService:userAuthenticationRequired:)`
```swift
-@Published public var deviceEngagement: Data?
+public init(presentationService: any PresentationService, userAuthenticationRequired: Bool)
```
-### `notAvailable`
+### `makeError(str:)`
```swift
-public static var notAvailable: PresentationSession
+public static func makeError(str: String) -> NSError
```
-## Methods
-### `init(presentationService:)`
+### `makeError(code:str:)`
```swift
-public init(presentationService: any PresentationService)
+public static func makeError(code: ErrorCode, str: String? = nil) -> NSError
```
-### `decodeRequest(_:)`
+### `startQrEngagement()`
```swift
-public func decodeRequest(_ request: [String: Any])
+public func startQrEngagement() async
```
-### `didFinishedWithError(_:)`
+Start QR engagement to be presented to verifier
+
+On success ``deviceEngagement`` published variable will be set with the result and ``status`` will be ``.qrEngagementReady``
+On error ``uiError`` will be filled and ``status`` will be ``.error``
+
+### `receiveRequest()`
```swift
-public func didFinishedWithError(_ error: Error)
+public func receiveRequest() async -> [String: Any]?
```
-### `makeError(str:)`
+Receive request from verifer
+
+The request is futher decoded internally. See also ``decodeRequest(_:)``
+On success ``disclosedDocuments`` published variable will be set and ``status`` will be ``.requestReceived``
+On error ``uiError`` will be filled and ``status`` will be ``.error``
+- Returns: A request dictionary keyed by ``MdocDataTransfer.UserRequestKeys``
+
+### `sendResponse(userAccepted:itemsToSend:onCancel:onSuccess:)`
```swift
-public static func makeError(str: String) -> NSError
+public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onCancel: (() -> Void)? = nil, onSuccess: ((URL?) -> Void)? = nil) async
```
+
+Send response to verifier
+- Parameters:
+ - userAccepted: Whether user confirmed to send the response
+ - itemsToSend: Data to send organized into a hierarcy of doc.types and namespaces
+ - onCancel: Action to perform if the user cancels the biometric authentication
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| userAccepted | Whether user confirmed to send the response |
+| itemsToSend | Data to send organized into a hierarcy of doc.types and namespaces |
+| onCancel | Action to perform if the user cancels the biometric authentication |
\ No newline at end of file
diff --git a/Documentation/Reference/classes/StorageManager.md b/Documentation/Reference/classes/StorageManager.md
new file mode 100644
index 0000000..f0d6fce
--- /dev/null
+++ b/Documentation/Reference/classes/StorageManager.md
@@ -0,0 +1,238 @@
+**CLASS**
+
+# `StorageManager`
+
+**Contents**
+
+- [Properties](#properties)
+ - `knownDocTypes`
+ - `docTypes`
+ - `mdocModels`
+ - `documentIds`
+ - `hasData`
+ - `hasWellKnownData`
+ - `docCount`
+ - `mdlModel`
+ - `pidModel`
+ - `otherModels`
+ - `uiError`
+- [Methods](#methods)
+ - `init(storageService:)`
+ - `loadDocuments()`
+ - `getDocumentModel(index:)`
+ - `getDocumentModel(docType:)`
+ - `deleteDocument(docType:)`
+ - `deleteDocument(index:)`
+ - `deleteDocuments()`
+
+```swift
+public class StorageManager: ObservableObject
+```
+
+Storage manager. Provides services and view models
+
+## Properties
+### `knownDocTypes`
+
+```swift
+public static let knownDocTypes = [EuPidModel.euPidDocType, IsoMdlModel.isoDocType]
+```
+
+### `docTypes`
+
+```swift
+public var docTypes: [String?] = []
+```
+
+Array of doc.types of documents loaded in the wallet
+
+### `mdocModels`
+
+```swift
+@Published public var mdocModels: [MdocDecodable?] = []
+```
+
+Array of document models loaded in the wallet
+
+### `documentIds`
+
+```swift
+public var documentIds: [String?] = []
+```
+
+Array of document identifiers loaded in the wallet
+
+### `hasData`
+
+```swift
+@Published public var hasData: Bool = false
+```
+
+Whether wallet currently has loaded data
+
+### `hasWellKnownData`
+
+```swift
+@Published public var hasWellKnownData: Bool = false
+```
+
+Whether wallet currently has loaded a document with doc.type included in the ``knownDocTypes`` array
+
+### `docCount`
+
+```swift
+@Published public var docCount: Int = 0
+```
+
+Count of documents loaded in the wallet
+
+### `mdlModel`
+
+```swift
+@Published public var mdlModel: IsoMdlModel?
+```
+
+The driver license model loaded in the wallet
+
+### `pidModel`
+
+```swift
+@Published public var pidModel: EuPidModel?
+```
+
+The PID model loaded in the wallet
+
+### `otherModels`
+
+```swift
+@Published public var otherModels: [GenericMdocModel] = []
+```
+
+Other document models loaded in the wallet
+
+### `uiError`
+
+```swift
+@Published public var uiError: WalletError?
+```
+
+Error object with localized message
+
+## Methods
+### `init(storageService:)`
+
+```swift
+public init(storageService: any DataStorageService)
+```
+
+### `loadDocuments()`
+
+```swift
+@discardableResult public func loadDocuments() async throws -> [WalletStorage.Document]?
+```
+
+Load documents from storage
+
+Internally sets the ``docTypes``, ``mdocModels``, ``documentIds``, ``mdocModels``, ``mdlModel``, ``pidModel`` variables
+- Returns: An array of ``WalletStorage.Document`` objects
+
+### `getDocumentModel(index:)`
+
+```swift
+public func getDocumentModel(index: Int) -> MdocDecodable?
+```
+
+Get document model by index
+- Parameter index: Index in array of loaded models
+- Returns: The ``MdocDecodable`` model
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| index | Index in array of loaded models |
+
+### `getDocumentModel(id:)`
+
+```swift
+public func getDocumentModel(id: String) -> MdocDecodable?
+```
+
+Get document model by id
+- Parameter id: The id of the document model to return
+- Returns: The ``MdocDecodable`` model
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| id | The id of the document model to return |
+
+### `getDocumentModels(docType:)`
+
+```swift
+public func getDocumentModels(docType: String) -> [MdocDecodable]
+```
+
+Get document model by docType
+- Parameter docType: The docType of the document model to return
+- Returns: The ``MdocDecodable`` model
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| docType | The docType of the document model to return |
+
+### `deleteDocuments(docType:)`
+
+```swift
+public func deleteDocuments(docType: String) async throws
+```
+
+Delete documents by docType
+- Parameter docType: Document type
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| docType | Document type |
+
+### `deleteDocument(id:)`
+
+```swift
+public func deleteDocument(id: String) async throws
+```
+
+Delete document by id
+- Parameter id: Document id
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| id | Document id |
+
+### `deleteDocument(index:)`
+
+```swift
+public func deleteDocument(index: Int) async throws
+```
+
+Delete document by Index
+- Parameter index: Index in array of loaded models
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| index | Index in array of loaded models |
+
+### `deleteDocuments()`
+
+```swift
+public func deleteDocuments() async throws
+```
+
+Delete documenmts
diff --git a/Documentation/Reference/classes/UserWallet.md b/Documentation/Reference/classes/UserWallet.md
deleted file mode 100644
index 2291d8b..0000000
--- a/Documentation/Reference/classes/UserWallet.md
+++ /dev/null
@@ -1,35 +0,0 @@
-**CLASS**
-
-# `UserWallet`
-
-**Contents**
-
-- [Properties](#properties)
- - `storageService`
-- [Methods](#methods)
- - `init(storageService:)`
- - `beginPresentation(flow:dataFormat:)`
-
-```swift
-public class UserWallet: ObservableObject
-```
-
-## Properties
-### `storageService`
-
-```swift
-public var storageService: any DataStorageService
-```
-
-## Methods
-### `init(storageService:)`
-
-```swift
-public init(storageService: any DataStorageService = KeyChainStorageService())
-```
-
-### `beginPresentation(flow:dataFormat:)`
-
-```swift
-public func beginPresentation(flow: FlowType, dataFormat: DataFormat = .cbor) -> PresentationSession
-```
diff --git a/Documentation/Reference/enums/DataFormat.md b/Documentation/Reference/enums/DataFormat.md
index 7a43cb4..ca75577 100644
--- a/Documentation/Reference/enums/DataFormat.md
+++ b/Documentation/Reference/enums/DataFormat.md
@@ -6,21 +6,23 @@
- [Cases](#cases)
- `cbor`
- - `jwt`
+ - `sdjwt`
```swift
-public enum DataFormat
+public enum DataFormat: String
```
+Data format of the exchanged data
+
## Cases
### `cbor`
```swift
-case cbor
+case cbor = "cbor"
```
-### `jwt`
+### `sdjwt`
```swift
-case jwt
+case sdjwt = "sdjwt"
```
diff --git a/Documentation/Reference/enums/FlowType.md b/Documentation/Reference/enums/FlowType.md
index 44d0d18..e83e450 100644
--- a/Documentation/Reference/enums/FlowType.md
+++ b/Documentation/Reference/enums/FlowType.md
@@ -7,13 +7,17 @@
- [Cases](#cases)
- `ble`
- `openid4vp(qrCode:)`
+ - `other`
- [Properties](#properties)
- `isProximity`
+ - `qrCode`
```swift
public enum FlowType: Codable, Hashable
```
+Data exchange flow type
+
## Cases
### `ble`
@@ -27,9 +31,23 @@ case ble
case openid4vp(qrCode: Data)
```
+### `other`
+
+```swift
+case other
+```
+
## Properties
### `isProximity`
```swift
public var isProximity: Bool
```
+
+True if proximity flow type (currently ``ble``)
+
+### `qrCode`
+
+```swift
+public var qrCode: Data?
+```
diff --git a/Documentation/Reference/enums/OpenId4VCIError.md b/Documentation/Reference/enums/OpenId4VCIError.md
new file mode 100644
index 0000000..5a76bf2
--- /dev/null
+++ b/Documentation/Reference/enums/OpenId4VCIError.md
@@ -0,0 +1,70 @@
+**ENUM**
+
+# `OpenId4VCIError`
+
+**Contents**
+
+- [Cases](#cases)
+ - `authRequestFailed(_:)`
+ - `authorizeResponseNoUrl`
+ - `authorizeResponseNoCode`
+ - `tokenRequestFailed(_:)`
+ - `tokenResponseNoData`
+ - `tokenResponseInvalidData(_:)`
+ - `dataNotValid`
+- [Properties](#properties)
+ - `localizedDescription`
+
+```swift
+public enum OpenId4VCIError: LocalizedError
+```
+
+## Cases
+### `authRequestFailed(_:)`
+
+```swift
+case authRequestFailed(Error)
+```
+
+### `authorizeResponseNoUrl`
+
+```swift
+case authorizeResponseNoUrl
+```
+
+### `authorizeResponseNoCode`
+
+```swift
+case authorizeResponseNoCode
+```
+
+### `tokenRequestFailed(_:)`
+
+```swift
+case tokenRequestFailed(Error)
+```
+
+### `tokenResponseNoData`
+
+```swift
+case tokenResponseNoData
+```
+
+### `tokenResponseInvalidData(_:)`
+
+```swift
+case tokenResponseInvalidData(String)
+```
+
+### `dataNotValid`
+
+```swift
+case dataNotValid
+```
+
+## Properties
+### `localizedDescription`
+
+```swift
+public var localizedDescription: String
+```
diff --git a/Documentation/Reference/enums/StorageType.md b/Documentation/Reference/enums/StorageType.md
new file mode 100644
index 0000000..ba81170
--- /dev/null
+++ b/Documentation/Reference/enums/StorageType.md
@@ -0,0 +1,19 @@
+**ENUM**
+
+# `StorageType`
+
+**Contents**
+
+- [Cases](#cases)
+ - `keyChain`
+
+```swift
+public enum StorageType
+```
+
+## Cases
+### `keyChain`
+
+```swift
+case keyChain
+```
diff --git a/Documentation/Reference/enums/TransferStatus.md b/Documentation/Reference/enums/TransferStatus.md
deleted file mode 100644
index 43c5a40..0000000
--- a/Documentation/Reference/enums/TransferStatus.md
+++ /dev/null
@@ -1,84 +0,0 @@
-**ENUM**
-
-# `TransferStatus`
-
-**Contents**
-
-- [Cases](#cases)
- - `initializing`
- - `initialized`
- - `qrEngagementReady`
- - `connected`
- - `started`
- - `requestReceived`
- - `userSelected`
- - `responseSent`
- - `disconnected`
- - `error`
-
-```swift
-public enum TransferStatus: String
-```
-
-Transfer status enumeration
-
-## Cases
-### `initializing`
-
-```swift
-case initializing
-```
-
-### `initialized`
-
-```swift
-case initialized
-```
-
-### `qrEngagementReady`
-
-```swift
-case qrEngagementReady
-```
-
-### `connected`
-
-```swift
-case connected
-```
-
-### `started`
-
-```swift
-case started
-```
-
-### `requestReceived`
-
-```swift
-case requestReceived
-```
-
-### `userSelected`
-
-```swift
-case userSelected
-```
-
-### `responseSent`
-
-```swift
-case responseSent
-```
-
-### `disconnected`
-
-```swift
-case disconnected
-```
-
-### `error`
-
-```swift
-case error
-```
diff --git a/Documentation/Reference/extensions/Array.md b/Documentation/Reference/extensions/Array.md
index 0f7aa93..eb30dfb 100644
--- a/Documentation/Reference/extensions/Array.md
+++ b/Documentation/Reference/extensions/Array.md
@@ -6,8 +6,8 @@ extension Array where Element == DocElementsViewModel
```
## Properties
-### `docSelectedDictionary`
+### `items`
```swift
-public var docSelectedDictionary: RequestItems
+public var items: RequestItems
```
diff --git a/Documentation/Reference/extensions/BlePresentationService.md b/Documentation/Reference/extensions/BlePresentationService.md
index 4eb7818..c62b1d9 100644
--- a/Documentation/Reference/extensions/BlePresentationService.md
+++ b/Documentation/Reference/extensions/BlePresentationService.md
@@ -12,14 +12,44 @@ extension BlePresentationService: MdocOfflineDelegate
public func didChangeStatus(_ newStatus: MdocDataTransfer18013.TransferStatus)
```
+BLE transfer changed status
+- Parameter newStatus: New status
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| newStatus | New status |
+
### `didFinishedWithError(_:)`
```swift
public func didFinishedWithError(_ error: Error)
```
+Transfer finished with error
+- Parameter error: The error description
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| error | The error description |
+
### `didReceiveRequest(_:handleSelected:)`
```swift
public func didReceiveRequest(_ request: [String : Any], handleSelected: @escaping (Bool, MdocDataTransfer18013.RequestItems?) -> Void)
```
+
+Received request handler
+- Parameters:
+ - request: Request items keyed by §UserRequestKeys§
+ - handleSelected: Callback function to call after user selection of items to send
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| request | Request items keyed by §UserRequestKeys§ |
+| handleSelected | Callback function to call after user selection of items to send |
\ No newline at end of file
diff --git a/Documentation/Reference/extensions/PresentationSession.md b/Documentation/Reference/extensions/PresentationSession.md
deleted file mode 100644
index 35397a9..0000000
--- a/Documentation/Reference/extensions/PresentationSession.md
+++ /dev/null
@@ -1,31 +0,0 @@
-**EXTENSION**
-
-# `PresentationSession`
-```swift
-extension PresentationSession: PresentationService
-```
-
-## Methods
-### `presentAttestations()`
-
-```swift
-@discardableResult public func presentAttestations() async throws -> [String: Any]
-```
-
-### `generateQRCode()`
-
-```swift
-public func generateQRCode() async throws -> Data?
-```
-
-### `receiveRequest()`
-
-```swift
-public func receiveRequest() async throws -> [String: Any]
-```
-
-### `sendResponse(userAccepted:itemsToSend:)`
-
-```swift
-public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws
-```
diff --git a/Documentation/Reference/extensions/String.md b/Documentation/Reference/extensions/String.md
new file mode 100644
index 0000000..56cc827
--- /dev/null
+++ b/Documentation/Reference/extensions/String.md
@@ -0,0 +1,13 @@
+**EXTENSION**
+
+# `String`
+```swift
+extension String
+```
+
+## Methods
+### `translated()`
+
+```swift
+public func translated() -> String
+```
diff --git a/Documentation/Reference/methods/fluttenItemViewModels(__valid_).md b/Documentation/Reference/methods/fluttenItemViewModels(__valid_).md
deleted file mode 100644
index 5d77ffa..0000000
--- a/Documentation/Reference/methods/fluttenItemViewModels(__valid_).md
+++ /dev/null
@@ -1,5 +0,0 @@
-### `fluttenItemViewModels(_:valid:)`
-
-```swift
-func fluttenItemViewModels(_ nsItems: [String:[String]], valid isEnabled: Bool) -> [ElementViewModel]
-```
diff --git a/Documentation/Reference/methods/nsItemsToViewModels(______).md b/Documentation/Reference/methods/nsItemsToViewModels(______).md
deleted file mode 100644
index d46b6f4..0000000
--- a/Documentation/Reference/methods/nsItemsToViewModels(______).md
+++ /dev/null
@@ -1,5 +0,0 @@
-### `nsItemsToViewModels(_:_:_:)`
-
-```swift
-func nsItemsToViewModels(_ ns: String, _ items: [String], _ isEnabled: Bool) -> [ElementViewModel]
-```
diff --git a/Documentation/Reference/protocols/DataStorageService.md b/Documentation/Reference/protocols/DataStorageService.md
deleted file mode 100644
index be433e9..0000000
--- a/Documentation/Reference/protocols/DataStorageService.md
+++ /dev/null
@@ -1,33 +0,0 @@
-**PROTOCOL**
-
-# `DataStorageService`
-
-```swift
-public protocol DataStorageService
-```
-
-## Properties
-### `defaultId`
-
-```swift
-static var defaultId: String
-```
-
-## Methods
-### `loadDocument(id:)`
-
-```swift
-func loadDocument(id: String) throws -> Data
-```
-
-### `saveDocument(id:value:)`
-
-```swift
-func saveDocument(id: String, value: inout Data) throws
-```
-
-### `deleteDocument(id:)`
-
-```swift
-func deleteDocument(id: String) throws
-```
diff --git a/Documentation/Reference/protocols/PresentationService.md b/Documentation/Reference/protocols/PresentationService.md
index a4eaf31..fb15704 100644
--- a/Documentation/Reference/protocols/PresentationService.md
+++ b/Documentation/Reference/protocols/PresentationService.md
@@ -6,34 +6,49 @@
public protocol PresentationService
```
-## Properties
-### `status`
-
-```swift
-var status: TransferStatus
-```
+Presentation service abstract protocol
+## Properties
### `flow`
```swift
var flow: FlowType
```
+instance of a presentation ``FlowType``
+
## Methods
-### `generateQRCode()`
+### `startQrEngagement()`
```swift
-func generateQRCode() async throws -> Data?
+func startQrEngagement() async throws -> String?
```
+Generate a QR code to be shown to verifier (optional)
+
### `receiveRequest()`
```swift
func receiveRequest() async throws -> [String: Any]
```
-### `sendResponse(userAccepted:itemsToSend:)`
+- Returns: The requested items.
+Receive request.
+
+### `sendResponse(userAccepted:itemsToSend:onSuccess:)`
```swift
-func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws
+func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)?) async throws
```
+
+Send response to verifier
+- Parameters:
+ - userAccepted: True if user accepted to send the response
+ - itemsToSend: The selected items to send organized in document types and namespaces (see ``RequestItems``)
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| userAccepted | True if user accepted to send the response |
+| itemsToSend | The selected items to send organized in document types and namespaces (see `RequestItems`) |
\ No newline at end of file
diff --git a/Documentation/Reference/structs/DocElementsViewModel.md b/Documentation/Reference/structs/DocElementsViewModel.md
index eec5f29..918520f 100644
--- a/Documentation/Reference/structs/DocElementsViewModel.md
+++ b/Documentation/Reference/structs/DocElementsViewModel.md
@@ -14,6 +14,8 @@
public struct DocElementsViewModel: Identifiable
```
+View model used in SwiftUI for presentation request elements
+
## Properties
### `id`
diff --git a/Documentation/Reference/structs/ElementViewModel.md b/Documentation/Reference/structs/ElementViewModel.md
index 59fe076..dd70c6b 100644
--- a/Documentation/Reference/structs/ElementViewModel.md
+++ b/Documentation/Reference/structs/ElementViewModel.md
@@ -8,6 +8,7 @@
- `id`
- `nameSpace`
- `elementIdentifier`
+ - `isMandatory`
- `isEnabled`
- `isDisabled`
- `isSelected`
@@ -35,6 +36,12 @@ public let nameSpace: String
public let elementIdentifier: String
```
+### `isMandatory`
+
+```swift
+public let isMandatory: Bool
+```
+
### `isEnabled`
```swift
diff --git a/Documentation/Reference/structs/WalletError.md b/Documentation/Reference/structs/WalletError.md
new file mode 100644
index 0000000..b4df202
--- /dev/null
+++ b/Documentation/Reference/structs/WalletError.md
@@ -0,0 +1,44 @@
+**STRUCT**
+
+# `WalletError`
+
+**Contents**
+
+- [Properties](#properties)
+ - `errorDescription`
+- [Methods](#methods)
+ - `init(key:code:)`
+ - `init(description:code:userInfo:)`
+ - `==(_:_:)`
+
+```swift
+public struct WalletError: LocalizedError
+```
+
+Wallet error
+
+## Properties
+### `errorDescription`
+
+```swift
+public var errorDescription: String?
+```
+
+## Methods
+### `init(key:code:)`
+
+```swift
+public init(key: String, code: Int = 0)
+```
+
+### `init(description:code:userInfo:)`
+
+```swift
+public init(description: String, code: Int = 0, userInfo: [String: Any]? = nil)
+```
+
+### `==(_:_:)`
+
+```swift
+public static func ==(lhs: Self, rhs: Self) -> Bool
+```
diff --git a/Documentation/Reference/typealiases/RequestItems.md b/Documentation/Reference/typealiases/RequestItems.md
index e55a51e..135bfd8 100644
--- a/Documentation/Reference/typealiases/RequestItems.md
+++ b/Documentation/Reference/typealiases/RequestItems.md
@@ -5,3 +5,5 @@
```swift
public typealias RequestItems = [String: [String: [String]]]
```
+
+[Doc Types to [Namespace to Items]] dictionary
\ No newline at end of file
diff --git a/Package.resolved b/Package.resolved
index 10c605b..74f9b4c 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,12 +1,12 @@
{
"pins" : [
{
- "identity" : "asn1decoder",
+ "identity" : "blueecc",
"kind" : "remoteSourceControl",
- "location" : "https://github.com/filom/ASN1Decoder",
+ "location" : "https://github.com/niscy-eudiw/BlueECC.git",
"state" : {
- "revision" : "d30708c89cbbf8f0f27122a53fbd98d0cda079fd",
- "version" : "1.9.0"
+ "revision" : "15e525cfff2da9b7429285346248e2c67ba0bd12",
+ "version" : "1.2.4"
}
},
{
@@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-model.git",
"state" : {
- "branch" : "develop",
- "revision" : "007fd011160522c7bbaedc78b83f5869dadf6c33"
+ "revision" : "12314daf45d637a1afeda321d605f328d422fe8d",
+ "version" : "0.2.6"
}
},
{
@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git",
"state" : {
- "branch" : "develop",
- "revision" : "2572b2c26d20110b40966a797d77abcf23b95ae2"
+ "revision" : "ceeddd2a37f579212752bfb38e54efa87ec567e3",
+ "version" : "0.2.9"
}
},
{
@@ -41,8 +41,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-security.git",
"state" : {
- "branch" : "develop",
- "revision" : "e43b4cf893b11d7e9958258a00f0a07825db78d3"
+ "revision" : "41a7ba66ff5d614c9ef7d50607ef0a9af7ebb577",
+ "version" : "0.2.1"
+ }
+ },
+ {
+ "identity" : "eudi-lib-ios-openid4vci-swift",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git",
+ "state" : {
+ "revision" : "727bc48dcbdb7cd1cd942c0086fb8e9b7dc06879",
+ "version" : "0.3.1"
}
},
{
@@ -50,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-presentation-exchange-swift.git",
"state" : {
- "revision" : "70e122d3eb371ea6d8d5e423d3e98dabeaaebfaa",
- "version" : "0.0.41"
+ "revision" : "2c5486e3c3e38dab5018445af545677b62e9a712",
+ "version" : "0.0.43"
}
},
{
@@ -59,8 +68,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git",
"state" : {
- "branch" : "main",
- "revision" : "66fe1eda233c6a76950d55443290d51f5a56f18e"
+ "revision" : "2af63b02edded773b2202d987bed49ea6359102f",
+ "version" : "0.3.0"
+ }
+ },
+ {
+ "identity" : "eudi-lib-ios-wallet-storage",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git",
+ "state" : {
+ "revision" : "ba9ba8b2b889638d3a4b62358c741b34291848da",
+ "version" : "0.2.0"
}
},
{
@@ -68,8 +86,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/KittyMac/Hitch.git",
"state" : {
- "revision" : "0698c164bd972c15e64f517dba63716b7217087b",
- "version" : "0.4.115"
+ "revision" : "1a415afbf026005e77d32b536fa8d26c1b29e59f",
+ "version" : "0.4.138"
}
},
{
@@ -77,8 +95,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/niscy-eudiw/JOSESwift.git",
"state" : {
- "revision" : "ef86488a729fb1eb85edc3b174f528ac212c5ce6",
- "version" : "2.4.1"
+ "revision" : "518cedba79ef18867191811b161471298b6cb7c8",
+ "version" : "2.4.1-gcm"
}
},
{
@@ -104,8 +122,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/KittyMac/Sextant.git",
"state" : {
- "revision" : "49b567226e72ba4f22943e352e1ee3bf3bf4824e",
- "version" : "0.4.24"
+ "revision" : "52a77d0bce0210cf9557faef7fd0adb9a6da02fb",
+ "version" : "0.4.31"
}
},
{
@@ -113,8 +131,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/KittyMac/Spanker.git",
"state" : {
- "revision" : "6b0edd2996d1f3bebb534fb05e20eac6bf8c8739",
- "version" : "0.2.46"
+ "revision" : "96e58f68274a2e6f370fff153ceb5674a06936c4",
+ "version" : "0.2.47"
}
},
{
@@ -126,13 +144,40 @@
"version" : "0.10.1"
}
},
+ {
+ "identity" : "swift-asn1",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-asn1.git",
+ "state" : {
+ "revision" : "c7e239b5c1492ffc3ebd7fbcc7a92548ce4e78f0",
+ "version" : "1.1.0"
+ }
+ },
+ {
+ "identity" : "swift-certificates",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-certificates.git",
+ "state" : {
+ "revision" : "4688f242811d21a9c7a8ad669b3bc5b336759929",
+ "version" : "1.4.0"
+ }
+ },
+ {
+ "identity" : "swift-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-collections.git",
+ "state" : {
+ "revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
+ "version" : "1.1.1"
+ }
+ },
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
- "revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894",
- "version" : "2.6.0"
+ "revision" : "bc1c29221f6dfeb0ebbfbc98eb95cd3d4967868e",
+ "version" : "3.4.0"
}
},
{
@@ -158,17 +203,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
- "branch" : "main",
- "revision" : "cb28750240a9389e0023ee3e3cb6c83ce0960f5c"
+ "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5",
+ "version" : "1.5.4"
}
},
{
"identity" : "swiftcbor",
"kind" : "remoteSourceControl",
- "location" : "https://github.com/valpackett/SwiftCBOR.git",
+ "location" : "https://github.com/niscy-eudiw/SwiftCBOR.git",
+ "state" : {
+ "revision" : "310dbc3975a5653237fed304d88a6dd59d04dd30",
+ "version" : "0.5.7"
+ }
+ },
+ {
+ "identity" : "swiftyjson",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state" : {
- "branch" : "master",
- "revision" : "edc01765cf6b3685bb622bb09242ef5964fb991b"
+ "revision" : "2b6054efa051565954e1d2b9da831680026cd768",
+ "version" : "4.3.0"
}
}
],
diff --git a/Package.swift b/Package.swift
index da5a5b4..c705395 100644
--- a/Package.swift
+++ b/Package.swift
@@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
name: "EudiWalletKit",
- platforms: [.macOS(.v12), .iOS(.v14)],
+ platforms: [.macOS(.v13), .iOS(.v14)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
@@ -14,9 +14,11 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
- .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", branch: "develop"),
- .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", branch: "main"),
- .package(url: "https://github.com/apple/swift-log.git", branch: "main"),
+ .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"),
+ .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.2.9"),
+ .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", .upToNextMajor(from: "0.2.0")),
+ .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.3.0"),
+ .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.3.1"),
],
targets: [
// Targets are the basic building∫ blocks of a package, defining a module or a test suite.
@@ -24,12 +26,16 @@ let package = Package(
.target(
name: "EudiWalletKit", dependencies: [
.product(name: "MdocDataTransfer18013", package: "eudi-lib-ios-iso18013-data-transfer"),
+ .product(name: "WalletStorage", package: "eudi-lib-ios-wallet-storage"),
.product(name: "SiopOpenID4VP", package: "eudi-lib-ios-siop-openid4vp-swift"),
- .product(name: "Logging", package: "swift-log"),
- ]
+ .product(name: "OpenID4VCI", package: "eudi-lib-ios-openid4vci-swift"),
+ .product(name: "Logging", package: "swift-log"),
+ ]
),
.testTarget(
name: "EudiWalletKitTests",
- dependencies: ["EudiWalletKit"]),
+ dependencies: ["EudiWalletKit"],
+ resources: [.process("Resources")]
+ )
]
)
diff --git a/README.md b/README.md
index e9501b8..55d1436 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,11 @@
-# EUDI Wallet Reference Implementation
+# EUDI Wallet Kit library for iOS
-:heavy_exclamation_mark: **Important!** Before you proceed, please read the [EUDI Wallet Reference Implementation project description](wiki/EUDI_Wallet_Reference_Implementation.md)
+:heavy_exclamation_mark: **Important!** Before you proceed, please read
+the [EUDI Wallet Reference Implementation project description](https://github.com/eu-digital-identity-wallet/.github/blob/main/profile/reference-implementation.md)
----
-# EUDI ISO 18013-5 iOS Wallet Kit library
+# EUDI ISO iOS Wallet Kit library
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
[![Swift](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-kit/actions/workflows/swift.yml/badge.svg)](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-kit/actions/workflows/swift.yml)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=eu-digital-identity-wallet_eudi-lib-ios-wallet-kit&metric=ncloc&token=ceca670d1f503fb68c5545e9d6bf44465a5883a6)](https://sonarcloud.io/summary/new_code?id=eu-digital-identity-wallet_eudi-lib-ios-wallet-kit)
@@ -12,47 +13,156 @@
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=eu-digital-identity-wallet_eudi-lib-ios-wallet-kit&metric=reliability_rating&token=ceca670d1f503fb68c5545e9d6bf44465a5883a6)](https://sonarcloud.io/summary/new_code?id=eu-digital-identity-wallet_eudi-lib-ios-wallet-kit)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=eu-digital-identity-wallet_eudi-lib-ios-wallet-kit&metric=vulnerabilities&token=ceca670d1f503fb68c5545e9d6bf44465a5883a6)](https://sonarcloud.io/summary/new_code?id=eu-digital-identity-wallet_eudi-lib-ios-wallet-kit)
-The initial implementation provides Proximity and Remote Flows for the EUDI Wallet. It is based on the following specifications:
+## Overview
+
+This repository contains the EUDI Wallet Kit library for iOS. The library is a part
+of the EUDI Wallet Reference Implementation project.
+
+This library acts as a coordinator by orchestrating the various components that are
+required to implement the EUDI Wallet functionality. On top of that, it provides a simplified API
+that can be used by the application to implement the EUDI Wallet functionality.
+
+```mermaid
+graph TD;
+ A[eudi-lib-ios-wallet-kit]
+ B[eudi-lib-ios-wallet-storage] --> |Wallet Storage|A
+ C[eudi-lib-ios-iso18013-data-transfer] --> |Transfer Manager|A
+ D[eudi-lib-ios-openid4vci-swift] --> |OpenId4Vci Manager|A
+ E[eudi-lib-ios-siop-openid4vp-swift] --> |OpenId4Vp Manager|A
+ F[eudi-lib-ios-iso18013-security] --> |Mdoc Security|C
+ G[eudi-lib-ios-iso18013-data-model] --> |Mdoc Data Model|C
+ H[eudi-lib-ios-presentation-exchange-swift] --> E
+```
+
+The library provides the following functionality:
+
+- Document management
+ - [x] Storage encryption
+ - [x] Using iOS Secure Enclave for generating/storing documents' keypair
+ - [x] Enforcing device user authentication when retrieving documents' private keys
+- Document issuance
+ - [x] Support for OpenId4VCI document issuance
+ - [x] Authorization Code Flow
+ - [ ] Pre-authorization Code Flow
+ - [x] Support for mso_mdoc format
+ - [ ] Support for sd-jwt-vc format
+- Proximity document presentation
+ - [x] Support for ISO-18013-5 device retrieval
+ - [x] QR device engagement
+ - [x] BLE data transfer
+- Remote document presentation
+ - [x] OpenId4VP document transfer
+ - [x] For pre-registered verifiers
+ - [x] Dynamic registration of verifiers
+
+The library is written in Swift and is compatible with iOS 14 or higher. It is distributed as a Swift package
+and can be included in any iOS project.
+
+It is based on the following specifications:
- ISO/IEC 18013-5 – Published
- Presentation Exchange v2.0.0 - Published
- OpenID4VP – Draft 18
- SIOPv2 – Draft
+## Installation
+To use EUDI Wallet Kit, add the following dependency to your Package.swift:
+```swift
+dependencies: [
+ .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-kit.git", .upToNextMajor(from: "0.2.0"))
+]
+```
+
+Then add the Eudi Wallet package to your target's dependencies:
+```swift
+dependencies: [
+ .product(name: "EudiWalletKit", package: "eudi-lib-ios-wallet-kit"),
+]
+```
+
## Initialization
-The ``UserWallet`` class provides a unified API for the 2 user attestation presentation flows. It is initialized with a document storage manager instance. For SwiftUI apps, the wallet instance can be added as an ``environmentObject`` to be accessible from all views. A [KeyChain](Documentation/Reference/classes/KeyChainStorageService.md) and a [sample-data](Documentation/Reference/classes/DataSampleStorageService.md) implementation of document storage are available.
+The [EudiWallet](Documentation/Reference/classes/EudiWallet.md) class provides a unified API for the two user attestation presentation flows. It is initialized with a document storage manager instance. For SwiftUI apps, the wallet instance can be added as an ``environmentObject`` to be accessible from all views. A KeyChain implementation of document storage is available.
```swift
-let storageSvc = DataSampleStorageService()
-let wallet = UserWallet(storageService: storageSvc)
+let wallet = EudiWallet.standard
+wallet.userAuthenticationRequired = true
+wallet.trustedReaderCertificates = [...] // array of der certificate data
+wallet.openId4VpVerifierApiUri = "https:// ... verifier api uri ..."
+wallet.loadDocuments()
```
+## Storage Manager
+The read-only property ``storage`` is an instance of a [StorageManager](Documentation/Reference/classes/StorageManager.md)
+Currently the keychain implementation is used. It provides document management functionality using the iOS KeyChain.
+
+The storage model provides the following models for the supported well-known document types:
+
+|DocType|Model|
+|-------|-----|
+|eu.europa.ec.eudiw.pid.1|[EuPidModel](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-model/blob/main/Documentation/Reference/structs/EuPidModel.md)|
+|org.iso.18013.5.1.mDL|[IsoMdlModel](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-model/blob/main/Documentation/Reference/structs/IsoMdlModel.md)|
+
+For other document types the [GenericMdocModel](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-model/blob/main/Documentation/Reference/structs/GenericMdocModel.md) is provided.
+
## Presentation Service
The [presentation service protocol](Documentation/Reference/protocols/PresentationService.md) abstracts the presentation flow. The [BlePresentationService](Documentation/Reference/classes/BlePresentationService.md) and [OpenId4VpService](Documentation/Reference/classes/OpenId4VpService.md) classes implement the proximity and remote presentation flows respectively. The [PresentationSession](Documentation/Reference/classes/PresentationSession.md) class is used to wrap the presentation service and provide @Published properties for SwiftUI screens. The following example code demonstrates the initialization of a SwiftUI view with a new presentation session of a selected [flow type](Documentation/Reference/enums/FlowType.md).
```swift
-let session = PresentationSession(presentationService: userWallet.beginPresentation(flow: flow))
+let session = eudiWallet.beginPresentation(flow: flow)
+// pass the session to a SwiftUI view
ShareView(presentationSession: session)
```
-On view appearance the attestations are presented with the presentAttestations method. For the BLE (proximity) case the deviceEngagement property is populated with the QR code to be displayed on the holder device.
+On view appearance the attestations are presented with the receiveRequest method. For the BLE (proximity) case the deviceEngagement property is populated with the QR code to be displayed on the holder device.
```swift
.task {
- try? await presentationSession.presentAttestations()
+ if presentationSession.flow.isProximity { await presentationSession.startQrEngagement() }
+ _ = await presentationSession.receiveRequest()
}
```
-After the request is received the selectedRequestItems contains the requested attested items. It can be modified from the UI before the presentation is sent with user selective disclosure. Finally the presentation is sent with the following code:
+After the request is received the ``presentationSession.disclosedDocuments`` contains the requested attested items. The selected state of the items can be modified via UI binding. Finally, the response is sent with the following code:
```swift
-Task { try await presentationSession.sendResponse(userAccepted: true, itemsToSend: presentationSession.selectedRequestItems.docSelectedDictionary) }
+// Send the disclosed document items after biometric authentication (FaceID or TouchID)
+// if the user cancels biometric authentication, onCancel method is called
+ await presentationSession.sendResponse(userAccepted: true,
+ itemsToSend: presentationSession.disclosedDocuments.items, onCancel: { dismiss() }, onSuccess: {
+ if let url = $0 { presentSafariView(url) }
+ })
```
+
+## Issue document using OpenID4VCI
+
+The library provides the functionality to issue documents using OpenID4VCI. To issue a document
+using this functionality, EudiWallet must be property initialized.
+To issue a document using OpenID4VCI, you need to know the document's docType.
+If ``userAuthenticationRequired`` is true, user authentication is required. The authentication prompt message has localisation key "issue_document".
+```swift
+wallet.openID4VciIssuerUrl = "https://eudi.netcompany-intrasoft.com/pid-issuer"
+wallet.openID4VciClientId = "wallet-dev"
+wallet.openID4VciRedirectUri = "eudi-openid4ci://authorize/"
+do {
+ let doc = try await userWallet.issueDocument(docType: EuPidModel.euPidDocType, format: .cbor)
+ // document has been added to wallet storage, you can display it
+}
+catch {
+ // display error
+}
+
+```
+
+
+## Reference
+Detailed documentation is provided [here](Documentation/Reference/README.md)
+
### Dependencies
The detailed functionality of the wallet kit is implemented in the following Swift Packages: [MdocDataModel18013](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-model.git), [MdocSecurity18013](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-security.git), [MdocDataTransfer18013](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git) and
[SiopOpenID4VP](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git)
+ [OpenID4VCI](https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift)
### Sample application
-A sample application that demonstrates the usage of this library is [Holder Demo](https://github.com/eu-digital-identity-wallet/eudi-app-ios-iso18013-holder-demo).
+A sample application that demonstrates the usage of this library is [App Wallet UI](https://github.com/eu-digital-identity-wallet/eudi-app-ios-wallet-ui).
### Disclaimer
The released software is a initial development release version:
diff --git a/SECURITY.md b/SECURITY.md
index b61e270..cc207ab 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,62 +1,42 @@
# EU Digital Identity Wallet Vulnerability Disclosure Policy (VDP)
-At the European Commission, we treat the security of our Communication and Information Systems as a
-top priority, in line with Commission Decision EC 2017/46. However, vulnerabilities can never be
-completely eliminated, despite all efforts. If exploited, such vulnerabilities can harm the
-confidentiality, integrity or availability of the Commission's systems and of the information
-processed therein. To identify and remediate vulnerabilities as soon as possible, we value the input
-of external entities acting in good faith, and we encourage responsible vulnerability research and
-disclosure. This document sets out our definition of good faith in the context of finding and
-reporting vulnerabilities, as well as what you can expect from us in return.
+At the European Commission, we treat the security of our Communication and Information Systems as a top priority, in line with Commission Decision EC 2017/46. However, vulnerabilities can never be completely eliminated, despite all efforts. If exploited, such vulnerabilities can harm the confidentiality, integrity or availability of the Commission's systems and of the information processed therein. To identify and remediate vulnerabilities as soon as possible, we value the input of external entities acting in good faith, and we encourage responsible vulnerability research and disclosure. This document sets out our definition of good faith in the context of finding and reporting vulnerabilities, as well as what you can expect from us in return.
## Scope
- Architecture and Reference Framework
-- Source code in [eu-digital-identity-wallet](https://github.com/eu-digital-identity-wallet) public
- repositories
-
-## If you have identified a vulnerability, please do the following:
-
-* E-mail your findings to EC-VULNERABILITY-DISCLOSURE@ec.europa.eu, specifying whether or not you
- agree to your name or pseudonym being made publicly available as the discoverer of the problem.
-* Encrypt your findings using
- our [PGP key](https://sks.hnet.se/pks/lookup?search=EC-VULNERABILITY-DISCLOSURE%40ec.europa.eu&fingerprint=on&op=index)
- to prevent this critical information from falling into the wrong hands.
-* Provide us sufficient information to reproduce the problem so that we can resolve it as quickly as
- possible. Usually, the IP address or the URL of the affected system and a description of the
- vulnerability will be sufficient, but complex vulnerabilities may require further explanation in
- terms of technical information or potential proof-of-concept code.
-* Provide your report in English, preferably, or in any other official language of the European
- Union.
-* Inform us if you agree to make your name/pseudonym publicly available as the discoverer of the
- vulnerability.
+- Source code in [eu-digital-identity-wallet](https://github.com/eu-digital-identity-wallet) public repositories
+
+## If you have identified a vulnerability, please do the following
+
+- E-mail your findings to , specifying whether or not you agree to your name or pseudonym being made publicly available as the discoverer of the problem.
+- Encrypt your findings using our [PGP key](https://ec.europa.eu/assets/digit/pgpkey/ec-vulnerability-disclosure-pgp.txt) to prevent this critical information from falling into the wrong hands.
+- Provide us with sufficient information to reproduce the problem so that we can resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation in terms of technical information or potential proof-of-concept code.
+- Provide your report in English, preferably, or in any other official language of the European Union.
+- Inform us if you agree to make your name/pseudonym publicly available as the discoverer of the vulnerability.
## Please do not do the following
-* Do not take advantage of the vulnerability or problem you have discovered, for example by
- downloading more data than necessary to demonstrate the vulnerability, deleting, or modifying
- other people’s data.
-* Do not reveal any data downloaded during the discovery to any other parties.
-* Do not reveal the problem to others until it has been resolved.
-* Do not perform the following actions:
- * Placing malware (virus, worm, Trojan horse, etc.) within the system.
- * Reading, copying, modifying or deleting data from the system.
- * Making changes to the system.
- * Repeatedly accessing the system or sharing access with others.
- * Using any access obtained to attempt to access other systems.
- * Changing access rights for any other users.
- * Using automated scanning tools.
- * Using the so-called "brute force" of access to the system.
- * Using denial-of-service or social engineering (phishing, vishing, spam etc.).
-* Do not use attacks on physical security.
-
-## What we promise:
-
-* We will respond to your report within three business days with our evaluation of the report.
-* We will handle your report with strict confidentiality.
-* Where possible, we will inform you when the vulnerability has been remedied.
-* We will process the personal data that you provide (such as your e-mail address and name) in
- accordance with the applicable data protection legislation and will not pass on your personal
- details to third parties without your permission.
-* In the public information concerning the problem reported, we will publish your name as the
- discoverer of the problem if you have agreed to this in your initial e-mail
+- Do not take advantage of the vulnerability or problem you have discovered, for example, by downloading more data than necessary to demonstrate the vulnerability, deleting, or modifying other people’s data.
+- Do not reveal any data downloaded during the discovery to any other parties.
+- Do not reveal the problem to others until it has been resolved.
+- Do not perform the following actions:
+ - Placing malware (virus, worm, Trojan horse, etc.) within the system.
+ - Reading, copying, modifying or deleting data from the system.
+ - Making changes to the system.
+ - Repeatedly accessing the system or sharing access with others.
+ - Using any access obtained to attempt to access other systems.
+ - Changing access rights for any other users.
+ - Using automated scanning tools.
+ - Using the so-called "brute force" of access to the system.
+ - Using denial-of-service or social engineering (phishing, vishing, spam, etc.).
+- Do not use attacks on physical security.
+
+## What we promise
+
+- We will respond to your report within three business days with our evaluation of the report.
+
+- We will handle your report with strict confidentiality.
+- Where possible, we will inform you when the vulnerability has been remedied.
+- We will process the personal data that you provide (such as your e-mail address and name) in accordance with the applicable data protection legislation and will not pass on your personal details to third parties without your permission.
+- In the public information concerning the problem reported, we will publish your name as the discoverer of the problem if you have agreed to this in your initial e-mail
diff --git a/Sources/EudiWalletKit/EudiWallet.swift b/Sources/EudiWalletKit/EudiWallet.swift
new file mode 100644
index 0000000..7804f7e
--- /dev/null
+++ b/Sources/EudiWalletKit/EudiWallet.swift
@@ -0,0 +1,310 @@
+/*
+Copyright (c) 2023 European Commission
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import Foundation
+import MdocDataModel18013
+import MdocSecurity18013
+import MdocDataTransfer18013
+import WalletStorage
+import LocalAuthentication
+import CryptoKit
+import OpenID4VCI
+import SwiftCBOR
+
+/// User wallet implementation
+public final class EudiWallet: ObservableObject {
+ /// Storage manager instance
+ public private(set) var storage: StorageManager
+ var storageService: any WalletStorage.DataStorageService { storage.storageService }
+ /// Instance of the wallet initialized with default parameters
+ public static private(set) var standard: EudiWallet = EudiWallet()
+ /// Whether user authentication via biometrics or passcode is required before sending user data
+ public var userAuthenticationRequired: Bool
+ /// Trusted root certificates to validate the reader authentication certificate included in the proximity request
+ public var trustedReaderCertificates: [Data]?
+ /// Method to perform mdoc authentication (MAC or signature). Defaults to device MAC
+ public var deviceAuthMethod: DeviceAuthMethod = .deviceMac
+ /// OpenID4VP verifier api URL (used for preregistered clients)
+ public var verifierApiUri: String?
+ /// OpenID4VP verifier legal name (used for preregistered clients)
+ public var verifierLegalName: String?
+ /// OpenID4VCI issuer url
+ public var openID4VciIssuerUrl: String?
+ /// OpenID4VCI client id
+ public var openID4VciClientId: String?
+ /// OpenID4VCI redirect URI. Defaults to "eudi-openid4ci://authorize/"
+ public var openID4VciRedirectUri: String = "eudi-openid4ci://authorize"
+ /// Use iPhone Secure Enclave to protect keys and perform cryptographic operations. Defaults to true (if available)
+ public var useSecureEnclave: Bool { didSet { if !SecureEnclave.isAvailable { useSecureEnclave = false } } }
+ /// This variable can be used to set a custom URLSession for network requests.
+ public var urlSession: URLSession
+
+ /// Initialize a wallet instance. All parameters are optional.
+ public init(storageType: StorageType = .keyChain, serviceName: String = "eudiw", accessGroup: String? = nil, trustedReaderCertificates: [Data]? = nil, userAuthenticationRequired: Bool = true, verifierApiUri: String? = nil, openID4VciIssuerUrl: String? = nil, openID4VciClientId: String? = nil, openID4VciRedirectUri: String? = nil, urlSession: URLSession? = nil) {
+ let keyChainObj = KeyChainStorageService(serviceName: serviceName, accessGroup: accessGroup)
+ let storageService = switch storageType { case .keyChain:keyChainObj }
+ storage = StorageManager(storageService: storageService)
+ self.trustedReaderCertificates = trustedReaderCertificates
+ self.userAuthenticationRequired = userAuthenticationRequired
+ #if DEBUG
+ self.userAuthenticationRequired = false
+ #endif
+ self.verifierApiUri = verifierApiUri
+ self.openID4VciIssuerUrl = openID4VciIssuerUrl
+ self.openID4VciClientId = openID4VciClientId
+ if let openID4VciRedirectUri { self.openID4VciRedirectUri = openID4VciRedirectUri }
+ self.urlSession = urlSession ?? URLSession.shared
+ useSecureEnclave = SecureEnclave.isAvailable
+ }
+
+ /// Prepare issuing
+ /// - Parameters:
+ /// - docType: document type
+ /// - promptMessage: Prompt message for biometric authentication (optional)
+ /// - Returns: (Issue request key pair, vci service, unique id)
+ func prepareIssuing(docType: String?, promptMessage: String? = nil) async throws -> (IssueRequest, OpenId4VCIService, String) {
+ guard let openID4VciIssuerUrl else { throw WalletError(description: "issuer Url not defined")}
+ guard let openID4VciClientId else { throw WalletError(description: "clientId not defined")}
+ let id: String = UUID().uuidString
+ let issueReq = try await Self.authorizedAction(action: {
+ return try await beginIssueDocument(id: id, privateKeyType: useSecureEnclave ? .secureEnclaveP256 : .x963EncodedP256, saveToStorage: false)
+ }, disabled: !userAuthenticationRequired || docType == nil, dismiss: {}, localizedReason: promptMessage ?? NSLocalizedString("issue_document", comment: "").replacingOccurrences(of: "{docType}", with: NSLocalizedString(docType ?? "", comment: "")))
+ guard let issueReq else { throw LAError(.userCancel)}
+ let openId4VCIService = OpenId4VCIService(issueRequest: issueReq, credentialIssuerURL: openID4VciIssuerUrl, clientId: openID4VciClientId, callbackScheme: openID4VciRedirectUri, urlSession: urlSession)
+ return (issueReq, openId4VCIService, id)
+ }
+
+ /// Issue a document with the given docType using OpenId4Vci protocol
+ ///
+ /// If ``userAuthenticationRequired`` is true, user authentication is required. The authentication prompt message has localisation key "issue_document"
+ /// - Parameters:
+ /// - docType: Document type
+ /// - format: Optional format type. Defaults to cbor
+ /// - promptMessage: Prompt message for biometric authentication (optional)
+ /// - Returns: The document issued. It is saved in storage.
+ @discardableResult public func issueDocument(docType: String, format: DataFormat = .cbor, promptMessage: String? = nil) async throws -> WalletStorage.Document {
+ let (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: docType, promptMessage: promptMessage)
+ let data = try await openId4VCIService.issueDocument(docType: docType, format: format, useSecureEnclave: useSecureEnclave)
+ return try await finalizeIssuing(id: id, data: data, docType: docType, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService)
+ }
+
+ func finalizeIssuing(id: String, data: Data, docType: String?, format: DataFormat, issueReq: IssueRequest, openId4VCIService: OpenId4VCIService) async throws -> WalletStorage.Document {
+ let iss = IssuerSigned(data: [UInt8](data))
+ guard let ddt = DocDataType(rawValue: format.rawValue) else { throw WalletError(description: "Invalid format \(format.rawValue)") }
+ let docTypeToSave = docType ?? (format == .cbor ? iss?.issuerAuth.mso.docType : nil)
+ var dataToSave: Data? = data
+ guard let docTypeToSave else { throw WalletError(description: "Unknown document type") }
+ guard let dataToSave else { throw WalletError(description: "Issued data cannot be recognized") }
+ var issued: WalletStorage.Document
+ if !openId4VCIService.usedSecureEnclave {
+ issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date())
+ } else {
+ issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .secureEnclaveP256, privateKey: issueReq.keyData, createdAt: Date())
+ }
+ try issueReq.saveToStorage(storage.storageService)
+ try endIssueDocument(issued)
+ await storage.appendDocModel(issued)
+ await storage.refreshPublishedVars()
+ return issued
+ }
+
+ /// Resolve OpenID4VCI offer URL document types. Resolved offer metadata are cached
+ /// - Parameters:
+ /// - uriOffer: url with offer
+ /// - format: data format
+ /// - useSecureEnclave: whether to use secure enclave (if supported)
+ /// - Returns: Offered issue information model
+ public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> OfferedIssueModel {
+ let (_, openId4VCIService, _) = try await prepareIssuing(docType: nil)
+ return try await openId4VCIService.resolveOfferDocTypes(uriOffer: uriOffer, format: format)
+ }
+
+ /// Issue documents by offer URI.
+ /// - Parameters:
+ /// - offerUri: url with offer
+ /// - docTypes: doc types to be issued
+ /// - txCodeValue: Transaction code given to user
+ /// - format: data format
+ /// - promptMessage: prompt message for biometric authentication (optional)
+ /// - useSecureEnclave: whether to use secure enclave (if supported)
+ /// - claimSet: claim set (optional)
+ /// - Returns: Array of issued and stored documents
+ public func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], txCodeValue: String? = nil, format: DataFormat = .cbor, promptMessage: String? = nil, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [WalletStorage.Document] {
+ guard format == .cbor else { throw fatalError("jwt format not implemented") }
+ var (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: docTypes.map(\.docType).joined(separator: ", "), promptMessage: promptMessage)
+ let docsData = try await openId4VCIService.issueDocumentsByOfferUrl(offerUri: offerUri, docTypes: docTypes, txCodeValue: txCodeValue, format: format, useSecureEnclave: useSecureEnclave, claimSet: claimSet)
+ var documents = [WalletStorage.Document]()
+ for (i, docData) in docsData.enumerated() {
+ if i > 0 { (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: nil) }
+ openId4VCIService.usedSecureEnclave = useSecureEnclave && SecureEnclave.isAvailable
+ documents.append(try await finalizeIssuing(id: id, data: docData, docType: nil, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService))
+ }
+ return documents
+ }
+ /// Begin issuing a document by generating an issue request
+ ///
+ /// - Parameters:
+ /// - id: Document identifier
+ /// - issuer: Issuer function
+ public func beginIssueDocument(id: String, privateKeyType: PrivateKeyType = .secureEnclaveP256, saveToStorage: Bool = true) async throws -> IssueRequest {
+ let request = try IssueRequest(id: id, privateKeyType: privateKeyType)
+ if saveToStorage { try request.saveToStorage(storage.storageService) }
+ return request
+ }
+
+ /// End issuing by saving the issuing document (and its private key) in storage
+ /// - Parameter issued: The issued document
+ public func endIssueDocument(_ issued: WalletStorage.Document) throws {
+ try storage.storageService.saveDocumentData(issued, dataToSaveType: .doc, dataType: issued.docDataType.rawValue, allowOverwrite: true)
+ try storage.storageService.saveDocumentData(issued, dataToSaveType: .key, dataType: issued.privateKeyType!.rawValue, allowOverwrite: true)
+ }
+
+ /// Load documents from storage
+ ///
+ /// Calls ``storage`` loadDocuments
+ /// - Returns: An array of ``WalletStorage.Document`` objects
+ @discardableResult public func loadDocuments() async throws -> [WalletStorage.Document]? {
+ return try await storage.loadDocuments()
+ }
+
+ /// Delete all documents from storage
+ ///
+ /// Calls ``storage`` loadDocuments
+ /// - Returns: An array of ``WalletStorage.Document`` objects
+ public func deleteDocuments() async throws {
+ return try await storage.deleteDocuments()
+ }
+
+ /// Load sample data from json files
+ ///
+ /// The mdoc data are stored in wallet storage as documents
+ /// - Parameter sampleDataFiles: Names of sample files provided in the app bundle
+ public func loadSampleData(sampleDataFiles: [String]? = nil) async throws {
+ try? storageService.deleteDocuments()
+ let docSamples = (sampleDataFiles ?? ["EUDI_sample_data"]).compactMap { Data(name:$0) }
+ .compactMap(SignUpResponse.decomposeCBORSignupResponse(data:)).flatMap {$0}
+ .map { Document(docType: $0.docType, docDataType: .cbor, data: $0.issData, privateKeyType: .x963EncodedP256, privateKey: $0.pkData, createdAt: Date.distantPast, modifiedAt: nil) }
+ do {
+ for docSample in docSamples {
+ try storageService.saveDocument(docSample, allowOverwrite: true)
+ }
+ try await storage.loadDocuments()
+ } catch {
+ await storage.setError(error)
+ throw WalletError(description: error.localizedDescription, code: (error as NSError).code)
+ }
+ }
+
+ /// Prepare Service Data Parameters
+ /// - Parameters:
+ /// - docType: docType of documents to present (optional)
+ /// - dataFormat: Exchanged data ``Format`` type
+ /// - Returns: A data dictionary that can be used to initialize a presentation service
+ public func prepareServiceDataParameters(docType: String? = nil, dataFormat: DataFormat = .cbor ) throws -> [String : Any] {
+ var parameters: [String: Any]
+ switch dataFormat {
+ case .cbor:
+ guard var docs = try storageService.loadDocuments(), docs.count > 0 else { throw WalletError(description: "No documents found") }
+ if let docType { docs = docs.filter { $0.docType == docType} }
+ if let docType { guard docs.count > 0 else { throw WalletError(description: "No documents of type \(docType) found") } }
+ let cborsWithKeys = docs.compactMap { $0.getCborData() }
+ guard cborsWithKeys.count > 0 else { throw WalletError(description: "Documents decode error") }
+ parameters = [InitializeKeys.document_signup_issuer_signed_obj.rawValue: Dictionary(uniqueKeysWithValues: cborsWithKeys.map(\.iss)), InitializeKeys.device_private_key_obj.rawValue: Dictionary(uniqueKeysWithValues: cborsWithKeys.map(\.dpk))]
+ if let trustedReaderCertificates { parameters[InitializeKeys.trusted_certificates.rawValue] = trustedReaderCertificates }
+ parameters[InitializeKeys.device_auth_method.rawValue] = deviceAuthMethod.rawValue
+ default:
+ fatalError("jwt format not implemented")
+ }
+ return parameters
+ }
+
+ /// Begin attestation presentation to a verifier
+ /// - Parameters:
+ /// - flow: Presentation ``FlowType`` instance
+ /// - docType: DocType of documents to present (optional)
+ /// - dataFormat: Exchanged data ``Format`` type
+ /// - Returns: A presentation session instance,
+ public func beginPresentation(flow: FlowType, docType: String? = nil, dataFormat: DataFormat = .cbor) -> PresentationSession {
+ do {
+ let parameters = try prepareServiceDataParameters(docType: docType, dataFormat: dataFormat)
+ let docIdAndTypes = storage.getDocIdsToTypes()
+ switch flow {
+ case .ble:
+ let bleSvc = try BlePresentationService(parameters: parameters)
+ return PresentationSession(presentationService: bleSvc, docIdAndTypes: docIdAndTypes, userAuthenticationRequired: userAuthenticationRequired)
+ case .openid4vp(let qrCode):
+ let openIdSvc = try OpenId4VpService(parameters: parameters, qrCode: qrCode, openId4VpVerifierApiUri: self.verifierApiUri, openId4VpVerifierLegalName: self.verifierLegalName, urlSession: urlSession)
+ return PresentationSession(presentationService: openIdSvc, docIdAndTypes: docIdAndTypes, userAuthenticationRequired: userAuthenticationRequired)
+ default:
+ return PresentationSession(presentationService: FaultPresentationService(error: PresentationSession.makeError(str: "Use beginPresentation(service:)")), docIdAndTypes: docIdAndTypes, userAuthenticationRequired: false)
+ }
+ } catch {
+ return PresentationSession(presentationService: FaultPresentationService(error: error), docIdAndTypes: [:], userAuthenticationRequired: false)
+ }
+ }
+
+ /// Begin attestation presentation to a verifier
+ /// - Parameters:
+ /// - service: A ``PresentationService`` instance
+ /// - docType: DocType of documents to present (optional)
+ /// - dataFormat: Exchanged data ``Format`` type
+ /// - Returns: A presentation session instance,
+ public func beginPresentation(service: any PresentationService) -> PresentationSession {
+ PresentationSession(presentationService: service, docIdAndTypes: storage.getDocIdsToTypes(), userAuthenticationRequired: userAuthenticationRequired)
+ }
+
+ @MainActor
+ /// Perform an action after user authorization via TouchID/FaceID/Passcode
+ /// - Parameters:
+ /// - dismiss: Action to perform if the user cancels authorization
+ /// - action: Action to perform after user authorization
+ public static func authorizedAction(action: () async throws -> T, disabled: Bool, dismiss: () -> Void, localizedReason: String) async throws -> T? {
+ return try await authorizedAction(isFallBack: false, action: action, disabled: disabled, dismiss: dismiss, localizedReason: localizedReason)
+ }
+
+ /// Wrap an action with TouchID or FaceID authentication
+ /// - Parameters:
+ /// - isFallBack: true if fallback (ask for pin code)
+ /// - dismiss: action to dismiss current page
+ /// - action: action to perform after authentication
+ static func authorizedAction(isFallBack: Bool = false, action: () async throws -> T, disabled: Bool, dismiss: () -> Void, localizedReason: String) async throws -> T? {
+ guard !disabled else {
+ return try await action()
+ }
+ let context = LAContext()
+ var error: NSError?
+ let policy: LAPolicy = .deviceOwnerAuthentication
+ if context.canEvaluatePolicy(policy, error: &error) {
+ do {
+ let success = try await context.evaluatePolicy(policy, localizedReason: localizedReason)
+ if success {
+ return try await action()
+ }
+ else { dismiss()}
+ } catch let laError as LAError {
+ if !isFallBack, laError.code == .userFallback {
+ return try await authorizedAction(isFallBack: true, action: action, disabled: disabled, dismiss: dismiss, localizedReason: localizedReason)
+ } else {
+ dismiss()
+ return nil
+ }
+ }
+ } else if let error {
+ throw WalletError(description: error.localizedDescription, code: error.code)
+ }
+ return nil
+ }
+}
diff --git a/Sources/eudi-lib-ios-wallet-kit/EudiWalletKit.swift b/Sources/EudiWalletKit/EudiWalletKit.swift
similarity index 100%
rename from Sources/eudi-lib-ios-wallet-kit/EudiWalletKit.swift
rename to Sources/EudiWalletKit/EudiWalletKit.swift
diff --git a/Sources/eudi-lib-ios-wallet-kit/Storage/DataStorageService.swift b/Sources/EudiWalletKit/Extensions.swift
similarity index 62%
rename from Sources/eudi-lib-ios-wallet-kit/Storage/DataStorageService.swift
rename to Sources/EudiWalletKit/Extensions.swift
index c533c36..38cb1b1 100644
--- a/Sources/eudi-lib-ios-wallet-kit/Storage/DataStorageService.swift
+++ b/Sources/EudiWalletKit/Extensions.swift
@@ -5,22 +5,30 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-*/
+Created on 09/11/2023
+*/
import Foundation
+import OpenID4VCI
+extension String {
+ public func translated() -> String {
+ NSLocalizedString(self, comment: "")
+ }
+}
-/// Data storage protocol
-public protocol DataStorageService {
- func loadDocument(id: String) throws -> Data
- func saveDocument(id: String, value: inout Data) throws
- func deleteDocument(id: String) throws
- static var defaultId: String { get set }
+extension Array where Element == Display {
+ func getName() -> String? {
+ (first(where: { $0.locale == Locale.current }) ?? first)?.name
+ }
}
+
+
+
diff --git a/Sources/eudi-lib-ios-wallet-kit/Services/BlePresentationService.swift b/Sources/EudiWalletKit/Services/BlePresentationService.swift
similarity index 89%
rename from Sources/eudi-lib-ios-wallet-kit/Services/BlePresentationService.swift
rename to Sources/EudiWalletKit/Services/BlePresentationService.swift
index c17c7b9..751ea17 100644
--- a/Sources/eudi-lib-ios-wallet-kit/Services/BlePresentationService.swift
+++ b/Sources/EudiWalletKit/Services/BlePresentationService.swift
@@ -21,16 +21,16 @@ import MdocDataTransfer18013
/// Implements proximity attestation presentation with QR to BLE data transfer
/// Implementation is based on the ISO/IEC 18013-5 specification
-class BlePresentationService : PresentationService {
+public class BlePresentationService : PresentationService {
var bleServerTransfer: MdocGattServer
- var status: TransferStatus = .initializing
- var continuationQrCode: CheckedContinuation?
+ public var status: TransferStatus = .initializing
+ var continuationQrCode: CheckedContinuation?
var continuationRequest: CheckedContinuation<[String: Any], Error>?
var continuationResponse: CheckedContinuation?
var handleSelected: ((Bool, RequestItems?) -> Void)?
- var deviceEngagement: Data?
+ var deviceEngagement: String?
var request: [String: Any]?
- var flow: FlowType { .ble }
+ public var flow: FlowType { .ble }
public init(parameters: [String: Any]) throws {
bleServerTransfer = try MdocGattServer(parameters: parameters)
@@ -41,7 +41,7 @@ class BlePresentationService : PresentationService {
/// The holder app should present the returned code to the verifier
/// - Returns: The image data for the QR code
- public func generateQRCode() async throws -> Data? {
+ public func startQrEngagement() async throws -> String? {
return try await withCheckedThrowingContinuation { c in
continuationQrCode = c
self.bleServerTransfer.performDeviceEngagement()
@@ -62,14 +62,13 @@ class BlePresentationService : PresentationService {
/// - Parameters:
/// - userAccepted: True if user accepted to send the response
/// - itemsToSend: The selected items to send organized in document types and namespaces
- public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws {
+ public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)? ) async throws {
return try await withCheckedThrowingContinuation { c in
continuationResponse = c
handleSelected?(userAccepted, itemsToSend)
handleSelected = nil
}
}
-
}
/// handle events from underlying BLE service
@@ -80,7 +79,7 @@ extension BlePresentationService: MdocOfflineDelegate {
status = if let st = TransferStatus(rawValue: newStatus.rawValue) { st } else { .error }
switch newStatus {
case .qrEngagementReady:
- if let qrCode = self.bleServerTransfer.qrCodeImageData {
+ if let qrCode = self.bleServerTransfer.qrCodePayload {
deviceEngagement = qrCode
continuationQrCode?.resume(returning: qrCode)
continuationQrCode = nil
diff --git a/Sources/eudi-lib-ios-wallet-kit/Services/Enumerations.swift b/Sources/EudiWalletKit/Services/Enumerations.swift
similarity index 80%
rename from Sources/eudi-lib-ios-wallet-kit/Services/Enumerations.swift
rename to Sources/EudiWalletKit/Services/Enumerations.swift
index 10b51bc..bba0ff9 100644
--- a/Sources/eudi-lib-ios-wallet-kit/Services/Enumerations.swift
+++ b/Sources/EudiWalletKit/Services/Enumerations.swift
@@ -23,14 +23,20 @@ public enum FlowType: Codable, Hashable {
case ble
case openid4vp(qrCode: Data)
+ case other
/// True if proximity flow type (currently ``ble``)
public var isProximity: Bool { switch self { case .ble: true; default: false } }
+ public var qrCode: Data? { if case let .openid4vp(qrCode) = self { qrCode} else { nil} }
}
/// Data format of the exchanged data
-public enum DataFormat {
- case cbor
- case jwt
+public enum DataFormat: String {
+ case cbor = "cbor"
+ case sdjwt = "sdjwt"
+}
+
+public enum StorageType {
+ case keyChain
}
diff --git a/Sources/eudi-lib-ios-wallet-kit/Services/FaultPresentationService.swift b/Sources/EudiWalletKit/Services/FaultPresentationService.swift
similarity index 59%
rename from Sources/eudi-lib-ios-wallet-kit/Services/FaultPresentationService.swift
rename to Sources/EudiWalletKit/Services/FaultPresentationService.swift
index f9aecc0..9529d5d 100644
--- a/Sources/eudi-lib-ios-wallet-kit/Services/FaultPresentationService.swift
+++ b/Sources/EudiWalletKit/Services/FaultPresentationService.swift
@@ -15,26 +15,31 @@ limitations under the License.
*/
import Foundation
+import MdocDataTransfer18013
/// Fault presentation service. Used to communicate error state to the user
-class FaultPresentationService: PresentationService {
- var status: TransferStatus = .error
- var flow: FlowType = .ble
+public class FaultPresentationService: PresentationService {
+ public var status: TransferStatus = .error
+ public var flow: FlowType = .other
var error: Error
- init(error: Error) {
+ public init(msg: String) {
+ self.error = PresentationSession.makeError(str: msg)
+ }
+
+ public init(error: Error) {
self.error = error
}
- func generateQRCode() async throws -> Data? {
+ public func startQrEngagement() async throws -> String? {
throw error
}
- func receiveRequest() async throws -> [String : Any] {
+ public func receiveRequest() async throws -> [String : Any] {
throw error
}
- func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws {
+ public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)?) async throws{
throw error
}
diff --git a/Sources/EudiWalletKit/Services/OpenId4VciService.swift b/Sources/EudiWalletKit/Services/OpenId4VciService.swift
new file mode 100644
index 0000000..946b859
--- /dev/null
+++ b/Sources/EudiWalletKit/Services/OpenId4VciService.swift
@@ -0,0 +1,401 @@
+/*
+ * Copyright (c) 2023 European Commission
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+import OpenID4VCI
+import JOSESwift
+import MdocDataModel18013
+import AuthenticationServices
+import Logging
+import CryptoKit
+import Security
+import WalletStorage
+
+public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContextProviding {
+ let issueReq: IssueRequest
+ let credentialIssuerURL: String
+ var privateKey: SecKey!
+ var publicKey: SecKey!
+ var bindingKey: BindingKey!
+ var usedSecureEnclave: Bool!
+ let logger: Logger
+ let config: OpenId4VCIConfig
+ let alg = JWSAlgorithm(.ES256)
+ static var metadataCache = [String: CredentialOffer]()
+ var urlSession: URLSession
+
+ init(issueRequest: IssueRequest, credentialIssuerURL: String, clientId: String, callbackScheme: String, urlSession: URLSession) {
+ self.issueReq = issueRequest
+ self.credentialIssuerURL = credentialIssuerURL
+ self.urlSession = urlSession
+ logger = Logger(label: "OpenId4VCI")
+ config = .init(clientId: clientId, authFlowRedirectionURI: URL(string: callbackScheme)!)
+ }
+
+ fileprivate func initSecurityKeys(_ useSecureEnclave: Bool) throws {
+ usedSecureEnclave = useSecureEnclave && SecureEnclave.isAvailable
+ if !usedSecureEnclave {
+ let key = try P256.KeyAgreement.PrivateKey(x963Representation: issueReq.keyData)
+ privateKey = try key.toSecKey()
+ } else {
+ let seKey = try SecureEnclave.P256.KeyAgreement.PrivateKey(dataRepresentation: issueReq.keyData)
+ privateKey = try seKey.toSecKey()
+ }
+ publicKey = try KeyController.generateECDHPublicKey(from: privateKey)
+ let publicKeyJWK = try ECPublicKey(publicKey: publicKey,additionalParameters: ["alg": alg.name, "use": "sig", "kid": UUID().uuidString])
+ bindingKey = .jwk(algorithm: alg, jwk: publicKeyJWK, privateKey: privateKey, issuer: config.clientId)
+ }
+
+ /// Issue a document with the given `docType` using OpenId4Vci protocol
+ /// - Parameters:
+ /// - docType: the docType of the document to be issued
+ /// - format: format of the exchanged data
+ /// - useSecureEnclave: use secure enclave to protect the private key
+ /// - Returns: The data of the document
+ public func issueDocument(docType: String, format: DataFormat, useSecureEnclave: Bool = true) async throws -> Data {
+ try initSecurityKeys(useSecureEnclave)
+ let str = try await issueByDocType(docType, format: format)
+ guard let data = Data(base64URLEncoded: str) else { throw OpenId4VCIError.dataNotValid }
+ return data
+ }
+
+ /// Resolve issue offer and return available document metadata
+ /// - Parameters:
+ /// - uriOffer: Uri of the offer (from a QR or a deep link)
+ /// - format: format of the exchanged data
+ /// - Returns: The data of the document
+ public func resolveOfferDocTypes(uriOffer: String, format: DataFormat = .cbor) async throws -> OfferedIssueModel {
+ let result = await CredentialOfferRequestResolver(fetcher: Fetcher(session: urlSession), credentialIssuerMetadataResolver: CredentialIssuerMetadataResolver(fetcher: Fetcher(session: urlSession)), authorizationServerMetadataResolver: AuthorizationServerMetadataResolver(oidcFetcher: Fetcher(session: urlSession), oauthFetcher: Fetcher(session: urlSession))).resolve(source: try .init(urlString: uriOffer))
+ switch result {
+ case .success(let offer):
+ let code: Grants.PreAuthorizedCode? = switch offer.grants { case .preAuthorizedCode(let preAuthorizedCode): preAuthorizedCode; case .both(_, let preAuthorizedCode): preAuthorizedCode; case .authorizationCode(_), .none: nil }
+ Self.metadataCache[uriOffer] = offer
+ let credentialInfo = try getCredentialIdentifiers(credentialsSupported: offer.credentialIssuerMetadata.credentialsSupported.filter { offer.credentialConfigurationIdentifiers.contains($0.key) }, format: format)
+ return OfferedIssueModel(issuerName: offer.credentialIssuerIdentifier.url.absoluteString, docModels: credentialInfo.map(\.offered), txCodeSpec: code?.txCode)
+ case .failure(let error):
+ throw WalletError(description: "Unable to resolve credential offer: \(error.localizedDescription)")
+ }
+ }
+
+ public func getIssuer(offer: CredentialOffer) throws -> Issuer {
+ try Issuer(authorizationServerMetadata: offer.authorizationServerMetadata, issuerMetadata: offer.credentialIssuerMetadata, config: config, parPoster: Poster(session: urlSession), tokenPoster: Poster(session: urlSession), requesterPoster: Poster(session: urlSession), deferredRequesterPoster: Poster(session: urlSession), notificationPoster: Poster(session: urlSession))
+ }
+
+ public func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], txCodeValue: String?, format: DataFormat, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [Data] {
+ guard format == .cbor else { throw fatalError("jwt format not implemented") }
+ try initSecurityKeys(useSecureEnclave)
+ guard let offer = Self.metadataCache[offerUri] else { throw WalletError(description: "offerUri not resolved. resolveOfferDocTypes must be called first")}
+ let credentialInfo = docTypes.compactMap { try? getCredentialIdentifier(credentialsSupported: offer.credentialIssuerMetadata.credentialsSupported, docType: $0.docType, format: format)
+ }
+ let code: Grants.PreAuthorizedCode? = switch offer.grants { case .preAuthorizedCode(let preAuthorizedCode): preAuthorizedCode; case .both(_, let preAuthorizedCode): preAuthorizedCode; case .authorizationCode(_), .none: nil }
+ let txCodeSpec: TxCode? = code?.txCode
+ let preAuthorizedCode: String? = code?.preAuthorizedCode
+ let issuer = try getIssuer(offer: offer)
+ if preAuthorizedCode != nil && txCodeSpec != nil && txCodeValue == nil { throw WalletError(description: "A transaction code is required for this offer") }
+ let authorized = if let preAuthorizedCode, let txCodeValue, let authCode = try? IssuanceAuthorization(preAuthorizationCode: preAuthorizedCode, txCode: txCodeSpec) { try await issuer.authorizeWithPreAuthorizationCode(credentialOffer: offer, authorizationCode: authCode, clientId: config.clientId, transactionCode: txCodeValue).get() } else { try await authorizeRequestWithAuthCodeUseCase(issuer: issuer, offer: offer) }
+ let data = await credentialInfo.asyncCompactMap {
+ do {
+ logger.info("Starting issuing with identifer \($0.identifier.value) and scope \($0.scope)")
+ let str = try await issueOfferedCredentialWithProof(authorized, offer: offer, issuer: issuer, credentialConfigurationIdentifier: $0.identifier, claimSet: claimSet)
+ logger.info("Credential str:\n\(str)")
+ return Data(base64URLEncoded: str)
+ } catch {
+ logger.error("Failed to issue document with scope \($0.scope)")
+ logger.info("Exception: \(error)")
+ return nil
+ }
+ }
+ Self.metadataCache.removeValue(forKey: offerUri)
+ return data
+ }
+
+ func issueByDocType(_ docType: String, format: DataFormat, claimSet: ClaimSet? = nil) async throws -> String {
+ let credentialIssuerIdentifier = try CredentialIssuerId(credentialIssuerURL)
+ let issuerMetadata = await CredentialIssuerMetadataResolver(fetcher: Fetcher(session: urlSession)).resolve(source: .credentialIssuer(credentialIssuerIdentifier))
+ switch issuerMetadata {
+ case .success(let metaData):
+ if let authorizationServer = metaData?.authorizationServers.first, let metaData {
+ let authServerMetadata = await AuthorizationServerMetadataResolver(oidcFetcher: Fetcher(session: urlSession), oauthFetcher: Fetcher(session: urlSession)).resolve(url: authorizationServer)
+ let (credentialConfigurationIdentifier, _) = try getCredentialIdentifier(credentialsSupported: metaData.credentialsSupported, docType: docType, format: format)
+ let offer = try CredentialOffer(credentialIssuerIdentifier: credentialIssuerIdentifier, credentialIssuerMetadata: metaData, credentialConfigurationIdentifiers: [credentialConfigurationIdentifier], grants: nil, authorizationServerMetadata: try authServerMetadata.get())
+ // Authorize with auth code flow
+ let issuer = try getIssuer(offer: offer)
+ let authorized = try await authorizeRequestWithAuthCodeUseCase(issuer: issuer, offer: offer)
+ return try await issueOfferedCredentialInternal(authorized, issuer: issuer, credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet)
+ } else {
+ throw WalletError(description: "Invalid authorization server")
+ }
+ case .failure:
+ throw WalletError(description: "Invalid issuer metadata")
+ }
+ }
+
+ private func issueOfferedCredentialInternal(_ authorized: AuthorizedRequest, issuer: Issuer, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet?) async throws -> String {
+ switch authorized {
+ case .noProofRequired:
+ return try await noProofRequiredSubmissionUseCase(issuer: issuer, noProofRequiredState: authorized, credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet)
+ case .proofRequired:
+ return try await proofRequiredSubmissionUseCase(issuer: issuer, authorized: authorized, credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet)
+ }
+ }
+
+ private func issueOfferedCredentialWithProof(_ authorized: AuthorizedRequest, offer: CredentialOffer, issuer: Issuer, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> String {
+ let issuerMetadata = offer.credentialIssuerMetadata
+ guard issuerMetadata.credentialsSupported.keys.contains(where: { $0.value == credentialConfigurationIdentifier.value }) else {
+ throw WalletError(description: "Cannot find credential identifier \(credentialConfigurationIdentifier.value) in offer")
+ }
+ return try await issueOfferedCredentialInternal(authorized, issuer: issuer, credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet)
+ }
+
+ func getCredentialIdentifier(credentialsSupported: [CredentialConfigurationIdentifier: CredentialSupported], docType: String, format: DataFormat) throws -> (identifier: CredentialConfigurationIdentifier, scope: String) {
+ switch format {
+ case .cbor:
+ guard let credential = credentialsSupported.first(where: { if case .msoMdoc(let msoMdocCred) = $0.value, msoMdocCred.docType == docType { true } else { false } }), case let .msoMdoc(msoMdocConf) = credential.value, let scope = msoMdocConf.scope else {
+ logger.error("No credential for docType \(docType). Currently supported credentials: \(credentialsSupported.values)")
+ throw WalletError(description: "Issuer does not support doc type\(docType)")
+ }
+ logger.info("Currently supported cryptographic suites: \(msoMdocConf.credentialSigningAlgValuesSupported)")
+ return (identifier: credential.key, scope: scope)
+ default:
+ throw WalletError(description: "Format \(format) not yet supported")
+ }
+ }
+
+ func getCredentialIdentifier(credentialsSupported: [CredentialConfigurationIdentifier: CredentialSupported], scope: String, format: DataFormat) throws -> (identifier: CredentialConfigurationIdentifier, scope: String) {
+ switch format {
+ case .cbor:
+ guard let credential = credentialsSupported.first(where: { if case .msoMdoc(let msoMdocCred) = $0.value, msoMdocCred.scope == scope { true } else { false } }), case let .msoMdoc(msoMdocConf) = credential.value, let scope = msoMdocConf.scope else {
+ logger.error("No credential for scope \(scope). Currently supported credentials: \(credentialsSupported.values)")
+ throw WalletError(description: "Issuer does not support scope \(scope)")
+ }
+ logger.info("Currently supported cryptographic suites: \(msoMdocConf.credentialSigningAlgValuesSupported)")
+ return (identifier: credential.key, scope: scope)
+ default:
+ throw WalletError(description: "Format \(format) not yet supported")
+ }
+ }
+
+ func getCredentialIdentifiers(credentialsSupported: [CredentialConfigurationIdentifier: CredentialSupported], format: DataFormat) throws -> [(identifier: CredentialConfigurationIdentifier, scope: String, offered: OfferedDocModel)] {
+ switch format {
+ case .cbor:
+ let credentialInfos = credentialsSupported.compactMap {
+ if case .msoMdoc(let msoMdocCred) = $0.value, let scope = msoMdocCred.scope, case let offered = OfferedDocModel(docType: msoMdocCred.docType, displayName: msoMdocCred.display.getName() ?? msoMdocCred.docType) { (identifier: $0.key, scope: scope,offered: offered) } else { nil } }
+ return credentialInfos
+ default:
+ throw WalletError(description: "Format \(format) not yet supported")
+ }
+ }
+
+ private func authorizeRequestWithAuthCodeUseCase(issuer: Issuer, offer: CredentialOffer) async throws -> AuthorizedRequest {
+ var pushedAuthorizationRequestEndpoint = ""
+ if case let .oidc(metaData) = offer.authorizationServerMetadata, let endpoint = metaData.pushedAuthorizationRequestEndpoint {
+ pushedAuthorizationRequestEndpoint = endpoint
+ } else if case let .oauth(metaData) = offer.authorizationServerMetadata, let endpoint = metaData.pushedAuthorizationRequestEndpoint {
+ pushedAuthorizationRequestEndpoint = endpoint
+ }
+ guard !pushedAuthorizationRequestEndpoint.isEmpty else { throw WalletError(description: "pushed Authorization Request Endpoint is nil") }
+ logger.info("--> [AUTHORIZATION] Placing PAR to AS server's endpoint \(pushedAuthorizationRequestEndpoint)")
+ let parPlaced = try await issuer.pushAuthorizationCodeRequest(credentialOffer: offer)
+
+ if case let .success(request) = parPlaced, case let .par(parRequested) = request {
+ logger.info("--> [AUTHORIZATION] Placed PAR. Get authorization code URL is: \(parRequested.getAuthorizationCodeURL)")
+ let authorizationCode = try await loginUserAndGetAuthCode(
+ getAuthorizationCodeUrl: parRequested.getAuthorizationCodeURL.url) ?? { throw WalletError(description: "Could not retrieve authorization code") }()
+ logger.info("--> [AUTHORIZATION] Authorization code retrieved")
+ let unAuthorized = await issuer.handleAuthorizationCode(parRequested: request, authorizationCode: .authorizationCode(authorizationCode: authorizationCode))
+ switch unAuthorized {
+ case .success(let request):
+ let authorizedRequest = await issuer.requestAccessToken(authorizationCode: request)
+ if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _) = authorized {
+ logger.info("--> [AUTHORIZATION] Authorization code exchanged with access token : \(token.accessToken)")
+ return authorized
+ }
+ case .failure(let error):
+ throw WalletError(description: error.localizedDescription)
+ }
+ } else if case let .failure(failure) = parPlaced {
+ throw WalletError(description: "Authorization error: \(failure.localizedDescription)")
+ }
+ throw WalletError(description: "Failed to get push authorization code request")
+ }
+
+ private func noProofRequiredSubmissionUseCase(issuer: Issuer, noProofRequiredState: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil) async throws -> String {
+ switch noProofRequiredState {
+ case .noProofRequired:
+ let payload: IssuanceRequestPayload = .configurationBased(credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet)
+ let responseEncryptionSpecProvider = { Issuer.createResponseEncryptionSpec($0) }
+ let requestOutcome = try await issuer.requestSingle(noProofRequest: noProofRequiredState, requestPayload: payload, responseEncryptionSpecProvider: responseEncryptionSpecProvider)
+ switch requestOutcome {
+ case .success(let request):
+ switch request {
+ case .success(let response):
+ if let result = response.credentialResponses.first {
+ switch result {
+ case .deferred(let transactionId):
+ return try await deferredCredentialUseCase(issuer: issuer, authorized: noProofRequiredState, transactionId: transactionId)
+ case .issued(_, let credential, _):
+ return credential
+ }
+ } else {
+ throw WalletError(description: "No credential response results available")
+ }
+ case .invalidProof(let cNonce, _):
+ return try await proofRequiredSubmissionUseCase(issuer: issuer, authorized: noProofRequiredState.handleInvalidProof(cNonce: cNonce), credentialConfigurationIdentifier: credentialConfigurationIdentifier)
+ case .failed(error: let error):
+ throw WalletError(description: error.localizedDescription)
+ }
+ case .failure(let error):
+ throw WalletError(description: error.localizedDescription)
+ }
+ default: throw WalletError(description: "Illegal noProofRequiredState case")
+ }
+ }
+
+ private func proofRequiredSubmissionUseCase(issuer: Issuer, authorized: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier?, claimSet: ClaimSet? = nil) async throws -> String {
+ guard let credentialConfigurationIdentifier else { throw WalletError(description: "Credential configuration identifier not found") }
+ let payload: IssuanceRequestPayload = .configurationBased(credentialConfigurationIdentifier: credentialConfigurationIdentifier, claimSet: claimSet)
+ let responseEncryptionSpecProvider = { Issuer.createResponseEncryptionSpec($0) }
+ let requestOutcome = try await issuer.requestSingle(proofRequest: authorized, bindingKey: bindingKey, requestPayload: payload, responseEncryptionSpecProvider: responseEncryptionSpecProvider)
+ switch requestOutcome {
+ case .success(let request):
+ switch request {
+ case .success(let response):
+ if let result = response.credentialResponses.first {
+ switch result {
+ case .deferred(let transactionId):
+ return try await deferredCredentialUseCase(issuer: issuer, authorized: authorized, transactionId: transactionId)
+ case .issued(_, let credential, _):
+ return credential
+ }
+ } else {
+ throw WalletError(description: "No credential response results available")
+ }
+ case .invalidProof:
+ throw WalletError(description: "Although providing a proof with c_nonce the proof is still invalid")
+ case .failed(let error):
+ throw WalletError(description: error.localizedDescription)
+ }
+ case .failure(let error): throw WalletError(description: error.localizedDescription)
+ }
+ }
+
+ private func deferredCredentialUseCase(issuer: Issuer, authorized: AuthorizedRequest, transactionId: TransactionId) async throws -> String {
+ logger.info("--> [ISSUANCE] Got a deferred issuance response from server with transaction_id \(transactionId.value). Retrying issuance...")
+ let deferredRequestResponse = try await issuer.requestDeferredIssuance(proofRequest: authorized, transactionId: transactionId)
+ switch deferredRequestResponse {
+ case .success(let response):
+ switch response {
+ case .issued(_, let credential):
+ return credential
+ case .issuancePending(let transactionId):
+ throw WalletError(description: "Credential not ready yet. Try after \(transactionId.interval ?? 0)")
+ case .errored(_, let errorDescription):
+ throw WalletError(description: "\(errorDescription ?? "Something went wrong with your deferred request response")")
+ }
+ case .failure(let error):
+ throw WalletError(description: error.localizedDescription)
+ }
+ }
+
+ @MainActor
+ private func loginUserAndGetAuthCode(getAuthorizationCodeUrl: URL) async throws -> String? {
+ return try await withCheckedThrowingContinuation { c in
+ let authenticationSession = ASWebAuthenticationSession(url: getAuthorizationCodeUrl, callbackURLScheme: config.authFlowRedirectionURI.scheme!) { optionalUrl, optionalError in
+ guard optionalError == nil else { c.resume(throwing: OpenId4VCIError.authRequestFailed(optionalError!)); return }
+ guard let url = optionalUrl else { c.resume(throwing: OpenId4VCIError.authorizeResponseNoUrl); return }
+ guard let code = url.getQueryStringParameter("code") else { c.resume(throwing: OpenId4VCIError.authorizeResponseNoCode); return }
+ c.resume(returning: code)
+ }
+ authenticationSession.prefersEphemeralWebBrowserSession = true
+ authenticationSession.presentationContextProvider = self
+ authenticationSession.start()
+ }
+ }
+
+ public func presentationAnchor(for session: ASWebAuthenticationSession)
+ -> ASPresentationAnchor {
+#if os(iOS)
+ let window = UIApplication.shared.windows.first { $0.isKeyWindow }
+ return window ?? ASPresentationAnchor()
+#else
+ return ASPresentationAnchor()
+#endif
+ }
+}
+
+fileprivate extension URL {
+ func getQueryStringParameter(_ parameter: String) -> String? {
+ guard let url = URLComponents(string: self.absoluteString) else { return nil }
+ return url.queryItems?.first(where: { $0.name == parameter })?.value
+ }
+}
+
+extension SecureEnclave.P256.KeyAgreement.PrivateKey {
+
+ func toSecKey() throws -> SecKey {
+ var errorQ: Unmanaged?
+ guard let sf = SecKeyCreateWithData(Data() as NSData, [
+ kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
+ kSecAttrKeyClass: kSecAttrKeyClassPrivate,
+ kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
+ "toid": dataRepresentation
+ ] as NSDictionary, &errorQ) else { throw errorQ!.takeRetainedValue() as Error }
+ return sf
+ }
+}
+
+extension P256.KeyAgreement.PrivateKey {
+ func toSecKey() throws -> SecKey {
+ var error: Unmanaged?
+ guard let privateKey = SecKeyCreateWithData(x963Representation as NSData, [kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClass: kSecAttrKeyClassPrivate] as NSDictionary, &error) else {
+ throw error!.takeRetainedValue() as Error
+ }
+ return privateKey
+ }
+}
+
+
+public enum OpenId4VCIError: LocalizedError {
+ case authRequestFailed(Error)
+ case authorizeResponseNoUrl
+ case authorizeResponseNoCode
+ case tokenRequestFailed(Error)
+ case tokenResponseNoData
+ case tokenResponseInvalidData(String)
+ case dataNotValid
+
+ public var localizedDescription: String {
+ switch self {
+ case .authRequestFailed(let error):
+ if let wae = error as? ASWebAuthenticationSessionError, wae.code == .canceledLogin { return "The login has been canceled." }
+ return "Authorization request failed: \(error.localizedDescription)"
+ case .authorizeResponseNoUrl:
+ return "Authorization response does not include a url"
+ case .authorizeResponseNoCode:
+ return "Authorization response does not include a code"
+ case .tokenRequestFailed(let error):
+ return "Token request failed: \(error.localizedDescription)"
+ case .tokenResponseNoData:
+ return "No data received as part of token response"
+ case .tokenResponseInvalidData(let reason):
+ return "Invalid data received as part of token response: \(reason)"
+ case .dataNotValid:
+ return "Issued data not valid"
+ }
+ }
+}
+
+
diff --git a/Sources/EudiWalletKit/Services/OpenId4VpService.swift b/Sources/EudiWalletKit/Services/OpenId4VpService.swift
new file mode 100644
index 0000000..8b63879
--- /dev/null
+++ b/Sources/EudiWalletKit/Services/OpenId4VpService.swift
@@ -0,0 +1,173 @@
+/*
+Copyright (c) 2023 European Commission
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+Created on 04/10/2023
+*/
+
+import Foundation
+import SwiftCBOR
+import MdocDataModel18013
+import MdocSecurity18013
+import MdocDataTransfer18013
+import SiopOpenID4VP
+import JOSESwift
+import Logging
+import X509
+/// Implements remote attestation presentation to online verifier
+
+/// Implementation is based on the OpenID4VP – Draft 18 specification
+public class OpenId4VpService: PresentationService {
+ public var status: TransferStatus = .initialized
+ var openid4VPlink: String
+ // map of document id to data
+ var docs: [String: IssuerSigned]!
+ var iaca: [SecCertificate]!
+ var dauthMethod: DeviceAuthMethod
+ var devicePrivateKeys: [String: CoseKeyPrivate]!
+ var logger = Logger(label: "OpenId4VpService")
+ var presentationDefinition: PresentationDefinition?
+ var resolvedRequestData: ResolvedRequestData?
+ var siopOpenId4Vp: SiopOpenID4VP!
+ var openId4VpVerifierApiUri: String?
+ var openId4VpVerifierLegalName: String?
+ var readerAuthValidated: Bool = false
+ var readerCertificateIssuer: String?
+ var readerCertificateValidationMessage: String?
+ var mdocGeneratedNonce: String!
+ var sessionTranscript: SessionTranscript!
+ var eReaderPub: CoseKey?
+ var urlSession: URLSession
+ public var flow: FlowType
+
+ public init(parameters: [String: Any], qrCode: Data, openId4VpVerifierApiUri: String?, openId4VpVerifierLegalName: String?, urlSession: URLSession) throws {
+ self.flow = .openid4vp(qrCode: qrCode)
+ guard let (docs, devicePrivateKeys, iaca, dauthMethod) = MdocHelpers.initializeData(parameters: parameters) else {
+ throw PresentationSession.makeError(str: "MDOC_DATA_NOT_AVAILABLE")
+ }
+ self.docs = docs; self.devicePrivateKeys = devicePrivateKeys; self.iaca = iaca; self.dauthMethod = dauthMethod
+ guard let openid4VPlink = String(data: qrCode, encoding: .utf8) else {
+ throw PresentationSession.makeError(str: "QR_DATA_MALFORMED")
+ }
+ self.openid4VPlink = openid4VPlink
+ self.openId4VpVerifierApiUri = openId4VpVerifierApiUri
+ self.openId4VpVerifierLegalName = openId4VpVerifierLegalName
+ self.urlSession = urlSession
+ }
+
+ public func startQrEngagement() async throws -> String? { nil }
+
+ /// Receive request from an openid4vp URL
+ ///
+ /// - Returns: The requested items.
+ public func receiveRequest() async throws -> [String: Any] {
+ guard status != .error, let openid4VPURI = URL(string: openid4VPlink) else { throw PresentationSession.makeError(str: "Invalid link \(openid4VPlink)") }
+ siopOpenId4Vp = SiopOpenID4VP(walletConfiguration: getWalletConf(verifierApiUrl: openId4VpVerifierApiUri, verifierLegalName: openId4VpVerifierLegalName))
+ switch try await siopOpenId4Vp.authorize(url: openid4VPURI) {
+ case .notSecured(data: _):
+ throw PresentationSession.makeError(str: "Not secure request received.")
+ case let .jwt(request: resolvedRequestData):
+ self.resolvedRequestData = resolvedRequestData
+ switch resolvedRequestData {
+ case let .vpToken(vp):
+ if let key = vp.clientMetaData?.jwkSet?.keys.first(where: { $0.use == "enc"}), let x = key.x, let xd = Data(base64URLEncoded: x), let y = key.y, let yd = Data(base64URLEncoded: y), let crv = key.crv, let crvType = MdocDataModel18013.ECCurveType(crvName: crv) {
+ logger.info("Found jwks public key with curve \(crv)")
+ eReaderPub = CoseKey(x: [UInt8](xd), y: [UInt8](yd), crv: crvType)
+ }
+ let responseUri = if case .directPostJWT(let uri) = vp.responseMode { uri.absoluteString } else { "" }
+ mdocGeneratedNonce = Openid4VpUtils.generateMdocGeneratedNonce()
+ sessionTranscript = Openid4VpUtils.generateSessionTranscript(clientId: vp.client.id,
+ responseUri: responseUri, nonce: vp.nonce, mdocGeneratedNonce: mdocGeneratedNonce)
+ logger.info("Session Transcript: \(sessionTranscript.encode().toHexString()), for clientId: \(vp.client.id), responseUri: \(responseUri), nonce: \(vp.nonce), mdocGeneratedNonce: \(mdocGeneratedNonce!)")
+ self.presentationDefinition = vp.presentationDefinition
+ let items = try Openid4VpUtils.parsePresentationDefinition(vp.presentationDefinition, logger: logger)
+ guard let items else { throw PresentationSession.makeError(str: "Invalid presentation definition") }
+ var result: [String: Any] = [UserRequestKeys.valid_items_requested.rawValue: items]
+ if let ln = resolvedRequestData.legalName { result[UserRequestKeys.reader_legal_name.rawValue] = ln }
+ if let readerCertificateIssuer {
+ result[UserRequestKeys.reader_auth_validated.rawValue] = readerAuthValidated
+ result[UserRequestKeys.reader_certificate_issuer.rawValue] = MdocHelpers.getCN(from: readerCertificateIssuer)
+ result[UserRequestKeys.reader_certificate_validation_message.rawValue] = readerCertificateValidationMessage
+ }
+ return result
+ default: throw PresentationSession.makeError(str: "SiopAuthentication request received, not supported yet.")
+ }
+ }
+ }
+
+ /// Send response via openid4vp
+ ///
+ /// - Parameters:
+ /// - userAccepted: True if user accepted to send the response
+ /// - itemsToSend: The selected items to send organized in document types and namespaces
+ public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)?) async throws {
+ guard let pd = presentationDefinition, let resolved = resolvedRequestData else {
+ throw PresentationSession.makeError(str: "Unexpected error")
+ }
+ guard userAccepted, itemsToSend.count > 0 else {
+ try await SendVpToken(nil, pd, resolved, onSuccess)
+ return
+ }
+ logger.info("Openid4vp request items: \(itemsToSend)")
+ guard let (deviceResponse, _, _) = try MdocHelpers.getDeviceResponseToSend(deviceRequest: nil, issuerSigned: docs, selectedItems: itemsToSend, eReaderKey: eReaderPub, devicePrivateKeys: devicePrivateKeys, sessionTranscript: sessionTranscript, dauthMethod: .deviceSignature) else { throw PresentationSession.makeError(str: "DOCUMENT_ERROR") }
+ // Obtain consent
+ let vpTokenStr = Data(deviceResponse.toCBOR(options: CBOROptions()).encode()).base64URLEncodedString()
+ try await SendVpToken(vpTokenStr, pd, resolved, onSuccess)
+ }
+
+ fileprivate func SendVpToken(_ vpTokenStr: String?, _ pd: PresentationDefinition, _ resolved: ResolvedRequestData, _ onSuccess: ((URL?) -> Void)?) async throws {
+ let consent: ClientConsent = if let vpTokenStr {
+ .vpToken(vpToken: .msoMdoc(vpTokenStr, apu: mdocGeneratedNonce.base64urlEncode), presentationSubmission: .init(id: UUID().uuidString, definitionID: pd.id, descriptorMap: pd.inputDescriptors.filter { $0.formatContainer?.formats.contains(where: { $0["designation"].string?.lowercased() == "mso_mdoc" }) ?? false }.map { DescriptorMap(id: $0.id, format: "mso_mdoc", path: "$")} ))
+ } else { .negative(message: "Rejected") }
+ // Generate a direct post authorisation response
+ let response = try AuthorizationResponse(resolvedRequest: resolved, consent: consent, walletOpenId4VPConfig: getWalletConf(verifierApiUrl: openId4VpVerifierApiUri, verifierLegalName: openId4VpVerifierLegalName))
+ let result: DispatchOutcome = try await siopOpenId4Vp.dispatch(response: response)
+ if case let .accepted(url) = result {
+ logger.info("Dispatch accepted, return url: \(url?.absoluteString ?? "")")
+ onSuccess?(url)
+ } else if case let .rejected(reason) = result {
+ logger.info("Dispatch rejected, reason: \(reason)")
+ throw PresentationSession.makeError(str: reason)
+ }
+ }
+
+ lazy var chainVerifier: CertificateTrust = { [weak self] certificates in
+ let chainVerifier = X509CertificateChainVerifier()
+ let verified = try? chainVerifier.verifyCertificateChain(base64Certificates: certificates)
+ var result = chainVerifier.isChainTrustResultSuccesful(verified ?? .failure)
+ guard let self, let b64cert = certificates.first, let data = Data(base64Encoded: b64cert), let cert = SecCertificateCreateWithData(nil, data as CFData), let x509 = try? X509.Certificate(derEncoded: [UInt8](data)) else { return result }
+ self.readerCertificateIssuer = x509.subject.description
+ let (isValid, validationMessages, _) = SecurityHelpers.isMdocCertificateValid(secCert: cert, usage: .mdocReaderAuth, rootCerts: self.iaca ?? [])
+ self.readerAuthValidated = isValid
+ self.readerCertificateValidationMessage = validationMessages.joined(separator: "\n")
+ return result
+ }
+
+ /// OpenId4VP wallet configuration
+ func getWalletConf(verifierApiUrl: String?, verifierLegalName: String?) -> WalletOpenId4VPConfiguration? {
+ guard let rsaPrivateKey = try? KeyController.generateRSAPrivateKey(), let privateKey = try? KeyController.generateECDHPrivateKey(),
+ let rsaPublicKey = try? KeyController.generateRSAPublicKey(from: rsaPrivateKey) else { return nil }
+ guard let rsaJWK = try? RSAPublicKey(publicKey: rsaPublicKey, additionalParameters: ["use": "sig", "kid": UUID().uuidString, "alg": "RS256"]) else { return nil }
+ guard let keySet = try? WebKeySet(jwk: rsaJWK) else { return nil }
+ var supportedClientIdSchemes: [SupportedClientIdScheme] = [.x509SanUri(trust: chainVerifier), .x509SanDns(trust: chainVerifier)]
+ if let verifierApiUrl, let verifierLegalName {
+ let verifierMetaData = PreregisteredClient(clientId: "Verifier", legalName: verifierLegalName, jarSigningAlg: JWSAlgorithm(.RS256), jwkSetSource: WebKeySource.fetchByReference(url: URL(string: "\(verifierApiUrl)/wallet/public-keys.json")!))
+ supportedClientIdSchemes += [.preregistered(clients: [verifierMetaData.clientId: verifierMetaData])]
+ }
+ let res = WalletOpenId4VPConfiguration(subjectSyntaxTypesSupported: [.decentralizedIdentifier, .jwkThumbprint], preferredSubjectSyntaxType: .jwkThumbprint, decentralizedIdentifier: try! DecentralizedIdentifier(rawValue: "did:example:123"), signingKey: privateKey, signingKeySet: keySet, supportedClientIdSchemes: supportedClientIdSchemes, vpFormatsSupported: [], session: urlSession)
+ return res
+ }
+
+}
+
diff --git a/Sources/EudiWalletKit/Services/Openid4VpUtils.swift b/Sources/EudiWalletKit/Services/Openid4VpUtils.swift
new file mode 100644
index 0000000..5c474d2
--- /dev/null
+++ b/Sources/EudiWalletKit/Services/Openid4VpUtils.swift
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2023-2024 European Commission
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+import SwiftCBOR
+import CryptoKit
+import Logging
+import PresentationExchange
+import MdocDataModel18013
+import MdocSecurity18013
+/**
+ * Utility class to generate the session transcript for the OpenID4VP protocol.
+ *
+ * SessionTranscript = [
+ * DeviceEngagementBytes,
+ * EReaderKeyBytes,
+ * Handover
+ * ]
+ *
+ * DeviceEngagementBytes = nil,
+ * EReaderKeyBytes = nil
+ *
+ * Handover = OID4VPHandover
+ * OID4VPHandover = [
+ * clientIdHash
+ * responseUriHash
+ * nonce
+ * ]
+ *
+ * clientIdHash = Data
+ * responseUriHash = Data
+ *
+ * where clientIdHash is the SHA-256 hash of clientIdToHash and responseUriHash is the SHA-256 hash of the responseUriToHash.
+ *
+ *
+ * clientIdToHash = [clientId, mdocGeneratedNonce]
+ * responseUriToHash = [responseUri, mdocGeneratedNonce]
+ *
+ *
+ * mdocGeneratedNonce = String
+ * clientId = String
+ * responseUri = String
+ * nonce = String
+ *
+ */
+
+class Openid4VpUtils {
+
+ static func generateSessionTranscript(clientId: String, responseUri: String, nonce: String, mdocGeneratedNonce: String) -> SessionTranscript {
+ let openID4VPHandover = generateOpenId4VpHandover(clientId: clientId, responseUri: responseUri, nonce: nonce, mdocGeneratedNonce: mdocGeneratedNonce)
+ return SessionTranscript(handOver: openID4VPHandover)
+ }
+
+ static func generateOpenId4VpHandover(clientId: String, responseUri: String, nonce: String, mdocGeneratedNonce: String) -> CBOR {
+ let clientIdToHash = CBOR.encodeArray([clientId, mdocGeneratedNonce])
+ let responseUriToHash = CBOR.encodeArray([responseUri, mdocGeneratedNonce])
+
+ let clientIdHash = [UInt8](SHA256.hash(data: clientIdToHash))
+ let responseUriHash = [UInt8](SHA256.hash(data: responseUriToHash))
+
+ return CBOR.array([.byteString(clientIdHash), .byteString(responseUriHash), .utf8String(nonce)])
+ }
+
+ static func generateMdocGeneratedNonce() -> String {
+ var bytes = [UInt8](repeating: 0, count: 16)
+ let result = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
+ if result != errSecSuccess {
+ logger.warning("Problem generating random bytes with SecRandomCopyBytes")
+ bytes = (0 ..< 16).map { _ in UInt8.random(in: UInt8.min ... UInt8.max) }
+ }
+ return Data(bytes).base64URLEncodedString()
+ }
+
+ /// Parse mDoc request from presentation definition (Presentation Exchange 2.0.0 protocol)
+ static func parsePresentationDefinition(_ presentationDefinition: PresentationDefinition, logger: Logger? = nil) throws -> RequestItems? {
+ let pathRx = try NSRegularExpression(pattern: "\\$\\['([^']+)'\\]\\['([^']+)'\\]", options: .caseInsensitive)
+ var res = RequestItems()
+ for inputDescriptor in presentationDefinition.inputDescriptors {
+ guard let fc = inputDescriptor.formatContainer else { logger?.warning("Input descriptor with id \(inputDescriptor.id) is invalid "); continue }
+ guard fc.formats.contains(where: { $0["designation"].string?.lowercased() == "mso_mdoc" }) else { logger?.warning("Input descriptor with id \(inputDescriptor.id) does not contain format mso_mdoc "); continue }
+ let docType = inputDescriptor.id.trimmingCharacters(in: .whitespacesAndNewlines)
+ let kvs: [(String, String)] = inputDescriptor.constraints.fields.compactMap(\.paths.first).compactMap { Self.parsePath($0, pathRx: pathRx) }
+ let nsItems = Dictionary(grouping: kvs, by: \.0).mapValues { $0.map(\.1) }
+ if !nsItems.isEmpty { res[docType] = nsItems }
+ }
+ return res
+ }
+
+ static func parsePath(_ path: String, pathRx: NSRegularExpression) -> (String, String)? {
+ guard let match = pathRx.firstMatch(in: path, options: [], range: NSRange(location: 0, length: path.utf16.count)) else { return nil }
+ let r1 = match.range(at:1);
+ let r1l = path.index(path.startIndex, offsetBy: r1.location)
+ let r1r = path.index(r1l, offsetBy: r1.length)
+ let r2 = match.range(at: 2)
+ let r2l = path.index(path.startIndex, offsetBy: r2.location)
+ let r2r = path.index(r2l, offsetBy: r2.length)
+ return (String(path[r1l.. Data?
+ func startQrEngagement() async throws -> String?
///
/// - Returns: The requested items.
/// Receive request.
@@ -36,7 +36,7 @@ public protocol PresentationService {
/// - Parameters:
/// - userAccepted: True if user accepted to send the response
/// - itemsToSend: The selected items to send organized in document types and namespaces (see ``RequestItems``)
- func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws
+ func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)?) async throws
}
diff --git a/Sources/EudiWalletKit/Services/PresentationSession.swift b/Sources/EudiWalletKit/Services/PresentationSession.swift
new file mode 100644
index 0000000..9970e82
--- /dev/null
+++ b/Sources/EudiWalletKit/Services/PresentationSession.swift
@@ -0,0 +1,149 @@
+/*
+Copyright (c) 2023 European Commission
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import Foundation
+import SwiftUI
+import Logging
+@_exported import MdocDataTransfer18013
+import LocalAuthentication
+
+/// Presentation session
+///
+/// This class wraps the ``PresentationService`` instance, providing bindable fields to a SwifUI view
+public class PresentationSession: ObservableObject {
+ public var presentationService: any PresentationService
+ /// Reader certificate issuer (only for BLE flow wih verifier using reader authentication)
+ @Published public var readerCertIssuer: String?
+ /// Reader legal name (if provided)
+ @Published public var readerLegalName: String?
+ /// Reader certificate validation message (only for BLE transfer wih verifier using reader authentication)
+ @Published public var readerCertValidationMessage: String?
+ /// Reader certificate issuer is valid (only for BLE transfer wih verifier using reader authentication)
+ @Published public var readerCertIssuerValid: Bool?
+ /// Error message when the ``status`` is in the error state.
+ @Published public var uiError: WalletError?
+ /// Request items selected by the user to be sent to verifier.
+ @Published public var disclosedDocuments: [DocElementsViewModel] = []
+ /// Status of the data transfer.
+ @Published public var status: TransferStatus = .initializing
+ /// The ``FlowType`` instance
+ // public var flow: FlowType { presentationService.flow }
+ var handleSelected: ((Bool, RequestItems?) -> Void)?
+ /// Device engagement data (QR data for the BLE flow)
+ @Published public var deviceEngagement: String?
+ // map of document id to doc types
+ public var docIdAndTypes: [String: String]
+ /// User authentication required
+ var userAuthenticationRequired: Bool
+
+ public init(presentationService: any PresentationService, docIdAndTypes: [String: String], userAuthenticationRequired: Bool) {
+ self.presentationService = presentationService
+ self.docIdAndTypes = docIdAndTypes
+ self.userAuthenticationRequired = userAuthenticationRequired
+ }
+
+ @MainActor
+ /// Decodes a presentation request
+ ///
+ /// The ``disclosedDocuments`` property will be set. Additionally ``readerCertIssuer`` and ``readerCertValidationMessage`` may be set
+ /// - Parameter request: Keys are defined in the ``UserRequestKeys``
+ func decodeRequest(_ request: [String: Any]) throws {
+ guard docIdAndTypes.count > 0 else { throw Self.makeError(str: "No documents added to session ")}
+ // show the items as checkboxes
+ guard let validRequestItems = request[UserRequestKeys.valid_items_requested.rawValue] as? RequestItems else { return }
+ disclosedDocuments = [DocElementsViewModel]()
+ for (docId, docType) in docIdAndTypes {
+ var tmp = validRequestItems.toDocElementViewModels(docId: docId, docType: docType, valid: true)
+ if let errorRequestItems = request[UserRequestKeys.error_items_requested.rawValue] as? RequestItems, errorRequestItems.count > 0 {
+ tmp = tmp.merging(with: errorRequestItems.toDocElementViewModels(docId: docId, docType: docType, valid: false))
+ }
+ disclosedDocuments.append(contentsOf: tmp)
+ }
+ if let readerAuthority = request[UserRequestKeys.reader_certificate_issuer.rawValue] as? String {
+ readerCertIssuer = readerAuthority
+ readerCertIssuerValid = request[UserRequestKeys.reader_auth_validated.rawValue] as? Bool
+ readerCertValidationMessage = request[UserRequestKeys.reader_certificate_validation_message.rawValue] as? String
+ }
+ readerLegalName = request[UserRequestKeys.reader_legal_name.rawValue] as? String
+ status = .requestReceived
+ }
+
+ public static func makeError(str: String) -> NSError {
+ logger.error(Logger.Message(unicodeScalarLiteral: str))
+ return NSError(domain: "\(PresentationSession.self)", code: 0, userInfo: [NSLocalizedDescriptionKey: str])
+ }
+
+ public static func makeError(code: ErrorCode, str: String? = nil) -> NSError {
+ let message = str ?? code.description
+ logger.error(Logger.Message(unicodeScalarLiteral: message))
+ return NSError(domain: "\(PresentationSession.self)", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
+ }
+
+ /// Start QR engagement to be presented to verifier
+ ///
+ /// On success ``deviceEngagement`` published variable will be set with the result and ``status`` will be ``.qrEngagementReady``
+ /// On error ``uiError`` will be filled and ``status`` will be ``.error``
+ public func startQrEngagement() async {
+ do {
+ if let data = try await presentationService.startQrEngagement() {
+ await MainActor.run {
+ deviceEngagement = data
+ status = .qrEngagementReady
+ }
+ }
+ } catch { await setError(error) }
+ }
+
+ @MainActor
+ func setError(_ error: Error) {
+ status = .error
+ uiError = WalletError(description: error.localizedDescription, code: (error as NSError).code, userInfo: (error as NSError).userInfo)
+ }
+
+ /// Receive request from verifer
+ ///
+ /// The request is futher decoded internally. See also ``decodeRequest(_:)``
+ /// On success ``disclosedDocuments`` published variable will be set and ``status`` will be ``.requestReceived``
+ /// On error ``uiError`` will be filled and ``status`` will be ``.error``
+ /// - Returns: A request dictionary keyed by ``MdocDataTransfer.UserRequestKeys``
+ public func receiveRequest() async -> [String: Any]? {
+ do {
+ let request = try await presentationService.receiveRequest()
+ try await decodeRequest(request)
+ return request
+ } catch {
+ await setError(error)
+ return nil
+ }
+ }
+
+ /// Send response to verifier
+ /// - Parameters:
+ /// - userAccepted: Whether user confirmed to send the response
+ /// - itemsToSend: Data to send organized into a hierarcy of doc.types and namespaces
+ /// - onCancel: Action to perform if the user cancels the biometric authentication
+ public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onCancel: (() -> Void)? = nil, onSuccess: ((URL?) -> Void)? = nil) async {
+ do {
+ await MainActor.run {status = .userSelected }
+ let action = { [ weak self] in _ = try await self?.presentationService.sendResponse(userAccepted: userAccepted, itemsToSend: itemsToSend, onSuccess: onSuccess) }
+ try await EudiWallet.authorizedAction(action: action, disabled: !userAuthenticationRequired, dismiss: { onCancel?()}, localizedReason: NSLocalizedString("authenticate_to_share_data", comment: "") )
+ await MainActor.run {status = .responseSent }
+ } catch { await setError(error) }
+ }
+
+
+
+}
diff --git a/Sources/EudiWalletKit/Services/StorageManager.swift b/Sources/EudiWalletKit/Services/StorageManager.swift
new file mode 100644
index 0000000..75c4c38
--- /dev/null
+++ b/Sources/EudiWalletKit/Services/StorageManager.swift
@@ -0,0 +1,192 @@
+/*
+ Copyright (c) 2023 European Commission
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import Foundation
+import SwiftCBOR
+import MdocDataModel18013
+import WalletStorage
+import Logging
+import CryptoKit
+
+/// Storage manager. Provides services and view models
+public class StorageManager: ObservableObject {
+ public static let knownDocTypes = [EuPidModel.euPidDocType, IsoMdlModel.isoDocType]
+ /// Array of doc.types of documents loaded in the wallet
+ public var docTypes: [String] { mdocModels.map(\.docType) }
+ /// Array of document models loaded in the wallet
+ @Published public var mdocModels: [any MdocDecodable] = []
+ /// Array of document identifiers loaded in the wallet
+ public var documentIds: [String] { mdocModels.map(\.id) }
+ var storageService: any DataStorageService
+ /// Whether wallet currently has loaded data
+ @Published public var hasData: Bool = false
+ /// Whether wallet currently has loaded a document with doc.type included in the ``knownDocTypes`` array
+ @Published public var hasWellKnownData: Bool = false
+ /// Count of documents loaded in the wallet
+ @Published public var docCount: Int = 0
+ /// The first driver license model loaded in the wallet (deprecated)
+ @Published public var mdlModel: IsoMdlModel?
+ /// The first PID model loaded in the wallet (deprecated)
+ @Published public var pidModel: EuPidModel?
+ /// Other document models loaded in the wallet
+ @Published public var otherModels: [GenericMdocModel] = []
+ /// Error object with localized message
+ @Published public var uiError: WalletError?
+ let logger: Logger
+
+ public init(storageService: any DataStorageService) {
+ logger = Logger(label: "\(StorageManager.self)")
+ self.storageService = storageService
+ }
+
+ @MainActor
+ func refreshPublishedVars() {
+ hasData = mdocModels.count > 0
+ hasWellKnownData = hasData && !Set(docTypes).isDisjoint(with: Self.knownDocTypes)
+ docCount = mdocModels.count
+ mdlModel = getTypedDoc()
+ pidModel = getTypedDoc()
+ otherModels = getTypedDocs()
+ }
+
+ @MainActor
+ fileprivate func refreshDocModels(_ docs: [WalletStorage.Document]) {
+ mdocModels = docs.compactMap(toModel(doc:))
+ }
+
+ @MainActor
+ @discardableResult func appendDocModel(_ doc: WalletStorage.Document) -> (any MdocDecodable)? {
+ let mdoc: (any MdocDecodable)? = toModel(doc: doc)
+ if let mdoc { mdocModels.append(mdoc) }
+ return mdoc
+ }
+
+ func toModel(doc: WalletStorage.Document) -> (any MdocDecodable)? {
+ guard let (iss, dpk) = doc.getCborData() else { return nil }
+ var retModel: (any MdocDecodable)? = switch doc.docType {
+ case EuPidModel.euPidDocType: EuPidModel(id: iss.0, createdAt: doc.createdAt, issuerSigned: iss.1, devicePrivateKey: dpk.1)
+ case IsoMdlModel.isoDocType: IsoMdlModel(id: iss.0, createdAt: doc.createdAt, issuerSigned: iss.1, devicePrivateKey: dpk.1)
+ default: nil
+ }
+ retModel = retModel ?? GenericMdocModel(id: iss.0, createdAt: doc.createdAt, issuerSigned: iss.1, devicePrivateKey: dpk.1, docType: doc.docType, title: doc.docType.translated())
+ return retModel
+ }
+
+ public func getDocIdsToTypes() -> [String: String] {
+ Dictionary(uniqueKeysWithValues: mdocModels.map { m in (m.id, m.docType) })
+ }
+
+ /// Load documents from storage
+ ///
+ /// Internally sets the ``docTypes``, ``mdocModels``, ``documentIds``, ``mdocModels``, ``mdlModel``, ``pidModel`` variables
+ /// - Returns: An array of ``WalletStorage.Document`` objects
+ @discardableResult public func loadDocuments() async throws -> [WalletStorage.Document]? {
+ do {
+ guard let docs = try storageService.loadDocuments() else { return nil }
+ await refreshDocModels(docs)
+ await refreshPublishedVars()
+ return docs
+ } catch {
+ await setError(error)
+ throw error
+ }
+ }
+
+ func getTypedDoc(of: T.Type = T.self) -> T? where T: MdocDecodable {
+ mdocModels.first(where: { type(of: $0) == of}) as? T
+ }
+
+ func getTypedDocs(of: T.Type = T.self) -> [T] where T: MdocDecodable {
+ mdocModels.filter({ type(of: $0) == of}).map { $0 as! T }
+ }
+
+ /// Get document model by index
+ /// - Parameter index: Index in array of loaded models
+ /// - Returns: The ``MdocDecodable`` model
+ public func getDocumentModel(index: Int) -> (any MdocDecodable)? {
+ guard index < mdocModels.count else { return nil }
+ return mdocModels[index]
+ }
+
+ /// Get document model by id
+ /// - Parameter id: The id of the document model to return
+ /// - Returns: The ``MdocDecodable`` model
+ public func getDocumentModel(id: String) -> (any MdocDecodable)? {
+ guard let i = documentIds.firstIndex(of: id) else { return nil }
+ return getDocumentModel(index: i)
+ }
+
+ /// Get document model by docType
+ /// - Parameter docType: The docType of the document model to return
+ /// - Returns: The ``MdocDecodable`` model
+ public func getDocumentModels(docType: String) -> [any MdocDecodable] {
+ return (0...Index = documentIds.firstIndex(of: id) else { return }
+ do {
+ try await deleteDocument(index: i)
+ } catch {
+ await setError(error)
+ throw error
+ }
+ }
+ /// Delete document by Index
+ /// - Parameter index: Index in array of loaded models
+ public func deleteDocument(index: Int) async throws {
+ guard index < documentIds.count else { return }
+ let id = mdocModels[index].id
+ do {
+ try storageService.deleteDocument(id: id)
+ await MainActor.run {
+ if docTypes[index] == IsoMdlModel.isoDocType { mdlModel = nil }
+ if docTypes[index] == EuPidModel.euPidDocType { pidModel = nil }
+ mdocModels.remove(at: index)
+ }
+ await refreshPublishedVars()
+ } catch {
+ await setError(error)
+ throw error
+ }
+ }
+
+ /// Delete documenmts
+ public func deleteDocuments() async throws {
+ do {
+ try storageService.deleteDocuments()
+ await MainActor.run { mdocModels = []; mdlModel = nil; pidModel = nil }
+ await refreshPublishedVars()
+ } catch {
+ await setError(error)
+ throw error
+ }
+ }
+
+ @MainActor
+ func setError(_ error: Error) {
+ uiError = WalletError(description: error.localizedDescription, code: (error as NSError).code, userInfo: (error as NSError).userInfo)
+ }
+
+}
+
+
+
diff --git a/Sources/EudiWalletKit/ViewModels/DocElementsViewModel.swift b/Sources/EudiWalletKit/ViewModels/DocElementsViewModel.swift
new file mode 100644
index 0000000..289dc7b
--- /dev/null
+++ b/Sources/EudiWalletKit/ViewModels/DocElementsViewModel.swift
@@ -0,0 +1,86 @@
+/*
+Copyright (c) 2023 European Commission
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import Foundation
+import MdocDataModel18013
+
+/// View model used in SwiftUI for presentation request elements
+public struct DocElementsViewModel: Identifiable {
+ public var id: String { docId }
+ public var docId: String
+ public let docType: String
+ public var isEnabled: Bool
+ public var elements: [ElementViewModel]
+}
+extension DocElementsViewModel {
+ static func fluttenItemViewModels(_ nsItems: [String:[String]], valid isEnabled: Bool, mandatoryElementKeys: [String]) -> [ElementViewModel] {
+ nsItems.map { k,v in nsItemsToViewModels(k,v, isEnabled, mandatoryElementKeys) }.flatMap {$0}
+ }
+
+ static func nsItemsToViewModels(_ ns: String, _ items: [String], _ isEnabled: Bool, _ mandatoryElementKeys: [String]) -> [ElementViewModel] {
+ items.map { ElementViewModel(nameSpace: ns, elementIdentifier:$0, isMandatory: mandatoryElementKeys.contains($0), isEnabled: isEnabled) }
+ }
+
+ static func getMandatoryElementKeys(docType: String) -> [String] {
+ switch docType {
+ case IsoMdlModel.isoDocType:
+ return IsoMdlModel.isoMandatoryElementKeys
+ case EuPidModel.euPidDocType:
+ return EuPidModel.pidMandatoryElementKeys
+ default:
+ return []
+ }
+ }
+}
+
+extension RequestItems {
+ func toDocElementViewModels(docId: String, docType: String, valid: Bool) -> [DocElementsViewModel] {
+ compactMap { dType,nsItems in
+ if dType != docType { nil }
+ else { DocElementsViewModel(docId: docId, docType: docType, isEnabled: valid, elements: DocElementsViewModel.fluttenItemViewModels(nsItems, valid: valid, mandatoryElementKeys: DocElementsViewModel.getMandatoryElementKeys(docType: docType))) }
+ }
+ }
+}
+
+extension Array where Element == DocElementsViewModel {
+ public var items: RequestItems { Dictionary(grouping: self, by: \.docId).mapValues { $0.first!.elements.filter(\.isSelected).nsDictionary } }
+
+ func merging(with other: Self) -> Self {
+ var res = Self()
+ for otherDE in other {
+ if let exist = first(where: { $0.docId == otherDE.docId}) {
+ let newElements = (exist.elements + otherDE.elements).sorted(by: { $0.isEnabled && $1.isDisabled })
+ res.append(DocElementsViewModel(docId: exist.docId, docType: exist.docType, isEnabled: exist.isEnabled, elements: newElements))
+ }
+ else { res.append(otherDE) }
+ }
+ return res
+ }
+}
+
+public struct ElementViewModel: Identifiable {
+ public var id: String { "\(nameSpace)_\(elementIdentifier)" }
+ public let nameSpace: String
+ public let elementIdentifier: String
+ public let isMandatory: Bool
+ public var isEnabled: Bool
+ public var isDisabled: Bool { !isEnabled }
+ public var isSelected = true
+}
+
+extension Array where Element == ElementViewModel {
+ var nsDictionary: [String: [String]] { Dictionary(grouping: self, by: \.nameSpace).mapValues { $0.map(\.elementIdentifier)} }
+}
diff --git a/Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift b/Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift
new file mode 100644
index 0000000..524f27c
--- /dev/null
+++ b/Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift
@@ -0,0 +1,40 @@
+/*
+Copyright (c) 2023 European Commission
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import Foundation
+import OpenID4VCI
+
+/// Offered issue model contains information gathered by resolving an issue offer URL.
+///
+/// This information is returned from ``EudiWallet/resolveOfferUrlDocTypes(uriOffer:format:useSecureEnclave:)``
+public struct OfferedIssueModel {
+ /// Issuer name (currently the URL)
+ public let issuerName: String
+ /// Document types included in the offer
+ public let docModels: [OfferedDocModel]
+ /// Transaction code specification (in case of preauthorized flow)
+ public let txCodeSpec: TxCode?
+ /// Helper var for transaction code requirement
+ public var isTxCodeRequired: Bool { txCodeSpec != nil }
+}
+
+/// Information about an offered document type
+public struct OfferedDocModel {
+ /// Document type
+ public let docType: String
+ /// Display name for document type
+ public let displayName: String
+}
diff --git a/Sources/EudiWalletKit/ViewModels/WalletError.swift b/Sources/EudiWalletKit/ViewModels/WalletError.swift
new file mode 100644
index 0000000..59958d2
--- /dev/null
+++ b/Sources/EudiWalletKit/ViewModels/WalletError.swift
@@ -0,0 +1,45 @@
+/*
+ Copyright (c) 2023 European Commission
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+import Foundation
+/// Wallet error
+public struct WalletError: LocalizedError {
+
+ var description: String
+ var code: Int
+
+ public init(key: String, code: Int = 0) {
+ self.description = key.translated()
+ self.code = code
+ }
+
+ public init(description: String, code: Int = 0, userInfo: [String: Any]? = nil) {
+ self.description = description
+ self.code = code
+ guard let userInfo else { return }
+ var strError: String?
+ if let key = userInfo["key"] as? String { strError = NSLocalizedString(key, comment: "") }
+ if let s = userInfo["%s"] as? String { strError = strError?.replacingOccurrences(of: "%s", with: NSLocalizedString(s, comment: "")) }
+ if let strError { self.description = strError }
+ }
+
+ public var errorDescription: String? {
+ return description
+ }
+
+ public static func ==(lhs: Self, rhs: Self) -> Bool {
+ return lhs.description == rhs.description
+ }
+}
diff --git a/Sources/eudi-lib-ios-wallet-kit/Services/OpenId4VpService.swift b/Sources/eudi-lib-ios-wallet-kit/Services/OpenId4VpService.swift
deleted file mode 100644
index a363ee2..0000000
--- a/Sources/eudi-lib-ios-wallet-kit/Services/OpenId4VpService.swift
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
-Copyright (c) 2023 European Commission
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
-Iso18013HolderDemo
-Created on 04/10/2023
-*/
-
-import Foundation
-import SwiftCBOR
-import MdocDataModel18013
-import MdocSecurity18013
-import MdocDataTransfer18013
-import SiopOpenID4VP
-import JOSESwift
-import Logging
-
-/// Implements remote attestation presentation to online verifier
-
-/// Implementation is based on the OpenID4VP – Draft 18 specification
-class OpenId4VpService: PresentationService {
- var status: TransferStatus = .initialized
- var openid4VPlink: String
- var docs: [DeviceResponse]!
- var iaca: [SecCertificate]!
- var devicePrivateKey: CoseKeyPrivate!
- var logger = Logger(label: "OpenId4VpService")
- var presentationDefinition: PresentationDefinition?
- var resolvedRequestData: ResolvedRequestData?
- var siopOpenId4Vp: SiopOpenID4VP!
- var flow: FlowType
-
- init(parameters: [String: Any], qrCode: Data) throws {
- self.flow = .openid4vp(qrCode: qrCode)
- guard let wallet = Self.walletConf else {
- throw PresentationSession.makeError(str: "INVALID_WALLET_CONFIGURATION")
- }
- guard let (docs, devicePrivateKey, iaca) = MdocHelpers.initializeData(parameters: parameters) else {
- throw PresentationSession.makeError(str: "MDOC_DATA_NOT_AVAILABLE")
- }
- self.docs = docs; self.devicePrivateKey = devicePrivateKey; self.iaca = iaca
- siopOpenId4Vp = SiopOpenID4VP(walletConfiguration: wallet)
- guard let openid4VPlink = String(data: qrCode, encoding: .utf8) else {
- throw PresentationSession.makeError(str: "QR_DATA_MALFORMED")
- }
- self.openid4VPlink = openid4VPlink
- }
-
- func generateQRCode() async throws -> Data? { nil }
-
- /// Receive request from an openid4vp URL
- ///
- /// - Returns: The requested items.
- func receiveRequest() async throws -> [String: Any] {
- guard status != .error, let openid4VPURI = URL(string: openid4VPlink) else { throw PresentationSession.makeError(str: "Invalid link \(openid4VPlink)") }
- switch try await siopOpenId4Vp.authorize(url: openid4VPURI) {
- case .notSecured(data: _):
- throw PresentationSession.makeError(str: "Not secure request received.")
- case let .jwt(request: resolvedRequestData):
- self.resolvedRequestData = resolvedRequestData
- switch resolvedRequestData {
- case let .vpToken(vp):
- self.presentationDefinition = vp.presentationDefinition
- let items = parsePresentationDefinition(vp.presentationDefinition)
- guard let items else { throw PresentationSession.makeError(str: "Invalid presentation definition") }
- return [UserRequestKeys.valid_items_requested.rawValue: items]
- default: throw PresentationSession.makeError(str: "SiopAuthentication request received, not supported yet.")
- }
- }
- }
-
- /// Send response via openid4vp
- ///
- /// - Parameters:
- /// - userAccepted: True if user accepted to send the response
- /// - itemsToSend: The selected items to send organized in document types and namespaces
- func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws {
- guard let pd = presentationDefinition, let resolved = resolvedRequestData else {
- throw PresentationSession.makeError(str: "Unexpected error")
- }
- guard userAccepted, itemsToSend.count > 0 else {
- try await SendVpToken(nil, pd, resolved)
- return
- }
- logger.info("Openid4vp request items: \(itemsToSend)")
- guard let (deviceResponse, _, _) = try MdocHelpers.getDeviceResponseToSend(deviceRequest: nil, deviceResponses: docs, selectedItems: itemsToSend) else { throw PresentationSession.makeError(str: "DOCUMENT_ERROR") }
- // Obtain consent
- let vpTokenStr = Data(deviceResponse.toCBOR(options: CBOROptions()).encode()).base64URLEncodedString()
- try await SendVpToken(vpTokenStr, pd, resolved)
- }
-
- fileprivate func SendVpToken(_ vpTokenStr: String?, _ pd: PresentationDefinition, _ resolved: ResolvedRequestData) async throws {
- let consent: ClientConsent = if let vpTokenStr { .vpToken(vpToken: vpTokenStr, presentationSubmission: .init(id: pd.id, definitionID: pd.id, descriptorMap: [])) } else { .negative(message: "Rejected") }
- // Generate a direct post authorisation response
- let response = try AuthorizationResponse(resolvedRequest: resolved, consent: consent, walletOpenId4VPConfig: Self.walletConf!)
- let result: DispatchOutcome = try await siopOpenId4Vp.dispatch(response: response)
- if case let .accepted(url) = result {
- logger.info("Dispatch accepted, return url: \(url?.absoluteString ?? "")")
- } else if case let .rejected(reason) = result {
- logger.info("Dispatch rejected, reason: \(reason)")
- throw PresentationSession.makeError(str: reason)
- }
- }
-
- /// Parse mDoc request from presentation definition (Presentation Exchange 2.0.0 protocol)
- func parsePresentationDefinition(_ presentationDefinition: PresentationDefinition) -> RequestItems? {
- guard let fieldConstraints = presentationDefinition.inputDescriptors.first?.constraints.fields else { return nil }
- guard let docType = fieldConstraints.first(where: {$0.paths.first == "$.mdoc.doctype" })?.filter?["const"] as? String else { return nil }
- guard let namespace = fieldConstraints.first(where: {$0.paths.first == "$.mdoc.namespace" })?.filter?["const"] as? String else { return nil }
- let requestedFields = fieldConstraints.filter { $0.intentToRetain != nil }.compactMap { $0.paths.first?.replacingOccurrences(of: "$.mdoc.", with: "") }
- return [docType:[namespace:requestedFields]]
- }
-
- /// OpenId4VP wallet configuration
- static var walletConf: WalletOpenId4VPConfiguration? = {
- let VERIFIER_API = ProcessInfo.processInfo.environment["VERIFIER_API"] ?? "http://localhost:8080"
- let verifierMetaData = PreregisteredClient(clientId: "Verifier", jarSigningAlg: JWSAlgorithm(.RS256), jwkSetSource: WebKeySource.fetchByReference(url: URL(string: "\(VERIFIER_API)/wallet/public-keys.json")!))
- guard let rsaPrivateKey = try? KeyController.generateRSAPrivateKey(), let privateKey = try? KeyController.generateECDHPrivateKey(),
- let rsaPublicKey = try? KeyController.generateRSAPublicKey(from: rsaPrivateKey) else { return nil }
- guard let rsaJWK = try? RSAPublicKey(publicKey: rsaPublicKey, additionalParameters: ["use": "sig", "kid": UUID().uuidString, "alg": "RS256"]) else { return nil }
- guard let keySet = try? WebKeySet(jwk: rsaJWK) else { return nil }
- var res = WalletOpenId4VPConfiguration(subjectSyntaxTypesSupported: [], preferredSubjectSyntaxType: .jwkThumbprint, decentralizedIdentifier: try! DecentralizedIdentifier(rawValue: "did:example:123"), idTokenTTL: 10 * 60, presentationDefinitionUriSupported: true, signingKey: privateKey, signingKeySet: keySet, supportedClientIdSchemes: [.preregistered(clients: [verifierMetaData.clientId: verifierMetaData])], vpFormatsSupported: [])
- return res
- }()
-
-
-}
diff --git a/Sources/eudi-lib-ios-wallet-kit/Storage/DataSampleStorageService.swift b/Sources/eudi-lib-ios-wallet-kit/Storage/DataSampleStorageService.swift
deleted file mode 100644
index 6e38be7..0000000
--- a/Sources/eudi-lib-ios-wallet-kit/Storage/DataSampleStorageService.swift
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
-Copyright (c) 2023 European Commission
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import Foundation
-import MdocDataModel18013
-import SwiftUI
-import Logging
-
-/// Sample data storage service
-public class DataSampleStorageService: ObservableObject, DataStorageService {
-
- @Published public var euPidModel: EuPidModel?
- @Published public var isoMdlModel: IsoMdlModel?
- var sampleData: Data?
- @AppStorage("pidLoaded") public var pidLoaded: Bool = false
- @AppStorage("mdlLoaded") public var mdlLoaded: Bool = false
- @AppStorage("DebugDisplay") var debugDisplay: Bool = false
- let logger: Logger
-
- public init() {
- logger = Logger(label: "logger")
- }
-
- public func getDoc(i: Int) -> MdocDecodable? { i == 0 ? euPidModel : isoMdlModel}
- public func removeDoc(i: Int) {
- if i == 0 { euPidModel = nil; pidLoaded = false }
- else { isoMdlModel = nil; mdlLoaded = false }
- }
-
- public var hasData: Bool { pidLoaded && getDoc(i: 0) != nil || mdlLoaded && getDoc(i: 1) != nil }
-
- public func loadSampleData(force: Bool = false) {
- debugDisplay = true
- guard let sd = try? loadDocument(id: Self.defaultId) else { return }
- let sr = sd.decodeJSON(type: SignUpResponse.self)!
- guard let dr = sr.deviceResponse, let dpk = sr.devicePrivateKey else { return }
- if force || pidLoaded { euPidModel = EuPidModel(response: dr, devicePrivateKey: dpk) }
- pidLoaded = euPidModel != nil
- if force || mdlLoaded { isoMdlModel = IsoMdlModel(response: dr, devicePrivateKey: dpk) }
- mdlLoaded = isoMdlModel != nil
- }
-
- public static var defaultId: String = "EUDI_sample_data"
-
- public func loadDocument(id: String) throws -> Data {
- if let sampleData { return sampleData }
- sampleData = Data(name: id) ?? Data()
- return sampleData!
- }
-
- public func saveDocument(id: String, value: inout Data) throws {
- }
-
- public func deleteDocument(id: String) throws {
- }
-}
diff --git a/Sources/eudi-lib-ios-wallet-kit/Storage/KeyChainStorageService.swift b/Sources/eudi-lib-ios-wallet-kit/Storage/KeyChainStorageService.swift
deleted file mode 100644
index 6c6e7ad..0000000
--- a/Sources/eudi-lib-ios-wallet-kit/Storage/KeyChainStorageService.swift
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- Copyright (c) 2023 European Commission
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
-
-import Foundation
-/// Implements key-chain storage
-public class KeyChainStorageService: DataStorageService {
- public static var defaultId: String = "eudiw"
- var vcService = "eudiw"
- var accessGroup: String?
- var itemTypeCode: Int?
-
- public init() {}
-
- /// Gets the secret with the id passed in parameter
- /// - Parameters:
- /// - id: The Id of the secret
- /// - itemTypeCode: the item type code for the secret
- /// - accessGroup: the access group for the secret
- /// - Returns: The secret
- public func loadDocument(id: String) throws -> Data {
- var query: [String: Any]
- query = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: id, kSecAttrService: vcService, kSecReturnData: true] as [String: Any]
- if let accessGroup, !accessGroup.isEmpty { query[kSecAttrAccessGroup as String] = accessGroup}
- if let itemTypeCode { query[kSecAttrType as String] = itemTypeCode}
-
- var item: CFTypeRef?
- let status = SecItemCopyMatching(query as CFDictionary, &item)
- let statusMessage = SecCopyErrorMessageString(status, nil) as? String
- guard status == errSecSuccess else {
- throw NSError(domain: "\(KeyChainStorageService.self)", code: Int(status), userInfo: [NSLocalizedDescriptionKey: statusMessage ?? ""])
- }
- guard var value = item as? Data else { throw NSError(domain: "\(KeyChainStorageService.self)", code: Int(0), userInfo: [NSLocalizedDescriptionKey: "Invalid item \(id)"]) }
- defer { let c = value.count; value.withUnsafeMutableBytes { memset_s($0.baseAddress, c, 0, c); return } }
- return value
- }
-
- /// Save the secret to keychain
- /// Note: the value passed in will be zeroed out after the secret is saved
- /// - Parameters:
- /// - id: The Id of the secret
- /// - itemTypeCode: The secret type code (4 chars)
- /// - accessGroup: The access group to use to save secret.
- /// - value: The value of the secret
- public func saveDocument(id: String, value: inout Data) throws {
- defer { let c = value.count; value.withUnsafeMutableBytes { memset_s($0.baseAddress, c, 0, c); return } }
- // kSecAttrAccount is used to store the secret Id so that we can look it up later
- // kSecAttrService is always set to vcService to enable us to lookup all our secrets later if needed
- // kSecAttrType is used to store the secret type to allow us to cast it to the right Type on search
- var query: [String: Any]
-
- query = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: id, kSecAttrService: vcService, kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecValueData: value] as [String: Any]
-
- if let accessGroup, !accessGroup.isEmpty { query[kSecAttrAccessGroup as String] = accessGroup}
- if let itemTypeCode { query[kSecAttrType as String] = itemTypeCode}
- let status = SecItemAdd(query as CFDictionary, nil)
- let statusMessage = SecCopyErrorMessageString(status, nil) as? String
- guard status == errSecSuccess else {
- throw NSError(domain: "\(KeyChainStorageService.self)", code: Int(status), userInfo: [NSLocalizedDescriptionKey: statusMessage ?? ""])
- }
- }
-
- /// Delete the secret from keychain
- /// Note: the value passed in will be zeroed out after the secret is deleted
- /// - Parameters:
- /// - id: The Id of the secret
- /// - itemTypeCode: The secret type code (4 chars)
- /// - accessGroup: The access group of the secret.
- public func deleteDocument(id: String) throws {
-
- // kSecAttrAccount is used to store the secret Id so that we can look it up later
- // kSecAttrService is always set to vcService to enable us to lookup all our secrets later if needed
- // kSecAttrType is used to store the secret type to allow us to cast it to the right Type on search
- var query = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: id, kSecAttrService: vcService, kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly] as [String: Any]
-
- if let accessGroup, !accessGroup.isEmpty {
- query[kSecAttrAccessGroup as String] = accessGroup
- }
- if let itemTypeCode { query[kSecAttrType as String] = itemTypeCode}
-
- let status = SecItemDelete(query as CFDictionary)
- let statusMessage = SecCopyErrorMessageString(status, nil) as? String
- guard status == errSecSuccess else {
- throw NSError(domain: "\(KeyChainStorageService.self)", code: Int(status), userInfo: [NSLocalizedDescriptionKey: statusMessage ?? ""])
- }
- }
-}
diff --git a/Sources/eudi-lib-ios-wallet-kit/UserWallet.swift b/Sources/eudi-lib-ios-wallet-kit/UserWallet.swift
deleted file mode 100644
index c0bfb64..0000000
--- a/Sources/eudi-lib-ios-wallet-kit/UserWallet.swift
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-Copyright (c) 2023 European Commission
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import Foundation
-import MdocDataModel18013
-import MdocDataTransfer18013
-
-/// User wallet implementation
-public class UserWallet: ObservableObject {
- public var storageService: any DataStorageService
-
- public init(storageService: any DataStorageService = KeyChainStorageService()) {
- self.storageService = storageService
- }
-
- /// Begin attestation presentation to a verifier
- /// - Parameters:
- /// - flow: Presentation ``FlowType`` instance
- /// - dataFormat: Exchanged data ``Format`` type
- /// - Returns: A presentation session instance,
- public func beginPresentation(flow: FlowType, dataFormat: DataFormat = .cbor) -> PresentationSession {
- var parameters: [String: Any]
- do {
- switch dataFormat {
- case .cbor:
- let data = try storageService.loadDocument(id: type(of: storageService).defaultId)
- guard let sr = data.decodeJSON(type: SignUpResponse.self), let dr = sr.deviceResponse, let dpk = sr.devicePrivateKey else { throw NSError(domain: "\(UserWallet.self)", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error in document data"]) }
- parameters = [InitializeKeys.document_signup_response_data.rawValue: [dr],
- InitializeKeys.device_private_key.rawValue: dpk,
- InitializeKeys.trusted_certificates.rawValue: [Data(name: "scytales_root_ca", ext: "der")!]
- ]
- default:
- fatalError("jwt format not implemented")
- }
- switch flow {
- case .ble:
- let bleSvc = try BlePresentationService(parameters: parameters)
- return PresentationSession(presentationService: bleSvc)
- case .openid4vp(let qrCode):
- let openIdSvc = try OpenId4VpService(parameters: parameters, qrCode: qrCode)
- return PresentationSession(presentationService: openIdSvc)
- }
- } catch {
- return PresentationSession(presentationService: FaultPresentationService(error: error))
- }
- }
-}
diff --git a/Sources/eudi-lib-ios-wallet-kit/ViewModels/DocElementsViewModel.swift b/Sources/eudi-lib-ios-wallet-kit/ViewModels/DocElementsViewModel.swift
deleted file mode 100644
index b95cfcf..0000000
--- a/Sources/eudi-lib-ios-wallet-kit/ViewModels/DocElementsViewModel.swift
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
-Copyright (c) 2023 European Commission
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import Foundation
-
-/// View model used in SwiftUI for presentation request elements
-public struct DocElementsViewModel: Identifiable {
- public var id: String { docType }
- public let docType: String
- public var isEnabled: Bool
- public var elements: [ElementViewModel]
-}
-
-func fluttenItemViewModels(_ nsItems: [String:[String]], valid isEnabled: Bool) -> [ElementViewModel] {
- nsItems.map { k,v in nsItemsToViewModels(k,v, isEnabled) }.flatMap {$0}
-}
-
-func nsItemsToViewModels(_ ns: String, _ items: [String], _ isEnabled: Bool) -> [ElementViewModel] {
- items.map { ElementViewModel(nameSpace: ns, elementIdentifier:$0, isEnabled: isEnabled) }
-}
-
-extension RequestItems {
- func toDocElementViewModels(valid: Bool) -> [DocElementsViewModel] {
- map { docType,nsItems in DocElementsViewModel(docType: docType, isEnabled: valid, elements: fluttenItemViewModels(nsItems, valid: valid)) }
- }
-}
-
-extension Array where Element == DocElementsViewModel {
- public var docSelectedDictionary: RequestItems { Dictionary(grouping: self, by: \.docType).mapValues { $0.first!.elements.filter(\.isSelected).nsDictionary } }
-
- func merging(with other: Self) -> Self {
- var res = Self()
- for otherDE in other {
- if let exist = first(where: { $0.docType == otherDE.docType}) {
- let newElements = (exist.elements + otherDE.elements).sorted(by: { $0.isEnabled && $1.isDisabled })
- res.append(DocElementsViewModel(docType: exist.docType, isEnabled: exist.isEnabled, elements: newElements))
- }
- else { res.append(otherDE) }
- }
- return res
- }
-}
-
-public struct ElementViewModel: Identifiable {
- public var id: String { "\(nameSpace)_\(elementIdentifier)" }
- public let nameSpace: String
- public let elementIdentifier: String
- public var isEnabled: Bool
- public var isDisabled: Bool { !isEnabled }
- public var isSelected = true
-}
-
-extension Array where Element == ElementViewModel {
- var nsDictionary: [String: [String]] { Dictionary(grouping: self, by: \.nameSpace).mapValues { $0.map(\.elementIdentifier)} }
-}
diff --git a/Sources/eudi-lib-ios-wallet-kit/ViewModels/PresentationSession.swift b/Sources/eudi-lib-ios-wallet-kit/ViewModels/PresentationSession.swift
deleted file mode 100644
index d99159a..0000000
--- a/Sources/eudi-lib-ios-wallet-kit/ViewModels/PresentationSession.swift
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
-Copyright (c) 2023 European Commission
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import Foundation
-import SwiftUI
-import Logging
-@_exported import MdocDataTransfer18013
-
-/// Presentation session
-///
-/// This class wraps the ``PresentationService`` instance, providing bindable fields to a SwifUI view
-public class PresentationSession: ObservableObject {
- var presentationService: any PresentationService
- /// Reader certificate issuer (only for BLE flow wih verifier using reader authentication)
- @Published public var readerCertIssuerMessage: String?
- /// Reader certificate validation message (only for BLE transfer wih verifier using reader authentication)
- @Published public var readerCertValidationMessage: String?
- /// Error message when the ``status`` is in the error state.
- @Published public var errorMessage: String = ""
- /// Request items selected by the user to be sent to verifier.
- @Published public var selectedRequestItems: [DocElementsViewModel] = []
- /// Status of the data transfer.
- @Published public var status: TransferStatus = .initializing
- /// The ``FlowType`` instance
- public var flow: FlowType { presentationService.flow }
- var handleSelected: ((Bool, RequestItems?) -> Void)?
- /// Device engagement data (QR image data for the BLE flow)
- @Published public var deviceEngagement: Data?
-
- public init(presentationService: any PresentationService) {
- self.presentationService = presentationService
- }
-
- @MainActor
- /// Decodes a presentation request
- /// - Parameter request: Keys are defined in the ``UserRequestKeys``
- public func decodeRequest(_ request: [String: Any]) {
- // show the items as checkboxes
- guard let validRequestItems = request[UserRequestKeys.valid_items_requested.rawValue] as? RequestItems else { return }
- var tmp = validRequestItems.toDocElementViewModels(valid: true)
- if let errorRequestItems = request[UserRequestKeys.error_items_requested.rawValue] as? RequestItems, errorRequestItems.count > 0 {
- tmp = tmp.merging(with: errorRequestItems.toDocElementViewModels(valid: false))
- }
- selectedRequestItems = tmp
- if let readerAuthority = request[UserRequestKeys.reader_certificate_issuer.rawValue] as? String {
- //let bAuthenticated = request[UserRequestKeys.reader_auth_validated.rawValue] as? Bool ?? false
- readerCertIssuerMessage = "Reader Certificate Issuer:\n\(readerAuthority)"
- readerCertValidationMessage = request[UserRequestKeys.reader_certificate_validation_message.rawValue] as? String ?? ""
- }
- }
-
- public func didFinishedWithError(_ error: Error) {
- errorMessage = error.localizedDescription
- }
-
- static func makeError(str: String) -> NSError {
- logger.error(Logger.Message(unicodeScalarLiteral: str))
- return NSError(domain: "\(PresentationSession.self)", code: 0, userInfo: [NSLocalizedDescriptionKey: str])
- }
-
- public static var notAvailable: PresentationSession { PresentationSession(presentationService: FaultPresentationService(error: Self.makeError(str: "N/A"))) }
-}
-
-extension PresentationSession: PresentationService {
-
- @MainActor
- @discardableResult public func presentAttestations() async throws -> [String: Any] {
- deviceEngagement = try await generateQRCode()
- return try await receiveRequest()
- }
-
- @MainActor
- public func generateQRCode() async throws -> Data? {
- do {
- let data = try await presentationService.generateQRCode()
- if let data, data.count > 0 { status = .qrEngagementReady }
- return data
- } catch {
- status = .error
- self.errorMessage = error.localizedDescription
- return nil
- }
- }
-
- @MainActor
- public func receiveRequest() async throws -> [String: Any] {
- do {
- let request = try await presentationService.receiveRequest()
- decodeRequest(request)
- status = .requestReceived
- return request
- } catch {
- status = .error
- self.errorMessage = error.localizedDescription
- return [:]
- }
- }
-
- @MainActor
- public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems) async throws {
- do {
- status = .userSelected
- try await presentationService.sendResponse(userAccepted: userAccepted, itemsToSend: itemsToSend)
- status = .responseSent
- } catch {
- status = .error
- self.errorMessage = error.localizedDescription
- }
- }
-}
diff --git a/Tests/EudiWalletKitTests/EudiWalletKitTests.swift b/Tests/EudiWalletKitTests/EudiWalletKitTests.swift
new file mode 100644
index 0000000..19d33ce
--- /dev/null
+++ b/Tests/EudiWalletKitTests/EudiWalletKitTests.swift
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023 European Commission
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import XCTest
+@testable import EudiWalletKit
+import Foundation
+import CryptoKit
+import PresentationExchange
+import MdocDataModel18013
+import SwiftCBOR
+
+final class EudiWalletKitTests: XCTestCase {
+ func testExample() throws {
+ // XCTest Documentation
+ // https://developer.apple.com/documentation/xctest
+
+ // Defining Test Cases and Test Methods
+ // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
+ }
+
+ func testParsePresentationDefinition() throws {
+ let testPD = try JSONDecoder().decode(PresentationDefinition.self, from: Data(name: "TestPresentationDefinition", ext: "json", from: Bundle.module)! )
+ let items = try XCTUnwrap(Openid4VpUtils.parsePresentationDefinition(testPD))
+ XCTAssert(!items.keys.isEmpty)
+ print(items)
+ }
+
+ let ANNEX_B_OPENID4VP_HANDOVER = "835820DA25C527E5FB75BC2DD31267C02237C4462BA0C1BF37071F692E7DD93B10AD0B5820F6ED8E3220D3C59A5F17EB45F48AB70AEECF9EE21744B1014982350BD96AC0C572616263646566676831323334353637383930"
+ let ANNEX_B_SESSION_TRANSCRIPT = "83F6F6835820DA25C527E5FB75BC2DD31267C02237C4462BA0C1BF37071F692E7DD93B10AD0B5820F6ED8E3220D3C59A5F17EB45F48AB70AEECF9EE21744B1014982350BD96AC0C572616263646566676831323334353637383930"
+
+ let clientId = "example.com"
+ let responseUri = "https://example.com/12345/response"
+ let nonce = "abcdefgh1234567890"
+ let mdocGeneratedNonce = "1234567890abcdefgh"
+
+
+ func testGenerateOpenId4VpHandover() {
+ let openid4VpHandover = Openid4VpUtils.generateOpenId4VpHandover(clientId: clientId, responseUri: responseUri, nonce: nonce, mdocGeneratedNonce: mdocGeneratedNonce)
+ XCTAssertEqual(ANNEX_B_OPENID4VP_HANDOVER, openid4VpHandover.encode().toHexString().uppercased())
+ }
+
+ func testGenerateSessionTranscript() {
+ let sessionTranscript = Openid4VpUtils.generateSessionTranscript(clientId: clientId, responseUri: responseUri, nonce: nonce, mdocGeneratedNonce: mdocGeneratedNonce).encode(options: CBOROptions())
+ XCTAssertEqual(ANNEX_B_SESSION_TRANSCRIPT, sessionTranscript.toHexString().uppercased())
+ }
+
+
+
+
+}
diff --git a/Tests/EudiWalletKitTests/Resources/TestPresentationDefinition.json b/Tests/EudiWalletKitTests/Resources/TestPresentationDefinition.json
new file mode 100644
index 0000000..34f3f5d
--- /dev/null
+++ b/Tests/EudiWalletKitTests/Resources/TestPresentationDefinition.json
@@ -0,0 +1,128 @@
+{
+ "id": "32f54163-7166-48f1-93d8-ff217bdb0653",
+ "input_descriptors": [
+ {
+ "id": "eu.europa.ec.eudiw.pid.1",
+ "name": "EUDI PID",
+ "purpose": "We need to verify your identity",
+ "format": {
+ "mso_mdoc": {
+ "alg": [
+ "ES256",
+ "ES384",
+ "ES512",
+ "EdDSA",
+ "ESB256",
+ "ESB320",
+ "ESB384",
+ "ESB512"
+ ]
+ }
+ },
+ "constraints": {
+ "fields": [
+ {
+ "path": [
+ "$['eu.europa.ec.eudiw.pid.1']['family_name']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['eu.europa.ec.eudiw.pid.1']['given_name']"
+ ],
+ "intent_to_retain": false
+ }
+ ]
+ }
+ },
+ {
+ "id": "org.iso.18013.5.1.mDL ",
+ "format": {
+ "mso_mdoc": {
+ "alg": [
+ "ES256",
+ "ES384",
+ "ES512",
+ "EdDSA",
+ "ESB256",
+ "ESB320",
+ "ESB384",
+ "ESB512"
+ ]
+ }
+ },
+ "constraints": {
+ "fields": [
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['birth_date']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['document_number']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['driving_privileges']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['expiry_date']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['family_name']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['given_name']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['issue_date']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['issuing_authority']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['issuing_country']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['portrait']"
+ ],
+ "intent_to_retain": false
+ },
+ {
+ "path": [
+ "$['org.iso.18013.5.1']['un_distinguishing_sign']"
+ ],
+ "intent_to_retain": false
+ }
+ ],
+ "limit_disclosure": "required"
+ }
+ }
+ ]
+ }
diff --git a/Tests/eudi-lib-ios-wallet-kitTests/EudiWalletKitTests.swift b/Tests/eudi-lib-ios-wallet-kitTests/EudiWalletKitTests.swift
deleted file mode 100644
index 6f804fe..0000000
--- a/Tests/eudi-lib-ios-wallet-kitTests/EudiWalletKitTests.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-import XCTest
-@testable import EudiWalletKit
-
-final class EudiWalletKitTests: XCTestCase {
- func testExample() throws {
- // XCTest Documentation
- // https://developer.apple.com/documentation/xctest
-
- // Defining Test Cases and Test Methods
- // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
- }
-}
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000..9fedc8b
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,131 @@
+## v0.5.4
+### Custom URLSession variable
+- Added `public var urlSession: URLSession` variable to `EudiWallet` class. This variable can be used to set a custom URLSession for network requests. Allows for custom configuration of the URLSession, such as setting a custom timeout interval or Self-Signed certificates.
+
+## v0.5.3
+- Library updates
+
+## v0.5.2
+### Support Pre-Authorized Code Flow
+
+The flow is supported by existing methods:
+
+1 - An issue offer url is scanned. The following method is called: `public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> OfferedIssueModel`
+### (Breaking change, the return value type is `OfferedIssueModel` instead of `[OfferedDocModel]`)
+
+2 - If `OfferedIssueModel.isTxCodeRequired` is true, the call to `issueDocumentsByOfferUrl` must include the transaction code (parameter `txCodeValue`).
+
+- Note: for the clientId value the `EudiWallet/openID4VciClientId` is used.
+
+## v0.5.1
+### Update eudi-lib-ios-openid4vci-swift dependency to version 0.1.5
+
+- Update eudi-lib-ios-openid4vci-swift dependency to version 0.1.5
+- Fixes iOS16 offer url parsing issue
+
+## v0.5.0
+- `EuPidModel` updated with new PID docType
+
+## v0.4.9
+### Openid4VP fixes and updates
+
+- Update eudi-lib-ios-siop-openid4vp-swift to version 0.1.1
+- Fix openid4vp certificate chain verification (PresentationSession's `readerCertIssuerValid` and `readerCertIssuer` properties)
+- Add `readerLegalName` property to PresentationSession
+
+## v0.4.8
+- Update eudi-lib-ios-siop-openid4vp-swift to version 0.1.0
+- Added wallet configuration parameter `public var verifierLegalName: String?` (used for Openid4VP preregistered clients)
+
+## v0.4.7
+###Update eudi-lib-ios-siop-openid4vp-swift to version 0.1.0
+
+## v0.4.6
+### Update openid4vci to version 0.1.2
+
+##v0.4.5
+### Update eudi-lib-ios-openid4vci-swift to version 0.0.9
+
+## v0.4.4
+### Breaking change - mdocModels contains not-nil items (SwiftUI breaks with nil items)
+@Published public var mdocModels: [any MdocDecodable] = []
+
+## v0.4.3
+Openid4vp, BLE: Support sending multiple documents with same doc-type
+- DocElementsViewModel: added `public var docId: String`
+- PresentationSession / func sendResponse: itemsToSend dictionary is keyed by docId (and not docType)
+
+## v0.4.2
+Refactoring for issuing documents with IssuerSigned cbor data
+### Breaking change: Document data is saved as encoded IssuerSigned cbor
+
+## v0.4.1
+OpenID4VCI: fix for filtering resolved identifiers
+Support mdoc Authentication for OpenId4Vp #46
+
+## v0.4.0
+OpenID4VCI fix
+
+## v0.3.9
+OpenID4VCI: Allow partial issuing when some documents fail to issue
+
+## v0.3.8
+OpenID4VCI: Fixed issuing with https://dev.issuer.eudiw.dev
+
+## v0.3.7
+### Added functions:
+/// Resolve OpenID4VCI offer URL document types. Resolved offer metadata are cached
+
+` public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> [OfferedDocModel] `
+
+/// Issue documents by offer URI.
+
+`public func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], format: DataFormat, promptMessage: String? = nil, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [WalletStorage.Document] `
+
+### Breaking change:
+ `// PresentationSession
+ @Published public var deviceEngagement: String?`
+ use the following code to convert to QR code image:
+
+ `let qrImage = DeviceEngagement.getQrCodeImage(qrCode: d)`
+
+## v0.3.6
+Updated `eudi-lib-ios-siop-openid4vp-swift` to v0.0.74
+Updated `eudi-lib-ios-openid4vci-swift` to v0.0.7
+
+## v0.3.5
+Updated `eudi-lib-ios-siop-openid4vp-swift` to v0.0.73
+Updated `eudi-lib-ios-openid4vci-swift` to v0.0.6
+
+## v0.3.4
+- Refactor MdocDecodable (DocType, DocumentIdentifier, createdAt),
+
+## v0.3.3
+- OpenID4VP draft 13 support
+
+## v0.3.2
+- Internal updates for security checks
+
+## v0.3.1
+- Updated presentation definition parsing
+
+## v0.3.0
+- Updated eudi-lib-ios-siop-openid4vp-swift to 0.0.72
+
+## v0.2.9
+- Fixed mDOC authentication MAC validation error for mDL document type
+
+## v0.1.7
+- Added delete documents func
+- Storage manager functions are now `async throws`
+### MdocDataModel18013
+- extractDisplayStrings is recursive (cbor elements can be dictionaries)
+- NameValue: added `var children: [NameValue]` property (tree-like structure)
+- MdocDecodable: added 'var displayImages: [NameImage]' property
+
+## v0.1.6
+- Add isMandatory property to DocElementsViewModel structure
+- `PresentationSession` methods do not run on main actor
+- `PresentationSession`: add `readerCertIssuerValid`` (is verifier certificate trusted)
+- `PresentationSession`: change `readerCertIssuer`` (has verifier certificate common name)
+- `MdocDecodable`: add extension method: `public func toJson() -> [String: Any]`
diff --git a/security/.well-known/security.txt b/security/.well-known/security.txt
new file mode 100644
index 0000000..08d7af1
--- /dev/null
+++ b/security/.well-known/security.txt
@@ -0,0 +1,6 @@
+Contact: mailto:EC-VULNERABILITY-DISCLOSURE@ec.europa.eu,
+Expires: 2025-12-31T23:59:59.000Z
+Encryption: https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework/blob/main/security/pgp-key.txt
+Preferred-Languages: en
+Canonical: https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework/blob/main/security/.well-known/security.txt
+Policy: https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework/blob/main/SECURITY.md
diff --git a/security/gitleaks/gitleaks.toml b/security/gitleaks/gitleaks.toml
new file mode 100644
index 0000000..f67459c
--- /dev/null
+++ b/security/gitleaks/gitleaks.toml
@@ -0,0 +1,2234 @@
+# This is the default gitleaks configuration file.
+# Rules and allowlists are defined within this file.
+# Rules instruct gitleaks on what should be considered a secret.
+# Allowlists instruct gitleaks on what is allowed, i.e. not a secret.
+title = "gitleaks config"
+
+[allowlist]
+description = "global allow lists"
+regexes = [
+ '''219-09-9999''',
+ '''078-05-1120''',
+ '''(9[0-9]{2}|666)-\d{2}-\d{4}''',
+ ]
+paths = [
+ '''gitleaks.toml''',
+ '''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$''',
+ '''(go.mod|go.sum)$'''
+]
+
+[[rules]]
+description = "Adobe Client ID (Oauth Web)"
+id = "adobe-client-id"
+regex = '''(?i)(?:adobe)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "adobe",
+]
+
+[[rules]]
+description = "Adobe Client Secret"
+id = "adobe-client-secret"
+regex = '''(?i)\b((p8e-)(?i)[a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+keywords = [
+ "p8e-",
+]
+
+[[rules]]
+description = "Age secret key"
+id = "age secret key"
+regex = '''AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}'''
+keywords = [
+ "age-secret-key-1",
+]
+
+[[rules]]
+description = "Algolia API Key"
+id = "algolia-api-key"
+regex = '''(?i)(?:algolia)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+keywords = [
+ "algolia",
+]
+
+[[rules]]
+description = "Alibaba AccessKey ID"
+id = "alibaba-access-key-id"
+regex = '''(?i)\b((LTAI)(?i)[a-z0-9]{20})(?:['|\"|\n|\r|\s|\x60]|$)'''
+keywords = [
+ "ltai",
+]
+
+[[rules]]
+description = "Alibaba Secret Key"
+id = "alibaba-secret-key"
+regex = '''(?i)(?:alibaba)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{30})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "alibaba",
+]
+
+[[rules]]
+description = "Asana Client ID"
+id = "asana-client-id"
+regex = '''(?i)(?:asana)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9]{16})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "asana",
+]
+
+[[rules]]
+description = "Asana Client Secret"
+id = "asana-client-secret"
+regex = '''(?i)(?:asana)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "asana",
+]
+
+[[rules]]
+description = "Atlassian API token"
+id = "atlassian-api-token"
+regex = '''(?i)(?:atlassian|confluence)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{24})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "atlassian","confluence",
+]
+
+[[rules]]
+description = "AWS"
+id = "aws-access-token"
+regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
+keywords = [
+ "akia","agpa","aida","aroa","aipa","anpa","anva","asia",
+]
+
+[[rules]]
+description = "BitBucket Client ID"
+id = "bitbucket-client-id"
+regex = '''(?i)(?:bitbucket)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "bitbucket",
+]
+
+[[rules]]
+description = "BitBucket Client Secret"
+id = "bitbucket-client-secret"
+regex = '''(?i)(?:bitbucket)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{64})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "bitbucket",
+]
+
+[[rules]]
+description = "Beamer API token"
+id = "beamer-api-token"
+regex = '''(?i)(?:beamer)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(b_[a-z0-9=_\-]{44})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "beamer",
+]
+
+[[rules]]
+description = "Clojars API token"
+id = "clojars-api-token"
+regex = '''(?i)(CLOJARS_)[a-z0-9]{60}'''
+keywords = [
+ "clojars",
+]
+
+[[rules]]
+description = "Contentful delivery API token"
+id = "contentful-delivery-api-token"
+regex = '''(?i)(?:contentful)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{43})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "contentful",
+]
+
+[[rules]]
+description = "Databricks API token"
+id = "databricks-api-token"
+regex = '''(?i)\b(dapi[a-h0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+keywords = [
+ "dapi",
+]
+
+[[rules]]
+description = "Discord API key"
+id = "discord-api-token"
+regex = '''(?i)(?:discord)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{64})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "discord",
+]
+
+[[rules]]
+description = "Discord client ID"
+id = "discord-client-id"
+regex = '''(?i)(?:discord)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9]{18})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "discord",
+]
+
+[[rules]]
+description = "Discord client secret"
+id = "discord-client-secret"
+regex = '''(?i)(?:discord)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "discord",
+]
+
+[[rules]]
+description = "Dropbox API secret"
+id = "doppler-api-token"
+regex = '''(?i)(?:dropbox)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{15})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "dropbox",
+]
+
+[[rules]]
+description = "Dropbox long lived API token"
+id = "dropbox-long-lived-api-token"
+regex = '''(?i)(?:dropbox)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43})(?:['|\"|\n|\r|\s|\x60]|$)'''
+keywords = [
+ "dropbox",
+]
+
+[[rules]]
+description = "Dropbox short lived API token"
+id = "dropbox-short-lived-api-token"
+regex = '''(?i)(?:dropbox)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(sl\.[a-z0-9\-=_]{135})(?:['|\"|\n|\r|\s|\x60]|$)'''
+keywords = [
+ "dropbox",
+]
+
+[[rules]]
+description = "Doppler API token"
+id = "doppler-api-token"
+regex = '''(dp\.pt\.)(?i)[a-z0-9]{43}'''
+keywords = [
+ "doppler",
+]
+
+[[rules]]
+description = "Duffel API token"
+id = "duffel-api-token"
+regex = '''duffel_(test|live)_(?i)[a-z0-9_\-=]{43}'''
+keywords = [
+ "duffel",
+]
+
+[[rules]]
+description = "Dynatrace API token"
+id = "dynatrace-api-token"
+regex = '''dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}'''
+keywords = [
+ "dynatrace",
+]
+
+[[rules]]
+description = "EasyPost API token"
+id = "easypost-api-token"
+regex = '''EZAK(?i)[a-z0-9]{54}'''
+keywords = [
+ "ezak",
+]
+
+[[rules]]
+description = "EasyPost test API token"
+id = "easypost-test-api-token"
+regex = '''EZTK(?i)[a-z0-9]{54}'''
+keywords = [
+ "eztk",
+]
+
+[[rules]]
+description = "facebook"
+id = "facebook"
+regex = '''(?i)(?:facebook)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "facebook",
+]
+
+[[rules]]
+description = "Fastly API key"
+id = "fastly-api-token"
+regex = '''(?i)(?:fastly)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "fastly",
+]
+
+[[rules]]
+description = "Finicity Client Secret"
+id = "finicity-client-secret"
+regex = '''(?i)(?:finicity)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{20})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "finicity",
+]
+
+[[rules]]
+description = "Finicity API token"
+id = "finicity-api-token"
+regex = '''(?i)(?:finicity)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "finicity",
+]
+
+[[rules]]
+description = "Finicity Public Key"
+id = "flutterwave-public-key"
+regex = '''FLWPUBK_TEST-(?i)[a-h0-9]{32}-X'''
+keywords = [
+ "flwpubk_test",
+]
+
+[[rules]]
+description = "Finicity Secret Key"
+id = "flutterwave-public-key"
+regex = '''FLWSECK_TEST-(?i)[a-h0-9]{32}-X'''
+keywords = [
+ "flwseck_test",
+]
+
+[[rules]]
+description = "Finicity Secret Key"
+id = "flutterwave-public-key"
+regex = '''FLWSECK_TEST-(?i)[a-h0-9]{32}-X'''
+keywords = [
+ "flwseck_test",
+]
+
+[[rules]]
+description = "Frame.io API token"
+id = "frameio-api-token"
+regex = '''fio-u-(?i)[a-z0-9\-_=]{64}'''
+keywords = [
+ "fio-u-",
+]
+
+[[rules]]
+description = "GoCardless API token"
+id = "gocardless-api-token"
+regex = '''(?i)(?:gocardless)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(live_(?i)[a-z0-9\-_=]{40})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "live_","gocardless",
+]
+
+[[rules]]
+description = "GitHub Personal Access Token"
+id = "github-pat"
+regex = '''ghp_[0-9a-zA-Z]{36}'''
+keywords = [
+ "ghp_",
+]
+
+[[rules]]
+description = "GitHub OAuth Access Token"
+id = "github-oauth"
+regex = '''gho_[0-9a-zA-Z]{36}'''
+keywords = [
+ "gho_",
+]
+
+[[rules]]
+description = "GitHub App Token"
+id = "github-app-token"
+regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}'''
+keywords = [
+ "ghu_","ghs_",
+]
+
+[[rules]]
+description = "GitHub Refresh Token"
+id = "github-refresh-token"
+regex = '''ghr_[0-9a-zA-Z]{36}'''
+keywords = [
+ "ghr_",
+]
+
+[[rules]]
+description = "Gitlab Personal Access Token"
+id = "gitlab-pat"
+regex = '''glpat-[0-9a-zA-Z\-\_]{20}'''
+keywords = [
+ "glpat-",
+]
+
+[[rules]]
+description = "HashiCorp Terraform user/org API token"
+id = "hashicorp-tf-api-token"
+regex = '''(?i)[a-z0-9]{14}\.atlasv1\.[a-z0-9\-_=]{60,70}'''
+keywords = [
+ "atlasv1",
+]
+
+[[rules]]
+description = "Heroku API Key"
+id = "heroku-api-key"
+regex = '''(?i)(?:heroku)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "heroku",
+]
+
+[[rules]]
+description = "HubSpot API Token"
+id = "hubspot-api-key"
+regex = '''(?i)(?:hubspot)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "hubspot",
+]
+
+[[rules]]
+description = "Intercom API Token"
+id = "intercom-api-key"
+regex = '''(?i)(?:intercom)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{60})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "intercom",
+]
+
+[[rules]]
+description = "Linear API Token"
+id = "linear-api-key"
+regex = '''lin_api_(?i)[a-z0-9]{40}'''
+keywords = [
+ "lin_api_",
+]
+
+[[rules]]
+description = "Linear Client Secret"
+id = "linear-client-secret"
+regex = '''(?i)(?:linear)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "linear",
+]
+
+[[rules]]
+description = "LinkedIn Client ID"
+id = "linkedin-client-id"
+regex = '''(?i)(?:linkedin|linked-in)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{14})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "linkedin","linked-in",
+]
+
+[[rules]]
+description = "LinkedIn Client secret"
+id = "linkedin-client-secret"
+regex = '''(?i)(?:linkedin|linked-in)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{16})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "linkedin","linked-in",
+]
+
+[[rules]]
+description = "Lob API Key"
+id = "lob-api-key"
+regex = '''(?i)(?:lob)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}((live|test)_[a-f0-9]{35})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "test_","live_",
+]
+
+[[rules]]
+description = "Lob Publishable API Key"
+id = "lob-pub-api-key"
+regex = '''(?i)(?:lob)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}((test|live)_pub_[a-f0-9]{31})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "test_pub","live_pub","_pub",
+]
+
+[[rules]]
+description = "Mailchimp API key"
+id = "mailchimp-api-key"
+regex = '''(?i)(?:mailchimp)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32}-us20)(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "mailchimp",
+]
+
+[[rules]]
+description = "Mailgun public validation key"
+id = "mailgun-pub-key"
+regex = '''(?i)(?:mailgun)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(pubkey-[a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "mailgun",
+]
+
+[[rules]]
+description = "Mailgun private API token"
+id = "mailgun-private-api-token"
+regex = '''(?i)(?:mailgun)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(key-[a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "mailgun",
+]
+
+[[rules]]
+description = "Mailgun webhook signing key"
+id = "mailgun-signing-key"
+regex = '''(?i)(?:mailgun)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "mailgun",
+]
+
+[[rules]]
+description = "MapBox API token"
+id = "mapbox-api-token"
+regex = '''(?i)(?:mapbox)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(pk\.[a-z0-9]{60}\.[a-z0-9]{22})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "mapbox",
+]
+
+[[rules]]
+description = "MessageBird API token"
+id = "messagebird-api-token"
+regex = '''(?i)(?:messagebird|message-bird|message_bird)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{25})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "messagebird","message-bird","message_bird",
+]
+
+[[rules]]
+description = "MessageBird client ID"
+id = "messagebird-client-id"
+regex = '''(?i)(?:messagebird|message-bird|message_bird)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "messagebird","message-bird","message_bird",
+]
+
+[[rules]]
+description = "New Relic user API Key"
+id = "new-relic-user-api-key"
+regex = '''(?i)(?:new-relic|newrelic|new_relic)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(NRAK-[a-z0-9]{27})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "nrak",
+]
+
+[[rules]]
+description = "New Relic user API ID"
+id = "new-relic-user-api-id"
+regex = '''(?i)(?:new-relic|newrelic|new_relic)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{64})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "new-relic","newrelic","new_relic",
+]
+
+[[rules]]
+description = "New Relic ingest browser API token"
+id = "new-relic-browser-api-token"
+regex = '''(?i)(?:new-relic|newrelic|new_relic)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(NRJS-[a-f0-9]{19})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "nrjs-",
+]
+
+[[rules]]
+description = "npm access token"
+id = "npm-access-token"
+regex = '''(?i)\b(npm_[a-z0-9]{36})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "npm_",
+]
+
+[[rules]]
+description = "PlanetScale password"
+id = "planetscale-password"
+regex = '''(?i)\b(pscale_pw_(?i)[a-z0-9=\-_\.]{32,64})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "pscale_pw_",
+]
+
+[[rules]]
+description = "PlanetScale API token"
+id = "planetscale-api-token"
+regex = '''(?i)\b(pscale_tkn_(?i)[a-z0-9=\-_\.]{32,64})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "pscale_tkn_",
+]
+
+[[rules]]
+description = "PlanetScale OAuth token"
+id = "planetscale-oauth-token"
+regex = '''(?i)\b(pscale_oauth_(?i)[a-z0-9=\-_\.]{32,64})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "pscale_oauth_",
+]
+
+[[rules]]
+description = "Postman API token"
+id = "postman-api-token"
+regex = '''(?i)\b(PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "pmak-",
+]
+
+[[rules]]
+description = "Private Key"
+id = "private-key"
+regex = '''(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY-----[\s\S-]*KEY----'''
+keywords = [
+ "-----begin",
+]
+
+[[rules]]
+description = "Pulumi API token"
+id = "pulumi-api-token"
+regex = '''(?i)\b(pul-[a-f0-9]{40})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "pul-",
+]
+
+[[rules]]
+description = "PyPI upload token"
+id = "pypi-upload-token"
+regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}'''
+keywords = [
+ "pypi-ageichlwas5vcmc",
+]
+
+[[rules]]
+description = "Rubygem API token"
+id = "rubygems-api-token"
+regex = '''(?i)\b(rubygems_[a-f0-9]{48})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "rubygems_",
+]
+
+[[rules]]
+description = "SendGrid API token"
+id = "sendgrid-api-token"
+regex = '''(?i)\b(SG\.(?i)[a-z0-9=_\-\.]{66})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "sg.",
+]
+
+[[rules]]
+description = "Sendinblue API token"
+id = "sendinblue-api-token"
+regex = '''(?i)\b(xkeysib-[a-f0-9]{64}\-(?i)[a-z0-9]{16})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "xkeysib-",
+]
+
+[[rules]]
+description = "Shippo API token"
+id = "shippo-api-token"
+regex = '''(?i)\b(shippo_(live|test)_[a-f0-9]{40})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "shippo_",
+]
+
+[[rules]]
+description = "Shopify access token"
+id = "shopify-access-token"
+regex = '''shpat_[a-fA-F0-9]{32}'''
+keywords = [
+ "shpat_",
+]
+
+[[rules]]
+description = "Shopify custom access token"
+id = "shopify-custom-access-token"
+regex = '''shpca_[a-fA-F0-9]{32}'''
+keywords = [
+ "shpca_",
+]
+
+[[rules]]
+description = "Shopify private app access token"
+id = "shopify-private-app-access-token"
+regex = '''shppa_[a-fA-F0-9]{32}'''
+keywords = [
+ "shppa_",
+]
+
+[[rules]]
+description = "Shopify shared secret"
+id = "shopify-shared-secret"
+regex = '''shpss_[a-fA-F0-9]{32}'''
+keywords = [
+ "shpss_",
+]
+
+[[rules]]
+description = "Slack token"
+id = "slack-access-token"
+regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})'''
+keywords = [
+ "xoxb","xoxa","xoxp","xoxr","xoxs",
+]
+
+[[rules]]
+description = "Slack Webhook"
+id = "slack-web-hook"
+regex = '''https:\/\/hooks.slack.com\/services\/[A-Za-z0-9+\/]{44,46}'''
+keywords = [
+ "hooks.slack.com",
+]
+
+[[rules]]
+description = "Stripe"
+id = "stripe-access-token"
+regex = '''(?i)(sk|pk)_(test|live)_[0-9a-z]{10,32}'''
+keywords = [
+ "sk_test","pk_test","sk_live","pk_live",
+]
+
+[[rules]]
+description = "Twilio API Key"
+id = "twilio-api-key"
+regex = '''SK[0-9a-fA-F]{32}'''
+keywords = [
+ "twilio",
+]
+
+[[rules]]
+description = "Twitch API token"
+id = "twitch-api-token"
+regex = '''(?i)(?:twitch)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{30})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "twitch",
+]
+
+[[rules]]
+description = "twitter"
+id = "twitter"
+regex = '''(?i)(?:twitter)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{35,44})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "twitter",
+]
+
+[[rules]]
+description = "Typeform API token"
+id = "typeform-api-token"
+regex = '''(?i)(?:typeform)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(tfp_[a-z0-9\-_\.=]{59})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+keywords = [
+ "tfp_",
+]
+
+[[rules]]
+description = "Env Var"
+regex = '''(?i)(apikey|secret|api|password|pass)=[0-9a-zA-Z-_.{}]{4,120}'''
+secretGroup = 1
+keywords = [
+ "env",
+]
+
+[[rules]]
+description = "Generic API Key"
+id = "generic-api-key"
+regex = '''(?i)(?:key|api|token|secret|client|passwd|password|auth)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9a-z\-_.=]{10,150})(?:['|\"|\n|\r|\s|\x60]|$)'''
+secretGroup = 1
+entropy = 3.5
+keywords = [
+ "key","api","token","secret","client","passwd","password","auth",
+]
+[rules.allowlist]
+stopwords= [
+ "client",
+ "endpoint",
+ "vpn",
+ "_ec2_",
+ "aws_",
+ "authorize",
+ "author",
+ "define",
+ "config",
+ "credential",
+ "setting",
+ "sample",
+ "xxxxxx",
+ "000000",
+ "buffer",
+ "delete",
+ "aaaaaa",
+ "fewfwef",
+ "getenv",
+ "env_",
+ "system",
+ "example",
+ "ecdsa",
+ "sha256",
+ "sha1",
+ "sha2",
+ "md5",
+ "alert",
+ "wizard",
+ "target",
+ "onboard",
+ "welcome",
+ "page",
+ "exploit",
+ "experiment",
+ "expire",
+ "rabbitmq",
+ "scraper",
+ "widget",
+ "music",
+ "dns_",
+ "dns-",
+ "yahoo",
+ "want",
+ "json",
+ "action",
+ "script",
+ "fix_",
+ "fix-",
+ "develop",
+ "compas",
+ "stripe",
+ "service",
+ "master",
+ "metric",
+ "tech",
+ "gitignore",
+ "rich",
+ "open",
+ "stack",
+ "irc_",
+ "irc-",
+ "sublime",
+ "kohana",
+ "has_",
+ "has-",
+ "fabric",
+ "wordpres",
+ "role",
+ "osx_",
+ "osx-",
+ "boost",
+ "addres",
+ "queue",
+ "working",
+ "sandbox",
+ "internet",
+ "print",
+ "vision",
+ "tracking",
+ "being",
+ "generator",
+ "traffic",
+ "world",
+ "pull",
+ "rust",
+ "watcher",
+ "small",
+ "auth",
+ "full",
+ "hash",
+ "more",
+ "install",
+ "auto",
+ "complete",
+ "learn",
+ "paper",
+ "installer",
+ "research",
+ "acces",
+ "last",
+ "binding",
+ "spine",
+ "into",
+ "chat",
+ "algorithm",
+ "resource",
+ "uploader",
+ "video",
+ "maker",
+ "next",
+ "proc",
+ "lock",
+ "robot",
+ "snake",
+ "patch",
+ "matrix",
+ "drill",
+ "terminal",
+ "term",
+ "stuff",
+ "genetic",
+ "generic",
+ "identity",
+ "audit",
+ "pattern",
+ "audio",
+ "web_",
+ "web-",
+ "crud",
+ "problem",
+ "statu",
+ "cms-",
+ "cms_",
+ "arch",
+ "coffee",
+ "workflow",
+ "changelog",
+ "another",
+ "uiview",
+ "content",
+ "kitchen",
+ "gnu_",
+ "gnu-",
+ "gnu.",
+ "conf",
+ "couchdb",
+ "client",
+ "opencv",
+ "rendering",
+ "update",
+ "concept",
+ "varnish",
+ "gui_",
+ "gui-",
+ "gui.",
+ "version",
+ "shared",
+ "extra",
+ "product",
+ "still",
+ "not_",
+ "not-",
+ "not.",
+ "drop",
+ "ring",
+ "png_",
+ "png-",
+ "png.",
+ "actively",
+ "import",
+ "output",
+ "backup",
+ "start",
+ "embedded",
+ "registry",
+ "pool",
+ "semantic",
+ "instagram",
+ "bash",
+ "system",
+ "ninja",
+ "drupal",
+ "jquery",
+ "polyfill",
+ "physic",
+ "league",
+ "guide",
+ "pack",
+ "synopsi",
+ "sketch",
+ "injection",
+ "svg_",
+ "svg-",
+ "svg.",
+ "friendly",
+ "wave",
+ "convert",
+ "manage",
+ "camera",
+ "link",
+ "slide",
+ "timer",
+ "wrapper",
+ "gallery",
+ "url_",
+ "url-",
+ "url.",
+ "todomvc",
+ "requirej",
+ "party",
+ "http",
+ "payment",
+ "async",
+ "library",
+ "home",
+ "coco",
+ "gaia",
+ "display",
+ "universal",
+ "function",
+ "metadata",
+ "hipchat",
+ "under",
+ "room",
+ "config",
+ "personal",
+ "realtime",
+ "resume",
+ "database",
+ "testing",
+ "tiny",
+ "basic",
+ "forum",
+ "meetup",
+ "yet_",
+ "yet-",
+ "yet.",
+ "cento",
+ "dead",
+ "fluentd",
+ "editor",
+ "utilitie",
+ "run_",
+ "run-",
+ "run.",
+ "box_",
+ "box-",
+ "box.",
+ "bot_",
+ "bot-",
+ "bot.",
+ "making",
+ "sample",
+ "group",
+ "monitor",
+ "ajax",
+ "parallel",
+ "cassandra",
+ "ultimate",
+ "site",
+ "get_",
+ "get-",
+ "get.",
+ "gen_",
+ "gen-",
+ "gen.",
+ "gem_",
+ "gem-",
+ "gem.",
+ "extended",
+ "image",
+ "knife",
+ "asset",
+ "nested",
+ "zero",
+ "plugin",
+ "bracket",
+ "mule",
+ "mozilla",
+ "number",
+ "act_",
+ "act-",
+ "act.",
+ "map_",
+ "map-",
+ "map.",
+ "micro",
+ "debug",
+ "openshift",
+ "chart",
+ "expres",
+ "backend",
+ "task",
+ "source",
+ "translate",
+ "jbos",
+ "composer",
+ "sqlite",
+ "profile",
+ "mustache",
+ "mqtt",
+ "yeoman",
+ "have",
+ "builder",
+ "smart",
+ "like",
+ "oauth",
+ "school",
+ "guideline",
+ "captcha",
+ "filter",
+ "bitcoin",
+ "bridge",
+ "color",
+ "toolbox",
+ "discovery",
+ "new_",
+ "new-",
+ "new.",
+ "dashboard",
+ "when",
+ "setting",
+ "level",
+ "post",
+ "standard",
+ "port",
+ "platform",
+ "yui_",
+ "yui-",
+ "yui.",
+ "grunt",
+ "animation",
+ "haskell",
+ "icon",
+ "latex",
+ "cheat",
+ "lua_",
+ "lua-",
+ "lua.",
+ "gulp",
+ "case",
+ "author",
+ "without",
+ "simulator",
+ "wifi",
+ "directory",
+ "lisp",
+ "list",
+ "flat",
+ "adventure",
+ "story",
+ "storm",
+ "gpu_",
+ "gpu-",
+ "gpu.",
+ "store",
+ "caching",
+ "attention",
+ "solr",
+ "logger",
+ "demo",
+ "shortener",
+ "hadoop",
+ "finder",
+ "phone",
+ "pipeline",
+ "range",
+ "textmate",
+ "showcase",
+ "app_",
+ "app-",
+ "app.",
+ "idiomatic",
+ "edit",
+ "our_",
+ "our-",
+ "our.",
+ "out_",
+ "out-",
+ "out.",
+ "sentiment",
+ "linked",
+ "why_",
+ "why-",
+ "why.",
+ "local",
+ "cube",
+ "gmail",
+ "job_",
+ "job-",
+ "job.",
+ "rpc_",
+ "rpc-",
+ "rpc.",
+ "contest",
+ "tcp_",
+ "tcp-",
+ "tcp.",
+ "usage",
+ "buildout",
+ "weather",
+ "transfer",
+ "automated",
+ "sphinx",
+ "issue",
+ "sas_",
+ "sas-",
+ "sas.",
+ "parallax",
+ "jasmine",
+ "addon",
+ "machine",
+ "solution",
+ "dsl_",
+ "dsl-",
+ "dsl.",
+ "episode",
+ "menu",
+ "theme",
+ "best",
+ "adapter",
+ "debugger",
+ "chrome",
+ "tutorial",
+ "life",
+ "step",
+ "people",
+ "joomla",
+ "paypal",
+ "developer",
+ "solver",
+ "team",
+ "current",
+ "love",
+ "visual",
+ "date",
+ "data",
+ "canva",
+ "container",
+ "future",
+ "xml_",
+ "xml-",
+ "xml.",
+ "twig",
+ "nagio",
+ "spatial",
+ "original",
+ "sync",
+ "archived",
+ "refinery",
+ "science",
+ "mapping",
+ "gitlab",
+ "play",
+ "ext_",
+ "ext-",
+ "ext.",
+ "session",
+ "impact",
+ "set_",
+ "set-",
+ "set.",
+ "see_",
+ "see-",
+ "see.",
+ "migration",
+ "commit",
+ "community",
+ "shopify",
+ "what'",
+ "cucumber",
+ "statamic",
+ "mysql",
+ "location",
+ "tower",
+ "line",
+ "code",
+ "amqp",
+ "hello",
+ "send",
+ "index",
+ "high",
+ "notebook",
+ "alloy",
+ "python",
+ "field",
+ "document",
+ "soap",
+ "edition",
+ "email",
+ "php_",
+ "php-",
+ "php.",
+ "command",
+ "transport",
+ "official",
+ "upload",
+ "study",
+ "secure",
+ "angularj",
+ "akka",
+ "scalable",
+ "package",
+ "request",
+ "con_",
+ "con-",
+ "con.",
+ "flexible",
+ "security",
+ "comment",
+ "module",
+ "flask",
+ "graph",
+ "flash",
+ "apache",
+ "change",
+ "window",
+ "space",
+ "lambda",
+ "sheet",
+ "bookmark",
+ "carousel",
+ "friend",
+ "objective",
+ "jekyll",
+ "bootstrap",
+ "first",
+ "article",
+ "gwt_",
+ "gwt-",
+ "gwt.",
+ "classic",
+ "media",
+ "websocket",
+ "touch",
+ "desktop",
+ "real",
+ "read",
+ "recorder",
+ "moved",
+ "storage",
+ "validator",
+ "add-on",
+ "pusher",
+ "scs_",
+ "scs-",
+ "scs.",
+ "inline",
+ "asp_",
+ "asp-",
+ "asp.",
+ "timeline",
+ "base",
+ "encoding",
+ "ffmpeg",
+ "kindle",
+ "tinymce",
+ "pretty",
+ "jpa_",
+ "jpa-",
+ "jpa.",
+ "used",
+ "user",
+ "required",
+ "webhook",
+ "download",
+ "resque",
+ "espresso",
+ "cloud",
+ "mongo",
+ "benchmark",
+ "pure",
+ "cakephp",
+ "modx",
+ "mode",
+ "reactive",
+ "fuel",
+ "written",
+ "flickr",
+ "mail",
+ "brunch",
+ "meteor",
+ "dynamic",
+ "neo_",
+ "neo-",
+ "neo.",
+ "new_",
+ "new-",
+ "new.",
+ "net_",
+ "net-",
+ "net.",
+ "typo",
+ "type",
+ "keyboard",
+ "erlang",
+ "adobe",
+ "logging",
+ "ckeditor",
+ "message",
+ "iso_",
+ "iso-",
+ "iso.",
+ "hook",
+ "ldap",
+ "folder",
+ "reference",
+ "railscast",
+ "www_",
+ "www-",
+ "www.",
+ "tracker",
+ "azure",
+ "fork",
+ "form",
+ "digital",
+ "exporter",
+ "skin",
+ "string",
+ "template",
+ "designer",
+ "gollum",
+ "fluent",
+ "entity",
+ "language",
+ "alfred",
+ "summary",
+ "wiki",
+ "kernel",
+ "calendar",
+ "plupload",
+ "symfony",
+ "foundry",
+ "remote",
+ "talk",
+ "search",
+ "dev_",
+ "dev-",
+ "dev.",
+ "del_",
+ "del-",
+ "del.",
+ "token",
+ "idea",
+ "sencha",
+ "selector",
+ "interface",
+ "create",
+ "fun_",
+ "fun-",
+ "fun.",
+ "groovy",
+ "query",
+ "grail",
+ "red_",
+ "red-",
+ "red.",
+ "laravel",
+ "monkey",
+ "slack",
+ "supported",
+ "instant",
+ "value",
+ "center",
+ "latest",
+ "work",
+ "but_",
+ "but-",
+ "but.",
+ "bug_",
+ "bug-",
+ "bug.",
+ "virtual",
+ "tweet",
+ "statsd",
+ "studio",
+ "path",
+ "real-time",
+ "frontend",
+ "notifier",
+ "coding",
+ "tool",
+ "firmware",
+ "flow",
+ "random",
+ "mediawiki",
+ "bosh",
+ "been",
+ "beer",
+ "lightbox",
+ "theory",
+ "origin",
+ "redmine",
+ "hub_",
+ "hub-",
+ "hub.",
+ "require",
+ "pro_",
+ "pro-",
+ "pro.",
+ "ant_",
+ "ant-",
+ "ant.",
+ "any_",
+ "any-",
+ "any.",
+ "recipe",
+ "closure",
+ "mapper",
+ "event",
+ "todo",
+ "model",
+ "redi",
+ "provider",
+ "rvm_",
+ "rvm-",
+ "rvm.",
+ "program",
+ "memcached",
+ "rail",
+ "silex",
+ "foreman",
+ "activity",
+ "license",
+ "strategy",
+ "batch",
+ "streaming",
+ "fast",
+ "use_",
+ "use-",
+ "use.",
+ "usb_",
+ "usb-",
+ "usb.",
+ "impres",
+ "academy",
+ "slider",
+ "please",
+ "layer",
+ "cros",
+ "now_",
+ "now-",
+ "now.",
+ "miner",
+ "extension",
+ "own_",
+ "own-",
+ "own.",
+ "app_",
+ "app-",
+ "app.",
+ "debian",
+ "symphony",
+ "example",
+ "feature",
+ "serie",
+ "tree",
+ "project",
+ "runner",
+ "entry",
+ "leetcode",
+ "layout",
+ "webrtc",
+ "logic",
+ "login",
+ "worker",
+ "toolkit",
+ "mocha",
+ "support",
+ "back",
+ "inside",
+ "device",
+ "jenkin",
+ "contact",
+ "fake",
+ "awesome",
+ "ocaml",
+ "bit_",
+ "bit-",
+ "bit.",
+ "drive",
+ "screen",
+ "prototype",
+ "gist",
+ "binary",
+ "nosql",
+ "rest",
+ "overview",
+ "dart",
+ "dark",
+ "emac",
+ "mongoid",
+ "solarized",
+ "homepage",
+ "emulator",
+ "commander",
+ "django",
+ "yandex",
+ "gradle",
+ "xcode",
+ "writer",
+ "crm_",
+ "crm-",
+ "crm.",
+ "jade",
+ "startup",
+ "error",
+ "using",
+ "format",
+ "name",
+ "spring",
+ "parser",
+ "scratch",
+ "magic",
+ "try_",
+ "try-",
+ "try.",
+ "rack",
+ "directive",
+ "challenge",
+ "slim",
+ "counter",
+ "element",
+ "chosen",
+ "doc_",
+ "doc-",
+ "doc.",
+ "meta",
+ "should",
+ "button",
+ "packet",
+ "stream",
+ "hardware",
+ "android",
+ "infinite",
+ "password",
+ "software",
+ "ghost",
+ "xamarin",
+ "spec",
+ "chef",
+ "interview",
+ "hubot",
+ "mvc_",
+ "mvc-",
+ "mvc.",
+ "exercise",
+ "leaflet",
+ "launcher",
+ "air_",
+ "air-",
+ "air.",
+ "photo",
+ "board",
+ "boxen",
+ "way_",
+ "way-",
+ "way.",
+ "computing",
+ "welcome",
+ "notepad",
+ "portfolio",
+ "cat_",
+ "cat-",
+ "cat.",
+ "can_",
+ "can-",
+ "can.",
+ "magento",
+ "yaml",
+ "domain",
+ "card",
+ "yii_",
+ "yii-",
+ "yii.",
+ "checker",
+ "browser",
+ "upgrade",
+ "only",
+ "progres",
+ "aura",
+ "ruby_",
+ "ruby-",
+ "ruby.",
+ "polymer",
+ "util",
+ "lite",
+ "hackathon",
+ "rule",
+ "log_",
+ "log-",
+ "log.",
+ "opengl",
+ "stanford",
+ "skeleton",
+ "history",
+ "inspector",
+ "help",
+ "soon",
+ "selenium",
+ "lab_",
+ "lab-",
+ "lab.",
+ "scheme",
+ "schema",
+ "look",
+ "ready",
+ "leveldb",
+ "docker",
+ "game",
+ "minimal",
+ "logstash",
+ "messaging",
+ "within",
+ "heroku",
+ "mongodb",
+ "kata",
+ "suite",
+ "picker",
+ "win_",
+ "win-",
+ "win.",
+ "wip_",
+ "wip-",
+ "wip.",
+ "panel",
+ "started",
+ "starter",
+ "front-end",
+ "detector",
+ "deploy",
+ "editing",
+ "based",
+ "admin",
+ "capture",
+ "spree",
+ "page",
+ "bundle",
+ "goal",
+ "rpg_",
+ "rpg-",
+ "rpg.",
+ "setup",
+ "side",
+ "mean",
+ "reader",
+ "cookbook",
+ "mini",
+ "modern",
+ "seed",
+ "dom_",
+ "dom-",
+ "dom.",
+ "doc_",
+ "doc-",
+ "doc.",
+ "dot_",
+ "dot-",
+ "dot.",
+ "syntax",
+ "sugar",
+ "loader",
+ "website",
+ "make",
+ "kit_",
+ "kit-",
+ "kit.",
+ "protocol",
+ "human",
+ "daemon",
+ "golang",
+ "manager",
+ "countdown",
+ "connector",
+ "swagger",
+ "map_",
+ "map-",
+ "map.",
+ "mac_",
+ "mac-",
+ "mac.",
+ "man_",
+ "man-",
+ "man.",
+ "orm_",
+ "orm-",
+ "orm.",
+ "org_",
+ "org-",
+ "org.",
+ "little",
+ "zsh_",
+ "zsh-",
+ "zsh.",
+ "shop",
+ "show",
+ "workshop",
+ "money",
+ "grid",
+ "server",
+ "octopres",
+ "svn_",
+ "svn-",
+ "svn.",
+ "ember",
+ "embed",
+ "general",
+ "file",
+ "important",
+ "dropbox",
+ "portable",
+ "public",
+ "docpad",
+ "fish",
+ "sbt_",
+ "sbt-",
+ "sbt.",
+ "done",
+ "para",
+ "network",
+ "common",
+ "readme",
+ "popup",
+ "simple",
+ "purpose",
+ "mirror",
+ "single",
+ "cordova",
+ "exchange",
+ "object",
+ "design",
+ "gateway",
+ "account",
+ "lamp",
+ "intellij",
+ "math",
+ "mit_",
+ "mit-",
+ "mit.",
+ "control",
+ "enhanced",
+ "emitter",
+ "multi",
+ "add_",
+ "add-",
+ "add.",
+ "about",
+ "socket",
+ "preview",
+ "vagrant",
+ "cli_",
+ "cli-",
+ "cli.",
+ "powerful",
+ "top_",
+ "top-",
+ "top.",
+ "radio",
+ "watch",
+ "fluid",
+ "amazon",
+ "report",
+ "couchbase",
+ "automatic",
+ "detection",
+ "sprite",
+ "pyramid",
+ "portal",
+ "advanced",
+ "plu_",
+ "plu-",
+ "plu.",
+ "runtime",
+ "git_",
+ "git-",
+ "git.",
+ "uri_",
+ "uri-",
+ "uri.",
+ "haml",
+ "node",
+ "sql_",
+ "sql-",
+ "sql.",
+ "cool",
+ "core",
+ "obsolete",
+ "handler",
+ "iphone",
+ "extractor",
+ "array",
+ "copy",
+ "nlp_",
+ "nlp-",
+ "nlp.",
+ "reveal",
+ "pop_",
+ "pop-",
+ "pop.",
+ "engine",
+ "parse",
+ "check",
+ "html",
+ "nest",
+ "all_",
+ "all-",
+ "all.",
+ "chinese",
+ "buildpack",
+ "what",
+ "tag_",
+ "tag-",
+ "tag.",
+ "proxy",
+ "style",
+ "cookie",
+ "feed",
+ "restful",
+ "compiler",
+ "creating",
+ "prelude",
+ "context",
+ "java",
+ "rspec",
+ "mock",
+ "backbone",
+ "light",
+ "spotify",
+ "flex",
+ "related",
+ "shell",
+ "which",
+ "clas",
+ "webapp",
+ "swift",
+ "ansible",
+ "unity",
+ "console",
+ "tumblr",
+ "export",
+ "campfire",
+ "conway'",
+ "made",
+ "riak",
+ "hero",
+ "here",
+ "unix",
+ "unit",
+ "glas",
+ "smtp",
+ "how_",
+ "how-",
+ "how.",
+ "hot_",
+ "hot-",
+ "hot.",
+ "debug",
+ "release",
+ "diff",
+ "player",
+ "easy",
+ "right",
+ "old_",
+ "old-",
+ "old.",
+ "animate",
+ "time",
+ "push",
+ "explorer",
+ "course",
+ "training",
+ "nette",
+ "router",
+ "draft",
+ "structure",
+ "note",
+ "salt",
+ "where",
+ "spark",
+ "trello",
+ "power",
+ "method",
+ "social",
+ "via_",
+ "via-",
+ "via.",
+ "vim_",
+ "vim-",
+ "vim.",
+ "select",
+ "webkit",
+ "github",
+ "ftp_",
+ "ftp-",
+ "ftp.",
+ "creator",
+ "mongoose",
+ "led_",
+ "led-",
+ "led.",
+ "movie",
+ "currently",
+ "pdf_",
+ "pdf-",
+ "pdf.",
+ "load",
+ "markdown",
+ "phalcon",
+ "input",
+ "custom",
+ "atom",
+ "oracle",
+ "phonegap",
+ "ubuntu",
+ "great",
+ "rdf_",
+ "rdf-",
+ "rdf.",
+ "popcorn",
+ "firefox",
+ "zip_",
+ "zip-",
+ "zip.",
+ "cuda",
+ "dotfile",
+ "static",
+ "openwrt",
+ "viewer",
+ "powered",
+ "graphic",
+ "les_",
+ "les-",
+ "les.",
+ "doe_",
+ "doe-",
+ "doe.",
+ "maven",
+ "word",
+ "eclipse",
+ "lab_",
+ "lab-",
+ "lab.",
+ "hacking",
+ "steam",
+ "analytic",
+ "option",
+ "abstract",
+ "archive",
+ "reality",
+ "switcher",
+ "club",
+ "write",
+ "kafka",
+ "arduino",
+ "angular",
+ "online",
+ "title",
+ "don't",
+ "contao",
+ "notice",
+ "analyzer",
+ "learning",
+ "zend",
+ "external",
+ "staging",
+ "busines",
+ "tdd_",
+ "tdd-",
+ "tdd.",
+ "scanner",
+ "building",
+ "snippet",
+ "modular",
+ "bower",
+ "stm_",
+ "stm-",
+ "stm.",
+ "lib_",
+ "lib-",
+ "lib.",
+ "alpha",
+ "mobile",
+ "clean",
+ "linux",
+ "nginx",
+ "manifest",
+ "some",
+ "raspberry",
+ "gnome",
+ "ide_",
+ "ide-",
+ "ide.",
+ "block",
+ "statistic",
+ "info",
+ "drag",
+ "youtube",
+ "koan",
+ "facebook",
+ "paperclip",
+ "art_",
+ "art-",
+ "art.",
+ "quality",
+ "tab_",
+ "tab-",
+ "tab.",
+ "need",
+ "dojo",
+ "shield",
+ "computer",
+ "stat",
+ "state",
+ "twitter",
+ "utility",
+ "converter",
+ "hosting",
+ "devise",
+ "liferay",
+ "updated",
+ "force",
+ "tip_",
+ "tip-",
+ "tip.",
+ "behavior",
+ "active",
+ "call",
+ "answer",
+ "deck",
+ "better",
+ "principle",
+ "ches",
+ "bar_",
+ "bar-",
+ "bar.",
+ "reddit",
+ "three",
+ "haxe",
+ "just",
+ "plug-in",
+ "agile",
+ "manual",
+ "tetri",
+ "super",
+ "beta",
+ "parsing",
+ "doctrine",
+ "minecraft",
+ "useful",
+ "perl",
+ "sharing",
+ "agent",
+ "switch",
+ "view",
+ "dash",
+ "channel",
+ "repo",
+ "pebble",
+ "profiler",
+ "warning",
+ "cluster",
+ "running",
+ "markup",
+ "evented",
+ "mod_",
+ "mod-",
+ "mod.",
+ "share",
+ "csv_",
+ "csv-",
+ "csv.",
+ "response",
+ "good",
+ "house",
+ "connect",
+ "built",
+ "build",
+ "find",
+ "ipython",
+ "webgl",
+ "big_",
+ "big-",
+ "big.",
+ "google",
+ "scala",
+ "sdl_",
+ "sdl-",
+ "sdl.",
+ "sdk_",
+ "sdk-",
+ "sdk.",
+ "native",
+ "day_",
+ "day-",
+ "day.",
+ "puppet",
+ "text",
+ "routing",
+ "helper",
+ "linkedin",
+ "crawler",
+ "host",
+ "guard",
+ "merchant",
+ "poker",
+ "over",
+ "writing",
+ "free",
+ "classe",
+ "component",
+ "craft",
+ "nodej",
+ "phoenix",
+ "longer",
+ "quick",
+ "lazy",
+ "memory",
+ "clone",
+ "hacker",
+ "middleman",
+ "factory",
+ "motion",
+ "multiple",
+ "tornado",
+ "hack",
+ "ssh_",
+ "ssh-",
+ "ssh.",
+ "review",
+ "vimrc",
+ "driver",
+ "driven",
+ "blog",
+ "particle",
+ "table",
+ "intro",
+ "importer",
+ "thrift",
+ "xmpp",
+ "framework",
+ "refresh",
+ "react",
+ "font",
+ "librarie",
+ "variou",
+ "formatter",
+ "analysi",
+ "karma",
+ "scroll",
+ "tut_",
+ "tut-",
+ "tut.",
+ "apple",
+ "tag_",
+ "tag-",
+ "tag.",
+ "tab_",
+ "tab-",
+ "tab.",
+ "category",
+ "ionic",
+ "cache",
+ "homebrew",
+ "reverse",
+ "english",
+ "getting",
+ "shipping",
+ "clojure",
+ "boot",
+ "book",
+ "branch",
+ "combination",
+ "combo",
+]
\ No newline at end of file
diff --git a/security/pgp-key.txt b/security/pgp-key.txt
new file mode 100644
index 0000000..924d000
--- /dev/null
+++ b/security/pgp-key.txt
@@ -0,0 +1,99 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: SKS 1.1.6
+Comment: Hostname: pgp.mit.edu
+
+mQINBGB0SvgBEADdvQmw+kfNqunbTXwui3uLdF9HymnTyGUREsCn3bxdFDcdY2WfThI/7Sfz
+pRI8cr4Zcl9ZQPHkg+t0Yx59wQMWxGGB/jZ7+xZ7YnwPeESFDX2/zq3dC9PyCvAiTI8H6nFQ
+APNfn+wq5JsLwRcG4F9NFH93U1Q4rGhiCHFx+yvBIB8W19b6T87Nh1Ikhpkl7z/1bAfgccHK
+EtbU//9k3dj3YvKvQrp/BzRIVwDRBTqFKKRi/aL6fvC0IsFYJKZab9OvdXlyD5/wUJYrYtQE
+zAPwc/m11bNPBJvNLYqz3gs5s8XOVz6Crqrvsb5qLkB12nZt7G+Mry3rDizqGi2jCUyt6jaf
+ARF8kD3+oohp2isFrEuY/3hHK/6Sbas//toruXLa4gZvGq9TdwGST/bZechibDUaGT9UWyaj
+EgDI9xQ8rbPzqh98U3c+5xFmyyETryHqtUHqKkpm3JA6hb/s3r+XxKmWd1IlcjgMr7mJtnwY
+zwNDfEfMzH2AwAPdDB4Ru1qaJMRaxfX6hGqXdtj+0CSPE2eOgYgB24dSFIhjF8kmvnydopaa
+n8nPmV7Vv4nxo2dIqJvcfkrakZMAlmtLHAg64SFkGAkrxaOzNiRXtTP8EjM/p7AktnXeYuGQ
+c56O/FsCCCPmEVRIHsWEC1EGKBrOxiYmR9OfRUaq/hBHJ3PTtQARAQABtEZFQyBWVUxORVJB
+QklMSVRZIERJU0NMT1NVUkUgPEVDLVZVTE5FUkFCSUxJVFktRElTQ0xPU1VSRUBlYy5ldXJv
+cGEuZXU+iQEzBBABCAAdFiEEXpWfQC94SQf0rZEnsot02u2nX4kFAmJDCj0ACgkQsot02u2n
+X4mQfAf/Wz10UmEmdu169u3xvkfMcUO2bkkC7OzsZyO1hEp9LUD0Xb1uZOT3pRJtvC3HN5n0
+EI4XB1dg69qC7i7no0glPut3wRkpUnQqu4Eeel3TlJVbl/6bwgqra8YvUY+9AvV6T0KRShyn
+vZe7Hn7b0FGySobZKptrSxh5AUzTPr1mfWi75KvwV1WyT7dbJ1+3McEBaephWi4GUCar631s
+bl14CaLqqwuE7VTDJkpSvNU8Xenb/ZXtG/Xq7mgBc+Z1Ed2psNRm9LnuTfzRzr9C2OXLeL+g
+DvALUO47SOAb55KViZ+c3U0zD/uppOkqo06BYOzV92jhGhOdDWsJPExJKUQSRYkBMwQQAQgA
+HRYhBF6Vn0AveEkH9K2RJ7KLdNrtp1+JBQJiQwu/AAoJELKLdNrtp1+JXvEIALjAXjg1CA0Z
+vvXiL7eDOU+Ur9nCvTBLStuwCtAPBgbaaKPhuRTviS+uxQepEfsvoUawQgOnd1Xp9OAbKiUp
+LgsBdBQw6hsisFtvHtw+cimwv05EXmc83esZKrSx1A4rzQxM/wVc/bfnS9BbPfWIvNwyRnSj
+JlFsIvlbI+rrk05mZU3EMipjTca3EhQdVhHOcF5mq+gIhwyhLGs8UpTmd3n0fnom4Ogtn6XO
+f0hAa1WkKezS3jPjZ9StcR+LFA8ThtUpuMG4JArsihgAnD7h9HAscSo0DtUVqdl5H36UgH/H
+VUFAvqWBffmjc4nwRtW9GvcfPQAEkVvczoy4somirquJATMEEAEIAB0WIQRelZ9AL3hJB/St
+kSeyi3Ta7adfiQUCZOX8hwAKCRCyi3Ta7adfibonB/4q73G/63vyCDTIIUfcmx+OAe/FZnkM
+KjXFnlch1p8dMKBLqHd4anj8BnOYHSyBA1uGDPhivYeNE0LX8DZ25qTCw6MDHxeUjl7yDrOW
+4cUMVtY/aCFQkNJkD8own5Nyf4Al5QBfFyUWT0BmZBqLdcSZpsB5ATD6XoippVvJX/QYen6s
+xymdBnq1zvkTb6e9EtBi+OSUFdsUsHS+Z3P49LUDrYiIbHjScPAMI779opVWx0ShsGYgJlmn
+VoGXP8ggGg0VYJZwF4O4zi2Dh+vZZr5gnc2raX5ZQ2bqG8BWktNABPWxCSb/j7litSKsGdF4
+E5M8XtPZAPNv3QlgoKp4sNl+iQJUBBMBCAA+AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
+FiEER2BObTDbig6L3gVdZ3OqzfCfZigFAmJmt6AFCQPUAmYACgkQZ3OqzfCfZiiLXxAAvU1g
+ljMEfSSOfCIYDFWb8BLRiYiEnrkA/0QvL9Ul49qw0ZT0JVMH0FcAKDWL2injESEiqQeae9v+
+AuH9KtgW5dFML6zX5eZjC6E4nGcMGTCuUNwvhxiUqjtBixmu9dnuvJGmgxZn/dMpMfByYYve
+EcbdKmU7ZzK4h0WFL5nX4c2WF50v4W9LhZ6EjZHTjIZy2QrVdsr2qiTFfya8/DPV0HmPoCrI
+SIE/UIZc3FoUFnZB2VnMYhBnZKMNRgSAiogHW7ilg9NUTAs9ztVX7ln39fEIpT9vs0bbLF8y
+G4cHrzIgrTu2Ft88xN6KhP1JE8UWwlsDMU7peyXAvtwBoloIiH/Vm7TxNfCYsxJYO/TA7div
+6XVT/RcatTFPRf7eIZnFDL39lStfc14CtsQZbhA5hNl+IV2TLgo7NH8ZOpeTMeR88g2Jrlwc
+cUFi204F1bYVpc4CnKhed4frhD5D9/dfBIND869o4z+fA7lKw7eQ3zm3ZEiA/Jsu4bdk6VKC
+cVFPGQ8r0yDOiDgae8FXYkiuuPKE0a66Nf69dL27qTRTWzjTwnWwtSq6oTGnH7jX5qyjHRU9
+mNlpDoUJkhnpuaX3fj6AOEIr9+gs6t89GwFAUG+cH0UbBMuQwVVRX+MzfAKnuTH1WDKXa9T+
+TkQB6TY/O/+2kR6kZFmXhHAsoREXayCJAlQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwEC
+HgECF4AWIQRHYE5tMNuKDoveBV1nc6rN8J9mKAUCZFyuVwUJBcpN5wAKCRBnc6rN8J9mKAe2
+D/4jnI1u48JUrI5F/GSlVTiJPOf699TzFwOLayIxGAHZol5+pRRdCzEUr2GFENV1VNS6oo1d
+E0UQ6ygWdFye48crD5jzMccLCsAiHSBVDRugJNMVjTJnrOwVTdvpDHLSpEm9uYze6RnvrVcY
+KRnEmhf8erxAYbM5UBhTnneKfhVVVEMyJ3a6UcYPBPHOL64sYH1bhdC0MBxIkZLu8CeikGwI
+oudNKx/ns+GLlnnGJ38RHxVysiwGW9bZSwicaFN9HNRtv8S3JjQrrrS3ujTCOE9BbjySII05
+QY/2XddcBIB2UfJmPHRi8wZix6Lut3szrRQc/eHzeY+lu1Q95pvqMQH4m2G/wQtdf1n3qvNa
+CzGtd3qnePN+Ndep7GvRXL8upd3FEDe5wu1GB6ZnuKadQkGAejgRYp5qMC4kEgD2M3QT9vYc
+DFI8El5pM4X3ESMim7RuMn1lL7NMrDPvlltY6fTBfTe5cud3pxikTiIiOkHhgtp8QLVbDNEe
+auGbhJuejYfuITYpvqUhmg3j3yoa0cd+DLKkVSTKoTCLu31oXiJLBJ2zQuSHZU/GOF3jRw7Z
+RtbgdTRcIFMXROO650PsPdp8VsXFpLuulO9nGElDtYUuq0Ia2dYlhxLqcPrZdxBRL19fPbZJ
+AXkrkAL66Dms5d3q/dWkcLtF32BNa7mRDr8UaYkCVAQTAQgAPhYhBEdgTm0w24oOi94FXWdz
+qs3wn2YoBQJgdEr4AhsDBQkB4QKoBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGdzqs3w
+n2YodkEQAJevULEYkA7Wu8C8kNIdH/FMCAGx3PAY0AiKzoUH2r5juX4SrxnFQA+G+sFXHJ9q
+9adUQ7hju/0S9lA5uF4EGW7IxfQ5vJFDTh12sz9qPr9Pz58QVZTBtMfwWwVpH7+ePtapMG9Z
+enb/dMI7/GYkNpvBr+cqQPyF2L3ddzGkqKtjvme04vgMVx6MInmdW5Mf2+lUjbCOM6KUiZU+
+SCku4fRuA+Wnvq5GmWbVOIXmXK8VEUx0Xnp7aG1gpOXrkgykNh/67j2drjoFDsInkaU1Z8vc
+IYhoXOVeIz0YegY8DEMEia3kFFjNH5wXGUy3l1jbtzMvlwgn/Ly/KNeY8ME//lUSB8U/Fan9
+4mYpVo521tG35mz71zaS5VfL3scnjaGU9QdoqdB4eLwNp0seZHDE5014hgUsbB5OMOflCxVI
+clBV9FN4SEosaJ5XIyVfCYe83j3+49BtI34wu32G2oiyB4eiH6+YJoGS6BA+o2f3lM0QOW92
++b8BPlSK2fE2cmTxPzjGslaoVibdRhWIfLPTPZVMJU8KcoiTPk9C5zmdbPM/+eUzUgVQ45j6
+NDJ0voCWz4GF7XP79DYekzw72kv93sIRiWku1wguKmNmaH+cSymXHY75EIGTEwPtEYPUVktv
+IMvCqd2YmMh+cUqVx5uAeV7iRmZHyAZ8uhhQgt5xVCpKuQINBGB0SvgBEACo+WXBBrNKr9Cz
+dYwOyEy1uRRhxgS5DrYbdbqp8FfSTTlgNFWGhOBwt5feKUd2SKvPEihYAKT5OSsFTs3U1uFf
+lE/zzsMbAUgt3cOGaRTEpPJ8dTjyPKkrY+8O0YnD6g3lH677zVRwfukXs8h77n1FYLWwvwQM
+TQImLprKokWEp2+Q9dJQuNddHHGATkMEQ6+TSVt+B8Yi73FZzG96sCkMUH8isjXm0OV5/lsA
+rSOjt61I2X6sz42wcEbpCqnWCp6HVe/+uqi1d2if9XhmNBy5FXuKP0cJXRoUPLoPXp/g1FYM
+N9qgNUpet1m5zNCg2RR1cf8SlmrroO6ox5rUoiVu7yopY+iX9bQbiV3kwLHUZiG8rliAGaeu
+SXXDe0vOJ/IWekmFBI8OVLI37hLitCHjKdiDPHhTMcjj4Bumm1H8kL2Ft8EkaTi99Jj59LbO
++vfLsTxigmjTv07AniS2FB3kDRY8ArR90pDMaA0ZF/At9z3jaoIZP1/R8N9SCieRcNCTuvF+
+n2CmADOdiZfVX40/Mg3Spce4oBAC/FwRr3cQskeP6950dHIAqNox507eALVyo+Ya3xsjecMS
+I0j+QntIuMh6b5SxwpuQVadGM45LGI//63eOkqREHgzxWwIJLs4V2Qn0flvV3QjbucjrMLkn
+dtUWg14Oy1LGN3bZvLsyqwARAQABiQI8BBgBCAAmAhsMFiEER2BObTDbig6L3gVdZ3OqzfCf
+ZigFAmTl/FcFCQXKTeYACgkQZ3OqzfCfZigzHhAAwHWjZKv/kTOudm0e2uEfehcOZOKsOiJv
+GOYRNUezcHZKqjfaLeicieQCZ/JWOiCmiI4Z7S1qZIC3fb6WRmOtsC5sCU9K7Ko7KRJHjwZK
+0xff6PWXCpR4CCtpyt6iYeDFu+ENEuhdbSBfwEW043gZCEvOhGn7RL499N7kfnXq04k060gv
+PgliM2roVmAtBT8DSak0Dt+FrdLnioPjqJb0F9GYOrnndCnkJyNFIXNIMcEm2HDWed6rxP8t
+pRMx/bsrFbM/ZbDOUqMJO4uJ/AXvprhUDrHxWWC5VW6gucIVCmMNtwvFd47DzkXglWgnjLqC
+oEXlQcmJ624jy6+f43iZH98BYIM3GyqtK5NaG1Eez6LTWOH4ZtMJr6/lcBtOZPUuJn8vtUhw
+sZaZ/56Ua2eVWm+db3yyQltApjDBQuC+nN0XuIyleeSVfDJZ6u6Hb0v9OklnhcNpNH0Fog2k
+zM8R/B034Ig8ymTIqITYv7Vh2eVHpwtwmUQypcBw5eMRdOOCcF2whICQAnQZLoZlqMnmwXa1
+LTbtKumieyISSxYoFMbGAkKrPF6ABeOGdQbFaZh/KSoginRqslvBxCcbDwnkYxaWYkzQacF9
+N4Wlm1aXQFgmhPTyAFRk6KJYXP5ojVPHQaii6J5UqjUN9f92rYu3MI9Efem3eGv0SG4N3fO1
+sYGJAjwEGAEIACYWIQRHYE5tMNuKDoveBV1nc6rN8J9mKAUCYHRK+AIbDAUJAeECqAAKCRBn
+c6rN8J9mKKd3D/9dIzmTr+snTXT4pU0aa8+p0Nj6AMkZjJHdLCWcL/56l+S4g4akdOUfWI4Z
+ufCNXKJ4GcLde8UWX7Zhr9xq/00a8sbsupmk4pLinJgwC6Tmg6KHWGb4ScRV3z+2TWuhhkeT
+WWIvnscxmXjuXWecRJ9nbwUcFZ7zl26P72eOmK1Omvmhm+dTqt4UyhKnBoIQ0BA45hNrOvWy
+9Pp702LO/BhI4Su1EFfjJ9jiqXuEsXab90Q+ig3PauRgJ95nWB8tGPKMGDnooZSTve5gtjGw
+6W2FJbL7HFKYZZCM95+ROYO6D9XJr78hZ2ocOhDuTwU1oGy1hUpCm29h2BfKUiqeYkHZ9w/s
+tyk5TyD1Ca5RsIbEFqUtiVJdhMcQuaT0WQYcVzXUKyiiOlOxbjVhWZ0WtkOsuCQWkHUAbRUs
+1NGsBldo31jQxTE/M+AIxEB0X0z0Pz/WxK4RQeMlK1qm3MraaZ9hh8gNyvfojvgEE7P/Mqv4
+PMNoqregEBqhVMzilk6eVAcY3axs/HLu7iqgJar+NqlEfklar1pm0ls9XJcn/vfv7XRAHWnm
+tpKeHLVpEEpZjDf5hAgU0BjxecC9pSOu5S12OYa4NQ6P49Kb2IIZRvxooSzH7MF4gIfi7/8C
+ItKxho+oBdJyG5cUpedCHB/YwNJFTpLfSlXItLxW2AxOqtfzDg==
+=wS9M
+-----END PGP PUBLIC KEY BLOCK-----