Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions PeerConnectivity-security.plan
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# PeerConnectivity Security Improvements Plan

## Goal

Add practical, well-documented security abstractions around MultipeerConnectivity features that PeerConnectivity currently omits or weakens, while preserving backward compatibility for existing users.

## Current State

- `PeerSession` creates `MCSession` with `securityIdentity: nil` and `encryptionPreference: .optional`.
- `MCSessionDelegate.session(_:didReceiveCertificate:fromPeer:certificateHandler:)` is surfaced as `.receivedCertificate`.
- `PeerConnectionManager` currently installs an internal listener that always calls `handler(true)` for certificate events.
- `.automatic` mode auto-invites found peers and auto-accepts incoming invitations.
- `MCNearbyServiceAdvertiser` and `MCAdvertiserAssistant` use `discoveryInfo: nil`.
- Browser receives `discoveryInfo`, but `PeerBrowserEvent.foundPeer` discards it.
- `invitePeer(_:withContext:timeout:)` and `.receivedInvitation(peer:withContext:invitationHandler:)` already expose invitation context.
- Service type and display name constraints are documented but not validated.

## Design Principles

1. Preserve existing initializer behavior where possible.
2. Prefer explicit security configuration over event-listener side effects.
3. Make insecure defaults visible in documentation.
4. Avoid overbuilding certificate generation/keychain helpers in the first pass.
5. Treat discovery info, display names, and invitation context as public or unauthenticated metadata unless documented otherwise.
6. Keep implementation aligned with the existing Observable/EventProducer architecture.

## Phase 1 — Session Security Configuration

### Add `PeerSecurityConfiguration`

Proposed API:

```swift
public struct PeerSecurityConfiguration {
public var encryptionPreference : MCEncryptionPreference
public var securityIdentity : [Any]?
public var certificatePolicy : PeerCertificatePolicy

public static let `default` = PeerSecurityConfiguration(
encryptionPreference: .optional,
securityIdentity: nil,
certificatePolicy: .acceptAll
)

public static let encrypted = PeerSecurityConfiguration(
encryptionPreference: .required,
securityIdentity: nil,
certificatePolicy: .acceptAll
)
}
```

### Add `PeerCertificatePolicy`

Proposed API:

```swift
public enum PeerCertificatePolicy {
case acceptAll
case rejectAll
case requireCertificate
case custom((Peer, [Any]?, @escaping (Bool) -> Void) -> Void)
}
```

### Implementation Tasks

- Add security configuration to `PeerConnectionManager` initializer, defaulting to backward-compatible behavior.
- Pass security configuration into `PeerSession`.
- Update `PeerSession` to initialize `MCSession` with configured `securityIdentity` and `encryptionPreference`.
- Remove the hardcoded always-accept certificate listener from `PeerConnectionManager.init`.
- Handle `.didReceiveCertificate` directly using `PeerCertificatePolicy`.
- Keep `.receivedCertificate` event only if useful for custom/manual handling, but avoid competing calls to the same certificate handler.

### Tests

- Verify default config maps to `.optional`, `nil`, `.acceptAll`.
- Verify `.encrypted` maps to `.required`.
- Verify `.acceptAll`, `.rejectAll`, and `.requireCertificate` call the handler correctly.
- Verify `.custom` receives peer/certificate and controls acceptance.

## Phase 2 — Discovery Metadata

### Add Discovery Info Support

Proposed additions:

```swift
public typealias PeerDiscoveryInfo = [String:String]
```

- Add `discoveryInfo: PeerDiscoveryInfo?` to `PeerConnectionManager` init/config.
- Pass discovery info to `PeerAdvertiser` and `PeerAdvertiserAssisstant`.
- Preserve browser-provided discovery info in internal browser events.
- Surface discovery info publicly.

Possible public event shape:

```swift
case foundPeer(peer: Peer, discoveryInfo: PeerDiscoveryInfo?)
case nearbyPeersChanged(foundPeers: [Peer])
```

Alternative non-breaking approach:

- Keep existing `.foundPeer(peer:)`.
- Add a new event:

```swift
case foundPeerWithDiscoveryInfo(peer: Peer, discoveryInfo: PeerDiscoveryInfo?)
```

### Documentation Notes

Document that discovery info is advertised over Bonjour TXT records and should not contain secrets, tokens, emails, stable user IDs, or sensitive device information.

Good examples:

- Protocol version
- Non-secret capability flags
- Room/session label that is not secret

## Phase 3 — Invitation Policy

### Add Invitation Policy

Proposed API:

```swift
public enum PeerInvitationPolicy {
case manual
case acceptAll
case rejectAll
case custom((Peer, Data?) -> Bool)
}
```

### Implementation Tasks

- Add invitation policy to manager/config.
- In `.automatic`, replace blind accept with policy-based accept.
- Preserve `.receivedInvitation` for manual/custom modes.
- Consider typed Codable helpers for invitation context later.

### Security Notes

Invitation context is received before session establishment and should be considered unauthenticated. It can support pairing flows, protocol negotiation, and signed challenges, but should not carry raw secrets.

## Phase 4 — Validation and Privacy Hardening

### Service Type Validation

Apple constraints:

- Up to 15 characters.
- ASCII lowercase letters, numbers, and hyphen.
- Bonjour-style service type.

Possible API:

```swift
public static func isValidServiceType(_ serviceType: String) -> Bool
```

Later option:

```swift
public struct PeerServiceType {
public let rawValue : String
}
```

### Display Name Validation

- Validate 63-byte UTF-8 maximum for `MCPeerID(displayName:)`.
- Document that display names are visible to nearby peers.
- Consider adding an anonymous/random display-name helper.
- Reconsider UI convenience default using `UIDevice.current.name`, since that may reveal personal names.

## Phase 5 — UI Filtering

In `PeerConnectivityUI`, expose `MCBrowserViewControllerDelegate.browserViewController(_:shouldPresentNearbyPeer:withDiscoveryInfo:)`.

Use cases:

- Hide peers with incompatible protocol versions.
- Hide peers lacking required advertised capabilities.
- Hide peers outside the intended app room/session.

## Suggested Initial Implementation Scope

Start with Phase 1 only:

1. Add `PeerSecurityConfiguration`.
2. Add `PeerCertificatePolicy`.
3. Thread config into `PeerSession`.
4. Replace hardcoded certificate acceptance.
5. Add focused tests.
6. Update docs/API comments.

This provides the largest security improvement with the least API churn.

## Open Questions

1. Should the default remain `.optional + acceptAll` for backward compatibility, or should the next release default to `.required`?
2. Should `.receivedCertificate` remain a public event, or should certificate handling move entirely to `PeerCertificatePolicy.custom`?
3. Should discovery info be added as a new event to avoid breaking existing switch statements?
4. Should service type validation fail initialization, log warnings, or be exposed only as a helper?
5. Should the iOS UI convenience initializer continue to use `UIDevice.current.name` by default?
8 changes: 8 additions & 0 deletions PeerConnectivity.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
3086CD2D1D09FB9900E269A3 /* PeerConnectivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3086CD221D09FB9800E269A3 /* PeerConnectivity.framework */; };
3086CD321D09FB9900E269A3 /* PeerConnectivityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3086CD311D09FB9900E269A3 /* PeerConnectivityTests.swift */; };
30PEERMSG2602020000000002 /* PeerMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30PEERMSG2602020000000001 /* PeerMessageTests.swift */; };
30SECURITY26060300000001 /* PeerSecurityConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30SECURITY26060300000002 /* PeerSecurityConfiguration.swift */; };
30SECURITY26060300000003 /* PeerSecurityConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30SECURITY26060300000004 /* PeerSecurityConfigurationTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -61,6 +63,8 @@
3086CD2C1D09FB9900E269A3 /* PeerConnectivityTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PeerConnectivityTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3086CD311D09FB9900E269A3 /* PeerConnectivityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerConnectivityTests.swift; sourceTree = "<group>"; };
3086CD331D09FB9900E269A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
30SECURITY26060300000002 /* PeerSecurityConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PeerSecurityConfiguration.swift; path = Sources/PeerSecurityConfiguration.swift; sourceTree = "<group>"; };
30SECURITY26060300000004 /* PeerSecurityConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerSecurityConfigurationTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -100,6 +104,7 @@
3080C7E71D80A1D600AF9EA3 /* PeerConnectionManager.swift */,
3080C7E81D80A1D600AF9EA3 /* PeerConnectionResponder.swift */,
3080C7E91D80A1D600AF9EA3 /* PeerConnectivity.h */,
30SECURITY26060300000002 /* PeerSecurityConfiguration.swift */,
3080C7EA1D80A1D600AF9EA3 /* PeerSession.swift */,
3080C7EB1D80A1D600AF9EA3 /* PeerSessionEventProducer.swift */,
);
Expand Down Expand Up @@ -129,6 +134,7 @@
children = (
3086CD311D09FB9900E269A3 /* PeerConnectivityTests.swift */,
30PEERMSG2602020000000001 /* PeerMessageTests.swift */,
30SECURITY26060300000004 /* PeerSecurityConfigurationTests.swift */,
3086CD331D09FB9900E269A3 /* Info.plist */,
);
path = PeerConnectivityTests;
Expand Down Expand Up @@ -258,6 +264,7 @@
3080C7F31D80A1D700AF9EA3 /* PeerAdvertiserEventProducer.swift in Sources */,
3080C7FC1D80A1D700AF9EA3 /* PeerSessionEventProducer.swift in Sources */,
3080C7F51D80A1D700AF9EA3 /* PeerBrowserAssisstant.swift in Sources */,
30SECURITY26060300000001 /* PeerSecurityConfiguration.swift in Sources */,
3080C7FB1D80A1D700AF9EA3 /* PeerSession.swift in Sources */,
3080C7EE1D80A1D700AF9EA3 /* Observable.swift in Sources */,
3080C7ED1D80A1D700AF9EA3 /* MultiObservable.swift in Sources */,
Expand All @@ -271,6 +278,7 @@
files = (
3086CD321D09FB9900E269A3 /* PeerConnectivityTests.swift in Sources */,
30PEERMSG2602020000000002 /* PeerMessageTests.swift in Sources */,
30SECURITY26060300000003 /* PeerSecurityConfigurationTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
12 changes: 9 additions & 3 deletions PeerConnectivityDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
309837421D8A8D600002338A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 309837411D8A8D600002338A /* Assets.xcassets */; };
309837451D8A8D600002338A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 309837431D8A8D600002338A /* LaunchScreen.storyboard */; };
B1E4E4E82F30592D00AE11AA /* PeerConnectivity in Frameworks */ = {isa = PBXBuildFile; productRef = B1E4E4E72F30592D00AE11AA /* PeerConnectivity */; };
B1E4E4EA2F30592D00AE11AA /* PeerConnectivityUI in Frameworks */ = {isa = PBXBuildFile; productRef = B1E4E4E92F30592D00AE11AA /* PeerConnectivityUI */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -32,6 +33,7 @@
buildActionMask = 2147483647;
files = (
B1E4E4E82F30592D00AE11AA /* PeerConnectivity in Frameworks */,
B1E4E4EA2F30592D00AE11AA /* PeerConnectivityUI in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -123,7 +125,7 @@
);
mainGroup = 3098372E1D8A8D600002338A;
packageReferences = (
B1E4E4E62F30592D00AE11AA /* XCLocalSwiftPackageReference "../PeerConnectivity" */,
B1E4E4E62F30592D00AE11AA /* XCLocalSwiftPackageReference "." */,
);
productRefGroup = 309837381D8A8D600002338A /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -355,9 +357,9 @@
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
B1E4E4E62F30592D00AE11AA /* XCLocalSwiftPackageReference "../PeerConnectivity" */ = {
B1E4E4E62F30592D00AE11AA /* XCLocalSwiftPackageReference "." */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../PeerConnectivity;
relativePath = .;
};
/* End XCLocalSwiftPackageReference section */

Expand All @@ -366,6 +368,10 @@
isa = XCSwiftPackageProductDependency;
productName = PeerConnectivity;
};
B1E4E4E92F30592D00AE11AA /* PeerConnectivityUI */ = {
isa = XCSwiftPackageProductDependency;
productName = PeerConnectivityUI;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 3098372F1D8A8D600002338A /* Project object */;
Expand Down
Loading