serverinfoview
This commit is contained in:
1 parent
d4b4c92759
commit
1650145d4d
12 files changed
+551
-23
No files matched your search
@@ -15,6 +15,7 @@
|
|||||||
03C65C3DAA1D8776AF496EFA /* LumaRoomStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F226A1F8D82B39630F9CEB /* LumaRoomStore.swift */; };
|
03C65C3DAA1D8776AF496EFA /* LumaRoomStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F226A1F8D82B39630F9CEB /* LumaRoomStore.swift */; };
|
||||||
03F2CD213153D0EF5CBC8021 /* EncryptionPreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD286F7AF06DFC5488FF0CEA /* EncryptionPreferenceTests.swift */; };
|
03F2CD213153D0EF5CBC8021 /* EncryptionPreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD286F7AF06DFC5488FF0CEA /* EncryptionPreferenceTests.swift */; };
|
||||||
0535722A2D4FC893F16D78F1 /* MessageReplySwipeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D3F72BE63D8F360ECA0419 /* MessageReplySwipeTests.swift */; };
|
0535722A2D4FC893F16D78F1 /* MessageReplySwipeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D3F72BE63D8F360ECA0419 /* MessageReplySwipeTests.swift */; };
|
||||||
|
059A5169D66AD8F04B341403 /* LumaSaslChallengeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F90741D722FD1A972B4C78 /* LumaSaslChallengeModule.swift */; };
|
||||||
067D121E9C8D8EA076D974FB /* RTCVideoRendererView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5003B4F9DF154543555E9825 /* RTCVideoRendererView.swift */; };
|
067D121E9C8D8EA076D974FB /* RTCVideoRendererView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5003B4F9DF154543555E9825 /* RTCVideoRendererView.swift */; };
|
||||||
087E600676B3F6134BB26112 /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B11FED2D58621C071C3AD14 /* CallView.swift */; };
|
087E600676B3F6134BB26112 /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B11FED2D58621C071C3AD14 /* CallView.swift */; };
|
||||||
088945A14C15828F5265AA29 /* ArchiveSyncRecoveryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28187D29B1BB6E3ECB02F637 /* ArchiveSyncRecoveryPolicy.swift */; };
|
088945A14C15828F5265AA29 /* ArchiveSyncRecoveryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28187D29B1BB6E3ECB02F637 /* ArchiveSyncRecoveryPolicy.swift */; };
|
||||||
@@ -142,6 +143,7 @@
|
|||||||
7AC93485DA12BC90717739E9 /* ChatMediaImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A438A50BC08940CA1410B4FA /* ChatMediaImageCache.swift */; };
|
7AC93485DA12BC90717739E9 /* ChatMediaImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A438A50BC08940CA1410B4FA /* ChatMediaImageCache.swift */; };
|
||||||
7BC399937E4586EC5067282C /* GeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD13FA20B6C2CD8B6718BF86 /* GeoLocation.swift */; };
|
7BC399937E4586EC5067282C /* GeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD13FA20B6C2CD8B6718BF86 /* GeoLocation.swift */; };
|
||||||
7C89D0095051D751FA245324 /* ArchiveMetadataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC7F3C5D27D08B1B01D712C /* ArchiveMetadataRecord.swift */; };
|
7C89D0095051D751FA245324 /* ArchiveMetadataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC7F3C5D27D08B1B01D712C /* ArchiveMetadataRecord.swift */; };
|
||||||
|
7D095DA2AAD5E7D813586E7D /* LumaSaslChallengeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F90741D722FD1A972B4C78 /* LumaSaslChallengeModule.swift */; };
|
||||||
7D93BF350FBBF6F9EB29BA23 /* MediaPickerSelectionPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E6875B522254FFB5FA880E /* MediaPickerSelectionPolicy.swift */; };
|
7D93BF350FBBF6F9EB29BA23 /* MediaPickerSelectionPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E6875B522254FFB5FA880E /* MediaPickerSelectionPolicy.swift */; };
|
||||||
7F012252FA73A8282B92323C /* LumaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C051C2A7014E8B5E825477 /* LumaApp.swift */; };
|
7F012252FA73A8282B92323C /* LumaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C051C2A7014E8B5E825477 /* LumaApp.swift */; };
|
||||||
7F211C3BBA81E5D43D3A88AE /* LumaCallEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72EC7DC2FE873C0BA4321496 /* LumaCallEngine.swift */; };
|
7F211C3BBA81E5D43D3A88AE /* LumaCallEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72EC7DC2FE873C0BA4321496 /* LumaCallEngine.swift */; };
|
||||||
@@ -232,6 +234,7 @@
|
|||||||
DA77E8A49D2D091EC0975DD3 /* InlineVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C81B9255E9886FB86E0785 /* InlineVideoPlayer.swift */; };
|
DA77E8A49D2D091EC0975DD3 /* InlineVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C81B9255E9886FB86E0785 /* InlineVideoPlayer.swift */; };
|
||||||
DBB0216BFAB04C3847FEE95C /* GroupInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA74044D62409FAC6062BC1 /* GroupInfoView.swift */; };
|
DBB0216BFAB04C3847FEE95C /* GroupInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA74044D62409FAC6062BC1 /* GroupInfoView.swift */; };
|
||||||
DC4FAE63AB8AF89C30512763 /* MediaFileIOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772A4F24CAF527E328657749 /* MediaFileIOTests.swift */; };
|
DC4FAE63AB8AF89C30512763 /* MediaFileIOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772A4F24CAF527E328657749 /* MediaFileIOTests.swift */; };
|
||||||
|
DD7E0B9153FCC21BAF6EE655 /* SCRAMDowngradeProtectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F5C75CB772638C15C54F101 /* SCRAMDowngradeProtectionTests.swift */; };
|
||||||
E2245416CB055ECEF0C5F81D /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A338A8EB30206647BDD922F /* SettingsView.swift */; };
|
E2245416CB055ECEF0C5F81D /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A338A8EB30206647BDD922F /* SettingsView.swift */; };
|
||||||
E42A674072CE5D3AF1F5848D /* Conversation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7846C2EEE2551C6690A4FDC6 /* Conversation.swift */; };
|
E42A674072CE5D3AF1F5848D /* Conversation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7846C2EEE2551C6690A4FDC6 /* Conversation.swift */; };
|
||||||
E449F2B17E6D5053EBA7F6A8 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C9866558B72CB1909A01EEC /* Shared.xcassets */; };
|
E449F2B17E6D5053EBA7F6A8 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C9866558B72CB1909A01EEC /* Shared.xcassets */; };
|
||||||
@@ -365,6 +368,7 @@
|
|||||||
4380219D3AF1D75EDF4D3167 /* MediaViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaViewer.swift; sourceTree = "<group>"; };
|
4380219D3AF1D75EDF4D3167 /* MediaViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaViewer.swift; sourceTree = "<group>"; };
|
||||||
43A4CE3E678096BA76F2988C /* NewChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
|
43A4CE3E678096BA76F2988C /* NewChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
|
||||||
47680B7C615374A8C245818F /* AppLockPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockPolicy.swift; sourceTree = "<group>"; };
|
47680B7C615374A8C245818F /* AppLockPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockPolicy.swift; sourceTree = "<group>"; };
|
||||||
|
4F5C75CB772638C15C54F101 /* SCRAMDowngradeProtectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCRAMDowngradeProtectionTests.swift; sourceTree = "<group>"; };
|
||||||
5003B4F9DF154543555E9825 /* RTCVideoRendererView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCVideoRendererView.swift; sourceTree = "<group>"; };
|
5003B4F9DF154543555E9825 /* RTCVideoRendererView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCVideoRendererView.swift; sourceTree = "<group>"; };
|
||||||
5078CBF0520AA3BE7D2E65E0 /* SCRAMSHA512Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCRAMSHA512Tests.swift; sourceTree = "<group>"; };
|
5078CBF0520AA3BE7D2E65E0 /* SCRAMSHA512Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCRAMSHA512Tests.swift; sourceTree = "<group>"; };
|
||||||
515DD6F1F80A848C39EBAE84 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
515DD6F1F80A848C39EBAE84 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||||
@@ -465,6 +469,7 @@
|
|||||||
F0A35C9A7B52458996513382 /* WatchVoiceRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchVoiceRecorder.swift; sourceTree = "<group>"; };
|
F0A35C9A7B52458996513382 /* WatchVoiceRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchVoiceRecorder.swift; sourceTree = "<group>"; };
|
||||||
F207C1B20DE3780F1754F81D /* ConnectionBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionBanner.swift; sourceTree = "<group>"; };
|
F207C1B20DE3780F1754F81D /* ConnectionBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionBanner.swift; sourceTree = "<group>"; };
|
||||||
F26F91180E43DE9C135B68C1 /* EmojiCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCatalog.swift; sourceTree = "<group>"; };
|
F26F91180E43DE9C135B68C1 /* EmojiCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCatalog.swift; sourceTree = "<group>"; };
|
||||||
|
F4F90741D722FD1A972B4C78 /* LumaSaslChallengeModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LumaSaslChallengeModule.swift; sourceTree = "<group>"; };
|
||||||
F69AB3930D5E93CB77EA9C7D /* CryptoKitAESGCMEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoKitAESGCMEngineTests.swift; sourceTree = "<group>"; };
|
F69AB3930D5E93CB77EA9C7D /* CryptoKitAESGCMEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoKitAESGCMEngineTests.swift; sourceTree = "<group>"; };
|
||||||
F6C7D0F207377DE99146A0D2 /* NotificationPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPolicyTests.swift; sourceTree = "<group>"; };
|
F6C7D0F207377DE99146A0D2 /* NotificationPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPolicyTests.swift; sourceTree = "<group>"; };
|
||||||
F8CABD9D1B4C987EF6AB19A6 /* StaticDNSSrvResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticDNSSrvResolver.swift; sourceTree = "<group>"; };
|
F8CABD9D1B4C987EF6AB19A6 /* StaticDNSSrvResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticDNSSrvResolver.swift; sourceTree = "<group>"; };
|
||||||
@@ -509,6 +514,7 @@
|
|||||||
24A68154C2D2B3E9AB0C145F /* LumaOMEMOStore.swift */,
|
24A68154C2D2B3E9AB0C145F /* LumaOMEMOStore.swift */,
|
||||||
E9F226A1F8D82B39630F9CEB /* LumaRoomStore.swift */,
|
E9F226A1F8D82B39630F9CEB /* LumaRoomStore.swift */,
|
||||||
F92830FB64B5BD0A06BC828D /* LumaRosterStore.swift */,
|
F92830FB64B5BD0A06BC828D /* LumaRosterStore.swift */,
|
||||||
|
F4F90741D722FD1A972B4C78 /* LumaSaslChallengeModule.swift */,
|
||||||
C3E21A7D359AF7A7AF97D8DE /* LumaSaslFailureModule.swift */,
|
C3E21A7D359AF7A7AF97D8DE /* LumaSaslFailureModule.swift */,
|
||||||
050B83F6DA3A04B661FFA3B1 /* LumaScramPlusMechanism.swift */,
|
050B83F6DA3A04B661FFA3B1 /* LumaScramPlusMechanism.swift */,
|
||||||
7C16CFC03E7F478838A704FD /* LumaScramSha512Mechanism.swift */,
|
7C16CFC03E7F478838A704FD /* LumaScramSha512Mechanism.swift */,
|
||||||
@@ -655,6 +661,7 @@
|
|||||||
1F7B2B37E9E8E6A46ED7547A /* SaslFailureMessageTests.swift */,
|
1F7B2B37E9E8E6A46ED7547A /* SaslFailureMessageTests.swift */,
|
||||||
37D79A5AC35479F474FF3F44 /* SASLMechanismPreferenceTests.swift */,
|
37D79A5AC35479F474FF3F44 /* SASLMechanismPreferenceTests.swift */,
|
||||||
13EFA01112297641B9B0EBC5 /* SASLprepTests.swift */,
|
13EFA01112297641B9B0EBC5 /* SASLprepTests.swift */,
|
||||||
|
4F5C75CB772638C15C54F101 /* SCRAMDowngradeProtectionTests.swift */,
|
||||||
21D03F8C05843DD139C1A4CE /* SCRAMPlusTests.swift */,
|
21D03F8C05843DD139C1A4CE /* SCRAMPlusTests.swift */,
|
||||||
5078CBF0520AA3BE7D2E65E0 /* SCRAMSHA512Tests.swift */,
|
5078CBF0520AA3BE7D2E65E0 /* SCRAMSHA512Tests.swift */,
|
||||||
9C8B12EFBC83F9235B6AF992 /* VideoNoteRecordingCompletionPolicyTests.swift */,
|
9C8B12EFBC83F9235B6AF992 /* VideoNoteRecordingCompletionPolicyTests.swift */,
|
||||||
@@ -1040,6 +1047,7 @@
|
|||||||
FB1052104B90994CF03FE6A2 /* LumaOMEMOStore.swift in Sources */,
|
FB1052104B90994CF03FE6A2 /* LumaOMEMOStore.swift in Sources */,
|
||||||
CBED8565C1313DC03D21A105 /* LumaRoomStore.swift in Sources */,
|
CBED8565C1313DC03D21A105 /* LumaRoomStore.swift in Sources */,
|
||||||
F1713D1CD4E019540BFA78FE /* LumaRosterStore.swift in Sources */,
|
F1713D1CD4E019540BFA78FE /* LumaRosterStore.swift in Sources */,
|
||||||
|
059A5169D66AD8F04B341403 /* LumaSaslChallengeModule.swift in Sources */,
|
||||||
0BD15E9B1C488AA2B7B56AF7 /* LumaSaslFailureModule.swift in Sources */,
|
0BD15E9B1C488AA2B7B56AF7 /* LumaSaslFailureModule.swift in Sources */,
|
||||||
ED1C54A2FB844226AA284CCC /* LumaScramPlusMechanism.swift in Sources */,
|
ED1C54A2FB844226AA284CCC /* LumaScramPlusMechanism.swift in Sources */,
|
||||||
E6B279889E62AB9AD0C5BCE1 /* LumaScramSha512Mechanism.swift in Sources */,
|
E6B279889E62AB9AD0C5BCE1 /* LumaScramSha512Mechanism.swift in Sources */,
|
||||||
@@ -1159,6 +1167,7 @@
|
|||||||
1B20F2D3E4F18E6094B103BF /* LumaOMEMOStore.swift in Sources */,
|
1B20F2D3E4F18E6094B103BF /* LumaOMEMOStore.swift in Sources */,
|
||||||
03C65C3DAA1D8776AF496EFA /* LumaRoomStore.swift in Sources */,
|
03C65C3DAA1D8776AF496EFA /* LumaRoomStore.swift in Sources */,
|
||||||
7F4C00E98574C8E5193F442E /* LumaRosterStore.swift in Sources */,
|
7F4C00E98574C8E5193F442E /* LumaRosterStore.swift in Sources */,
|
||||||
|
7D095DA2AAD5E7D813586E7D /* LumaSaslChallengeModule.swift in Sources */,
|
||||||
C1AB954660804F7925607038 /* LumaSaslFailureModule.swift in Sources */,
|
C1AB954660804F7925607038 /* LumaSaslFailureModule.swift in Sources */,
|
||||||
FCC5133038CDBEC39B5B42D3 /* LumaScramPlusMechanism.swift in Sources */,
|
FCC5133038CDBEC39B5B42D3 /* LumaScramPlusMechanism.swift in Sources */,
|
||||||
127C4B7A5083081D22FD21CD /* LumaScramSha512Mechanism.swift in Sources */,
|
127C4B7A5083081D22FD21CD /* LumaScramSha512Mechanism.swift in Sources */,
|
||||||
@@ -1245,6 +1254,7 @@
|
|||||||
9E98F99F3FBFC9DAB99A3650 /* NotificationPolicyTests.swift in Sources */,
|
9E98F99F3FBFC9DAB99A3650 /* NotificationPolicyTests.swift in Sources */,
|
||||||
5A5C811D7D19AE7C694273FC /* SASLMechanismPreferenceTests.swift in Sources */,
|
5A5C811D7D19AE7C694273FC /* SASLMechanismPreferenceTests.swift in Sources */,
|
||||||
1B7C9F60AA7BFF6A2C0FE45D /* SASLprepTests.swift in Sources */,
|
1B7C9F60AA7BFF6A2C0FE45D /* SASLprepTests.swift in Sources */,
|
||||||
|
DD7E0B9153FCC21BAF6EE655 /* SCRAMDowngradeProtectionTests.swift in Sources */,
|
||||||
662B8E72F00F5A77DE28819A /* SCRAMPlusTests.swift in Sources */,
|
662B8E72F00F5A77DE28819A /* SCRAMPlusTests.swift in Sources */,
|
||||||
3BF0521699C170F8411386CD /* SCRAMSHA512Tests.swift in Sources */,
|
3BF0521699C170F8411386CD /* SCRAMSHA512Tests.swift in Sources */,
|
||||||
3DDB1B15BCE5B58D12B60558 /* SaslFailureMessageTests.swift in Sources */,
|
3DDB1B15BCE5B58D12B60558 /* SaslFailureMessageTests.swift in Sources */,
|
||||||
|
|||||||
+1
-1
@@ -366,7 +366,7 @@ grep -q 'SCRAM-SHA-512' Sources/Shared/XMPP/LumaScramSha512Mechanism.swift || {
|
|||||||
echo "The SCRAM-SHA-512 mechanism must be available"
|
echo "The SCRAM-SHA-512 mechanism must be available"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
grep -q 'addMechanism(LumaScramSha512Mechanism' Sources/Shared/XMPP/XMPPService.swift || {
|
grep -q 'LumaScramSha512Mechanism(channelBindingStore: channelBindingStore)' Sources/Shared/XMPP/XMPPService.swift || {
|
||||||
echo "SCRAM-SHA-512 must be registered ahead of Martin's mechanisms"
|
echo "SCRAM-SHA-512 must be registered ahead of Martin's mechanisms"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ enum SASLMechanismPreference {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mechanisms that send credentials in a way a TLS MITM can replay or
|
||||||
|
/// read directly; shown with a warning on the server-information screen.
|
||||||
|
static let weakMechanisms: Set<String> = ["PLAIN", "OAUTHBEARER"]
|
||||||
|
|
||||||
/// True when `mechanism` is the strongest one available among
|
/// True when `mechanism` is the strongest one available among
|
||||||
/// `offered`: the server offers nothing better than the chosen one.
|
/// `offered`: the server offers nothing better than the chosen one.
|
||||||
static func isStrongestAvailable(_ mechanism: String, among offered: [String]) -> Bool {
|
static func isStrongestAvailable(_ mechanism: String, among offered: [String]) -> Bool {
|
||||||
|
|||||||
@@ -72,6 +72,20 @@ struct ServerInformation: Equatable, Sendable {
|
|||||||
let externalServices: [ExternalService]
|
let externalServices: [ExternalService]
|
||||||
let connectionStats: ConnectionStats
|
let connectionStats: ConnectionStats
|
||||||
let saslMethods: [String]
|
let saslMethods: [String]
|
||||||
|
/// SASL2 (RFC 9050) mechanisms advertised in `<authentication/>`.
|
||||||
|
let sasl2Methods: [String]
|
||||||
|
/// XEP-0440 channel-binding types advertised by the server, used to
|
||||||
|
/// detect MITM attacks on the TLS layer during SCRAM-PLUS.
|
||||||
|
let channelBindingTypes: [String]
|
||||||
|
/// The channel-binding type actually used for this connection's SASL
|
||||||
|
/// exchange, when a -PLUS mechanism was selected.
|
||||||
|
let usedChannelBindingType: String?
|
||||||
|
/// True when the server included its XEP-0474 downgrade-protection
|
||||||
|
/// hash in the SCRAM exchange (`h` attribute of the server-first message).
|
||||||
|
let supportsSCRAMDowngradeProtection: Bool
|
||||||
|
/// Raw name of the SASL mechanism used for this connection's auth
|
||||||
|
/// (without the channel-binding suffix), highlighted in the method lists.
|
||||||
|
let usedSASLMechanism: String?
|
||||||
/// TLS version negotiated with the server during this connection
|
/// TLS version negotiated with the server during this connection
|
||||||
/// (probed from a parallel handshake when the library does not expose it).
|
/// (probed from a parallel handshake when the library does not expose it).
|
||||||
let tlsVersion: String?
|
let tlsVersion: String?
|
||||||
@@ -156,8 +170,10 @@ struct ServerInformation: Equatable, Sendable {
|
|||||||
),
|
),
|
||||||
Capability(
|
Capability(
|
||||||
title: "XEP-0474 SASL SCRAM Downgrade Protection",
|
title: "XEP-0474 SASL SCRAM Downgrade Protection",
|
||||||
detail: "Защита SASL/SASL2-рукопожатия от понижения метода и channel-binding. Определяется во время SCRAM-аутентификации, а не рекламируется статически.",
|
detail: supportsSCRAMDowngradeProtection
|
||||||
status: .normal
|
? "Сервер включил downgrade-protection hash в SCRAM-обмен (атрибут h): списки SASL-механизмов и channel-binding защищены от подмены MITM."
|
||||||
|
: "Сервер не включил downgrade-protection hash в SCRAM-обмен (или вход выполнен через PLAIN): списки механизмов и channel-binding не защищены от подмены.",
|
||||||
|
status: supportsSCRAMDowngradeProtection ? .success : .error
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,18 @@ struct ServerInfoView: View {
|
|||||||
Section("Методы аутентификации SASL, которые поддерживает ваш сервер.") {
|
Section("Методы аутентификации SASL, которые поддерживает ваш сервер.") {
|
||||||
saslEntries(info)
|
saslEntries(info)
|
||||||
}
|
}
|
||||||
|
Section("Методы аутентификации SASL2 (RFC 9050), которые поддерживает ваш сервер.") {
|
||||||
|
sasl2Entries(info)
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
channelBindingEntries(info)
|
||||||
|
} header: {
|
||||||
|
Text("Привязка канала (channel binding), которую поддерживает ваш сервер.")
|
||||||
|
} footer: {
|
||||||
|
Text(
|
||||||
|
"Channel binding связывает SASL-обмен с TLS-каналом: MITM-ретрансляция не сможет подменить соединение. Зелёным отмечен тип, использованный в текущем соединении, оранжевым — типы, которые Luma не поддерживает."
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if isLoading {
|
} else if isLoading {
|
||||||
Section {
|
Section {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
@@ -233,13 +245,101 @@ struct ServerInfoView: View {
|
|||||||
ForEach(info.saslMethods, id: \.self) { method in
|
ForEach(info.saslMethods, id: \.self) { method in
|
||||||
entry(
|
entry(
|
||||||
title: "Метод: \(method)",
|
title: "Метод: \(method)",
|
||||||
detail: saslDescription(method),
|
detail: method == info.usedSASLMechanism
|
||||||
status: method == "PLAIN" ? .warning : .normal
|
? "Используется в текущем соединении. " + saslDescription(method)
|
||||||
|
: saslDescription(method),
|
||||||
|
status: method == info.usedSASLMechanism
|
||||||
|
? .success
|
||||||
|
: (SASLMechanismPreference.weakMechanisms.contains(method)
|
||||||
|
? .warning
|
||||||
|
: .normal)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func sasl2Entries(_ info: ServerInformation) -> some View {
|
||||||
|
if info.sasl2Methods.isEmpty {
|
||||||
|
entry(
|
||||||
|
title: "Нет",
|
||||||
|
detail: "Сервер не объявил методы аутентификации SASL2 (RFC 9050).",
|
||||||
|
status: .normal
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ForEach(info.sasl2Methods, id: \.self) { method in
|
||||||
|
entry(
|
||||||
|
title: "Метод: \(method)",
|
||||||
|
detail: method == info.usedSASLMechanism
|
||||||
|
? "Используется в текущем соединении. " + sasl2Description(method)
|
||||||
|
: sasl2Description(method),
|
||||||
|
status: method == info.usedSASLMechanism
|
||||||
|
? .success
|
||||||
|
: (SASLMechanismPreference.weakMechanisms.contains(method)
|
||||||
|
? .warning
|
||||||
|
: .normal)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sasl2Description(_ method: String) -> String {
|
||||||
|
switch method {
|
||||||
|
case "PLAIN":
|
||||||
|
return "Отправляет пароль открытым текстом (шифруется только TLS), не очень безопасно."
|
||||||
|
case "OAUTHBEARER":
|
||||||
|
return "OAuth 2.0 bearer-токен: пароль не передаётся, но сам токен — bearer-секрет, не очень безопасно."
|
||||||
|
case "EXTERNAL":
|
||||||
|
return "Использует TLS-клиентские сертификаты для аутентификации."
|
||||||
|
case let method where method.hasPrefix("SCRAM-") && method.hasSuffix("-PLUS"):
|
||||||
|
return "Salted Challenge Response Authentication Mechanism с channel-binding."
|
||||||
|
case let method where method.hasPrefix("SCRAM-"):
|
||||||
|
return "Salted Challenge Response Authentication Mechanism на указанном хэше."
|
||||||
|
default:
|
||||||
|
return "Неизвестный метод аутентификации."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel-binding types Luma can actually produce binding data for.
|
||||||
|
private static let supportedChannelBindingTypes: Set<String> = [
|
||||||
|
"tls-exporter", "tls-server-end-point",
|
||||||
|
]
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func channelBindingEntries(_ info: ServerInformation) -> some View {
|
||||||
|
if info.channelBindingTypes.isEmpty {
|
||||||
|
entry(
|
||||||
|
title: "Нет",
|
||||||
|
detail: "Сервер не рекламирует channel-binding типы (XEP-0440): SASL-обмен не детектирует MITM-атаку на TLS-слой.",
|
||||||
|
status: .warning
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ForEach(info.channelBindingTypes, id: \.self) { type in
|
||||||
|
let used = info.usedChannelBindingType == type
|
||||||
|
let supported = Self.supportedChannelBindingTypes.contains(type)
|
||||||
|
entry(
|
||||||
|
title: "Тип: \(type)",
|
||||||
|
detail: channelBindingDescription(type, used: used, supported: supported),
|
||||||
|
status: used ? .success : (supported ? .normal : .warning)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func channelBindingDescription(
|
||||||
|
_ type: String,
|
||||||
|
used: Bool,
|
||||||
|
supported: Bool
|
||||||
|
) -> String {
|
||||||
|
if used {
|
||||||
|
return "Используется в текущем соединении."
|
||||||
|
}
|
||||||
|
if supported {
|
||||||
|
return "Luma поддерживает этот тип привязки."
|
||||||
|
}
|
||||||
|
return "Luma не поддерживает этот тип привязки."
|
||||||
|
}
|
||||||
|
|
||||||
private func saslDescription(_ method: String) -> String {
|
private func saslDescription(_ method: String) -> String {
|
||||||
switch method {
|
switch method {
|
||||||
case "PLAIN":
|
case "PLAIN":
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import Foundation
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
/// Observes the raw SASL `<challenge/>` stanzas before `SaslModule` consumes
|
||||||
|
/// them, so Luma can detect the XEP-0474 downgrade-protection hash (`h=`
|
||||||
|
/// attribute in the SCRAM server-first message) regardless of which SCRAM
|
||||||
|
/// mechanism implementation actually runs the exchange. Registered before
|
||||||
|
/// `SaslModule` in the module list.
|
||||||
|
final class LumaSaslChallengeModule: XmppModuleBase, XmppModule {
|
||||||
|
static let ID = "lumaSaslChallenge"
|
||||||
|
static let saslXMLNS = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||||
|
|
||||||
|
let criteria = Criteria.name("challenge", xmlns: LumaSaslChallengeModule.saslXMLNS)
|
||||||
|
let features: [String] = []
|
||||||
|
|
||||||
|
private let onChallenge: (String) -> Void
|
||||||
|
|
||||||
|
init(onChallenge: @escaping (String) -> Void) {
|
||||||
|
self.onChallenge = onChallenge
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func process(stanza: Stanza) throws {
|
||||||
|
onChallenge(stanza.element.value ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,40 @@ enum SCRAMHash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// XEP-0474 SASL SCRAM Downgrade Protection: the hash both sides include in
|
||||||
|
/// the SCRAM exchange (server `h` / client `x` attributes). Per §5.1 the
|
||||||
|
/// input is the server-advertised SASL mechanisms SORTED with i;octet
|
||||||
|
/// collation and joined with 0x1E; when the server also advertised
|
||||||
|
/// channel-binding types, a 0x1F delimiter followed by the sorted
|
||||||
|
/// channel-binding types joined with 0x1E. The digest is the SCRAM hash of
|
||||||
|
/// the negotiated mechanism. A mismatch means a MITM tampered with the
|
||||||
|
/// advertised lists.
|
||||||
|
enum SCRAMDowngradeProtection {
|
||||||
|
static func hash(
|
||||||
|
mechanisms: [String],
|
||||||
|
channelBindingTypes: [String],
|
||||||
|
using hash: SCRAMHash
|
||||||
|
) -> Data {
|
||||||
|
// "i;octet" collation = byte-wise comparison; for these pure-ASCII
|
||||||
|
// identifiers plain lexicographic sorting matches it.
|
||||||
|
let sortedMechanisms = mechanisms.sorted()
|
||||||
|
let sortedBindings = channelBindingTypes.sorted()
|
||||||
|
var input = Data()
|
||||||
|
for (index, mechanism) in sortedMechanisms.enumerated() {
|
||||||
|
if index > 0 { input.append(0x1E) }
|
||||||
|
input.append(contentsOf: mechanism.utf8)
|
||||||
|
}
|
||||||
|
if !sortedBindings.isEmpty {
|
||||||
|
input.append(0x1F)
|
||||||
|
for (index, type) in sortedBindings.enumerated() {
|
||||||
|
if index > 0 { input.append(0x1E) }
|
||||||
|
input.append(contentsOf: type.utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hash.hash(data: input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Pure SCRAM channel-binding exchange math (RFC 5802 + RFC 9266): given
|
/// Pure SCRAM channel-binding exchange math (RFC 5802 + RFC 9266): given
|
||||||
/// the fixed inputs of one exchange it produces the wire messages and the
|
/// the fixed inputs of one exchange it produces the wire messages and the
|
||||||
/// expected server signature. Kept independent of Martin's SaslMechanism
|
/// expected server signature. Kept independent of Martin's SaslMechanism
|
||||||
@@ -116,7 +150,10 @@ struct SCRAMPlusExchange {
|
|||||||
(gs2Header.data(using: .utf8) ?? Data()) + channelBindingData
|
(gs2Header.data(using: .utf8) ?? Data()) + channelBindingData
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientFinalMessage(serverFirst: String) throws -> String {
|
func clientFinalMessage(
|
||||||
|
serverFirst: String,
|
||||||
|
downgradeProtectionHashBase64: String? = nil
|
||||||
|
) throws -> String {
|
||||||
let parsed = try SCRAMSHA512.parseServerFirst(
|
let parsed = try SCRAMSHA512.parseServerFirst(
|
||||||
serverFirst,
|
serverFirst,
|
||||||
expectedNoncePrefix: clientNonce
|
expectedNoncePrefix: clientNonce
|
||||||
@@ -126,9 +163,15 @@ struct SCRAMPlusExchange {
|
|||||||
salt: parsed.salt,
|
salt: parsed.salt,
|
||||||
iterations: parsed.iterations
|
iterations: parsed.iterations
|
||||||
)
|
)
|
||||||
let finalWithoutProof =
|
let finalWithoutProof = finalWithoutProofString(
|
||||||
"c=\(channelBindingInput.base64EncodedString()),r=\(parsed.nonce)"
|
parsed: parsed,
|
||||||
let authMessage = authMessageString(serverFirst: serverFirst, parsed: parsed)
|
downgradeProtectionHashBase64: downgradeProtectionHashBase64
|
||||||
|
)
|
||||||
|
let authMessage = authMessageString(
|
||||||
|
serverFirst: serverFirst,
|
||||||
|
parsed: parsed,
|
||||||
|
downgradeProtectionHashBase64: downgradeProtectionHashBase64
|
||||||
|
)
|
||||||
let clientKey = hash.hmac(key: saltedPassword, data: Data("Client Key".utf8))
|
let clientKey = hash.hmac(key: saltedPassword, data: Data("Client Key".utf8))
|
||||||
let storedKey = hash.hash(data: clientKey)
|
let storedKey = hash.hash(data: clientKey)
|
||||||
let signature = hash.hmac(key: storedKey, data: Data(authMessage.utf8))
|
let signature = hash.hmac(key: storedKey, data: Data(authMessage.utf8))
|
||||||
@@ -136,7 +179,10 @@ struct SCRAMPlusExchange {
|
|||||||
return finalWithoutProof + ",p=" + proof.base64EncodedString()
|
return finalWithoutProof + ",p=" + proof.base64EncodedString()
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectedServerSignature(serverFirst: String) throws -> Data {
|
func expectedServerSignature(
|
||||||
|
serverFirst: String,
|
||||||
|
downgradeProtectionHashBase64: String? = nil
|
||||||
|
) throws -> Data {
|
||||||
let parsed = try SCRAMSHA512.parseServerFirst(
|
let parsed = try SCRAMSHA512.parseServerFirst(
|
||||||
serverFirst,
|
serverFirst,
|
||||||
expectedNoncePrefix: clientNonce
|
expectedNoncePrefix: clientNonce
|
||||||
@@ -146,17 +192,36 @@ struct SCRAMPlusExchange {
|
|||||||
salt: parsed.salt,
|
salt: parsed.salt,
|
||||||
iterations: parsed.iterations
|
iterations: parsed.iterations
|
||||||
)
|
)
|
||||||
let authMessage = authMessageString(serverFirst: serverFirst, parsed: parsed)
|
let authMessage = authMessageString(
|
||||||
|
serverFirst: serverFirst,
|
||||||
|
parsed: parsed,
|
||||||
|
downgradeProtectionHashBase64: downgradeProtectionHashBase64
|
||||||
|
)
|
||||||
let serverKey = hash.hmac(key: saltedPassword, data: Data("Server Key".utf8))
|
let serverKey = hash.hmac(key: saltedPassword, data: Data("Server Key".utf8))
|
||||||
return hash.hmac(key: serverKey, data: Data(authMessage.utf8))
|
return hash.hmac(key: serverKey, data: Data(authMessage.utf8))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func finalWithoutProofString(
|
||||||
|
parsed: SCRAMSHA512.ServerFirst,
|
||||||
|
downgradeProtectionHashBase64: String?
|
||||||
|
) -> String {
|
||||||
|
var result =
|
||||||
|
"c=\(channelBindingInput.base64EncodedString()),r=\(parsed.nonce)"
|
||||||
|
if let downgradeProtectionHashBase64 {
|
||||||
|
result += ",x=\(downgradeProtectionHashBase64)"
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
private func authMessageString(
|
private func authMessageString(
|
||||||
serverFirst: String,
|
serverFirst: String,
|
||||||
parsed: SCRAMSHA512.ServerFirst
|
parsed: SCRAMSHA512.ServerFirst,
|
||||||
|
downgradeProtectionHashBase64: String?
|
||||||
) -> String {
|
) -> String {
|
||||||
let finalWithoutProof =
|
let finalWithoutProof = finalWithoutProofString(
|
||||||
"c=\(channelBindingInput.base64EncodedString()),r=\(parsed.nonce)"
|
parsed: parsed,
|
||||||
|
downgradeProtectionHashBase64: downgradeProtectionHashBase64
|
||||||
|
)
|
||||||
return clientFirstMessageBare + "," + serverFirst + "," + finalWithoutProof
|
return clientFirstMessageBare + "," + serverFirst + "," + finalWithoutProof
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +249,9 @@ final class LumaScramPlusMechanism: SaslMechanism {
|
|||||||
private var stage = 0
|
private var stage = 0
|
||||||
private var exchange: SCRAMPlusExchange?
|
private var exchange: SCRAMPlusExchange?
|
||||||
private var serverFirst: String?
|
private var serverFirst: String?
|
||||||
|
/// XEP-0474: our own downgrade-protection hash sent in the `x`
|
||||||
|
/// attribute once the server has presented its `h` attribute.
|
||||||
|
private var downgradeHashBase64: String?
|
||||||
|
|
||||||
init(hash: SCRAMHash, channelBindingStore: LumaChannelBindingStore) {
|
init(hash: SCRAMHash, channelBindingStore: LumaChannelBindingStore) {
|
||||||
self.hash = hash
|
self.hash = hash
|
||||||
@@ -197,6 +265,7 @@ final class LumaScramPlusMechanism: SaslMechanism {
|
|||||||
stage = 0
|
stage = 0
|
||||||
exchange = nil
|
exchange = nil
|
||||||
serverFirst = nil
|
serverFirst = nil
|
||||||
|
downgradeHashBase64 = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAllowedToUse(_ context: Context) -> Bool {
|
func isAllowedToUse(_ context: Context) -> Bool {
|
||||||
@@ -243,7 +312,34 @@ final class LumaScramPlusMechanism: SaslMechanism {
|
|||||||
throw ClientSaslException.badChallenge(msg: "Invalid challenge")
|
throw ClientSaslException.badChallenge(msg: "Invalid challenge")
|
||||||
}
|
}
|
||||||
serverFirst = decodedServerFirst
|
serverFirst = decodedServerFirst
|
||||||
let final = try exchange.clientFinalMessage(serverFirst: decodedServerFirst)
|
// XEP-0474: verify the server's `h` downgrade-protection hash over
|
||||||
|
// the advertised mechanism and channel-binding lists, and answer
|
||||||
|
// with our own hash in `x`. A mismatch means a MITM tampered with
|
||||||
|
// the advertised lists.
|
||||||
|
var downgradeHash: String?
|
||||||
|
if let serverHash = try SCRAMSHA512.parseServerFirst(
|
||||||
|
decodedServerFirst,
|
||||||
|
expectedNoncePrefix: exchange.clientNonce
|
||||||
|
).downgradeProtectionHash {
|
||||||
|
channelBindingStore.markDowngradeProtectionDetected()
|
||||||
|
let expected = SCRAMDowngradeProtection.hash(
|
||||||
|
mechanisms: channelBindingStore.advertisedSASLMechanismsOrdered,
|
||||||
|
channelBindingTypes: channelBindingStore.advertisedChannelBindingTypesOrdered,
|
||||||
|
using: hash
|
||||||
|
)
|
||||||
|
let expectedBase64 = expected.base64EncodedString()
|
||||||
|
guard expectedBase64 == serverHash else {
|
||||||
|
throw ClientSaslException.badChallenge(
|
||||||
|
msg: "SCRAM downgrade protection hash mismatch (possible MITM)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
downgradeHash = expectedBase64
|
||||||
|
}
|
||||||
|
downgradeHashBase64 = downgradeHash
|
||||||
|
let final = try exchange.clientFinalMessage(
|
||||||
|
serverFirst: decodedServerFirst,
|
||||||
|
downgradeProtectionHashBase64: downgradeHash
|
||||||
|
)
|
||||||
stage = 2
|
stage = 2
|
||||||
return final.data(using: .utf8)?.base64EncodedString()
|
return final.data(using: .utf8)?.base64EncodedString()
|
||||||
|
|
||||||
@@ -257,7 +353,10 @@ final class LumaScramPlusMechanism: SaslMechanism {
|
|||||||
else {
|
else {
|
||||||
throw ClientSaslException.badChallenge(msg: "Invalid final challenge")
|
throw ClientSaslException.badChallenge(msg: "Invalid final challenge")
|
||||||
}
|
}
|
||||||
let expected = try exchange.expectedServerSignature(serverFirst: serverFirst)
|
let expected = try exchange.expectedServerSignature(
|
||||||
|
serverFirst: serverFirst,
|
||||||
|
downgradeProtectionHashBase64: downgradeHashBase64
|
||||||
|
)
|
||||||
guard value == expected else {
|
guard value == expected else {
|
||||||
throw ClientSaslException.invalidServerSignature
|
throw ClientSaslException.invalidServerSignature
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ enum SCRAMSHA512 {
|
|||||||
let nonce: String
|
let nonce: String
|
||||||
let salt: Data
|
let salt: Data
|
||||||
let iterations: Int
|
let iterations: Int
|
||||||
|
/// XEP-0474: base64 downgrade-protection hash in the optional `h=`
|
||||||
|
/// SCRAM attribute, when the server supports the extension.
|
||||||
|
let downgradeProtectionHash: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let nonceAlphabet = Array(
|
private static let nonceAlphabet = Array(
|
||||||
@@ -25,7 +28,7 @@ enum SCRAMSHA512 {
|
|||||||
_ message: String,
|
_ message: String,
|
||||||
expectedNoncePrefix: String
|
expectedNoncePrefix: String
|
||||||
) throws -> ServerFirst {
|
) throws -> ServerFirst {
|
||||||
let pattern = #"^(?:m=[^\000=]+,)?r=([\x21-\x2B\x2D-\x7E]+),s=([a-zA-Z0-9/+=]+),i=(\d+)(?:,.*)?$"#
|
let pattern = #"^(?:m=[^\000=]+,)?r=([\x21-\x2B\x2D-\x7E]+),s=([a-zA-Z0-9/+=]+),i=(\d+)(?:,h=([a-zA-Z0-9/+=]+))?(?:,.*)?$"#
|
||||||
guard let regex = try? NSRegularExpression(pattern: pattern) else {
|
guard let regex = try? NSRegularExpression(pattern: pattern) else {
|
||||||
throw ClientSaslException.badChallenge(msg: "Failed to parse challenge")
|
throw ClientSaslException.badChallenge(msg: "Failed to parse challenge")
|
||||||
}
|
}
|
||||||
@@ -39,12 +42,23 @@ enum SCRAMSHA512 {
|
|||||||
}
|
}
|
||||||
let nonce = String(message[nonceRange])
|
let nonce = String(message[nonceRange])
|
||||||
let iterations = Int(message[iterationsRange]) ?? 0
|
let iterations = Int(message[iterationsRange]) ?? 0
|
||||||
|
let downgradeProtectionHash: String?
|
||||||
|
if let hashRange = Range(match.range(at: 4), in: message) {
|
||||||
|
downgradeProtectionHash = String(message[hashRange])
|
||||||
|
} else {
|
||||||
|
downgradeProtectionHash = nil
|
||||||
|
}
|
||||||
guard nonce.hasPrefix(expectedNoncePrefix),
|
guard nonce.hasPrefix(expectedNoncePrefix),
|
||||||
let salt = Data(base64Encoded: String(message[saltRange])),
|
let salt = Data(base64Encoded: String(message[saltRange])),
|
||||||
iterations > 0 else {
|
iterations > 0 else {
|
||||||
throw ClientSaslException.badChallenge(msg: "Invalid challenge")
|
throw ClientSaslException.badChallenge(msg: "Invalid challenge")
|
||||||
}
|
}
|
||||||
return ServerFirst(nonce: nonce, salt: salt, iterations: iterations)
|
return ServerFirst(
|
||||||
|
nonce: nonce,
|
||||||
|
salt: salt,
|
||||||
|
iterations: iterations,
|
||||||
|
downgradeProtectionHash: downgradeProtectionHash
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PBKDF2-HMAC-SHA-512 (RFC 5802 \"Hi\" function).
|
/// PBKDF2-HMAC-SHA-512 (RFC 5802 \"Hi\" function).
|
||||||
@@ -114,12 +128,21 @@ final class LumaScramSha512Mechanism: SaslMechanism {
|
|||||||
let name = "SCRAM-SHA-512"
|
let name = "SCRAM-SHA-512"
|
||||||
private(set) var status: SaslMechanismStatus = .new
|
private(set) var status: SaslMechanismStatus = .new
|
||||||
|
|
||||||
|
/// Optional channel-binding store: when present the mechanism verifies
|
||||||
|
/// the XEP-0474 downgrade-protection hash from the server and answers
|
||||||
|
/// with its own hash.
|
||||||
|
private let channelBindingStore: LumaChannelBindingStore?
|
||||||
|
|
||||||
private var stage = 0
|
private var stage = 0
|
||||||
private var clientNonce = ""
|
private var clientNonce = ""
|
||||||
private var clientFirstMessageBare = ""
|
private var clientFirstMessageBare = ""
|
||||||
private var authMessage = ""
|
private var authMessage = ""
|
||||||
private var saltedPassword: [UInt8] = []
|
private var saltedPassword: [UInt8] = []
|
||||||
|
|
||||||
|
init(channelBindingStore: LumaChannelBindingStore? = nil) {
|
||||||
|
self.channelBindingStore = channelBindingStore
|
||||||
|
}
|
||||||
|
|
||||||
func reset(scopes: Set<ResetableScope>) {
|
func reset(scopes: Set<ResetableScope>) {
|
||||||
guard scopes.contains(.stream) else { return }
|
guard scopes.contains(.stream) else { return }
|
||||||
status = .new
|
status = .new
|
||||||
@@ -168,7 +191,30 @@ final class LumaScramSha512Mechanism: SaslMechanism {
|
|||||||
serverFirst,
|
serverFirst,
|
||||||
expectedNoncePrefix: clientNonce
|
expectedNoncePrefix: clientNonce
|
||||||
)
|
)
|
||||||
|
// XEP-0474: the optional `h` attribute carries the server's
|
||||||
|
// downgrade-protection hash over the advertised mechanism and
|
||||||
|
// channel-binding lists. Verify it (a mismatch means a MITM
|
||||||
|
// tampered with the lists) and answer with our own hash in `x`.
|
||||||
|
var downgradeHashBase64: String?
|
||||||
|
if let serverHash = parsed.downgradeProtectionHash {
|
||||||
|
channelBindingStore?.markDowngradeProtectionDetected()
|
||||||
|
let mechanisms = channelBindingStore?.advertisedSASLMechanismsOrdered ?? []
|
||||||
|
let bindingTypes = channelBindingStore?.advertisedChannelBindingTypesOrdered ?? []
|
||||||
|
let expected = SCRAMDowngradeProtection.hash(
|
||||||
|
mechanisms: mechanisms,
|
||||||
|
channelBindingTypes: bindingTypes,
|
||||||
|
using: .sha512
|
||||||
|
)
|
||||||
|
let expectedBase64 = expected.base64EncodedString()
|
||||||
|
guard expectedBase64 == serverHash else {
|
||||||
|
throw ClientSaslException.badChallenge(
|
||||||
|
msg: "SCRAM downgrade protection hash mismatch (possible MITM)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
downgradeHashBase64 = expectedBase64
|
||||||
|
}
|
||||||
let clientFinalWithoutProof = "c=biws,r=\(parsed.nonce)"
|
let clientFinalWithoutProof = "c=biws,r=\(parsed.nonce)"
|
||||||
|
+ (downgradeHashBase64.map { ",x=\($0)" } ?? "")
|
||||||
authMessage = clientFirstMessageBare + "," + serverFirst + "," + clientFinalWithoutProof
|
authMessage = clientFirstMessageBare + "," + serverFirst + "," + clientFinalWithoutProof
|
||||||
saltedPassword = SCRAMSHA512.saltedPassword(
|
saltedPassword = SCRAMSHA512.saltedPassword(
|
||||||
password: password,
|
password: password,
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ final class LumaChannelBindingStore: @unchecked Sendable {
|
|||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private var tlsState: LumaTLSState?
|
private var tlsState: LumaTLSState?
|
||||||
private var advertisedTypes: Set<String> = []
|
private var advertisedTypes: Set<String> = []
|
||||||
|
/// Ordered copies of the advertised SASL mechanism and channel-binding
|
||||||
|
/// lists — XEP-0474 hashes them in advertisement order.
|
||||||
|
private var mechanismsOrdered: [String] = []
|
||||||
|
private var channelBindingTypesOrdered: [String] = []
|
||||||
|
private var downgradeProtectionDetected = false
|
||||||
|
|
||||||
func setTLSState(_ state: LumaTLSState) {
|
func setTLSState(_ state: LumaTLSState) {
|
||||||
lock.lock()
|
lock.lock()
|
||||||
@@ -38,10 +43,49 @@ final class LumaChannelBindingStore: @unchecked Sendable {
|
|||||||
lock.unlock()
|
lock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Records the advertised lists for the XEP-0474 downgrade-protection
|
||||||
|
/// hash and the server-information screen.
|
||||||
|
func setSASLContext(
|
||||||
|
mechanisms: [String],
|
||||||
|
channelBindingTypes: [String]
|
||||||
|
) {
|
||||||
|
lock.lock()
|
||||||
|
mechanismsOrdered = mechanisms
|
||||||
|
channelBindingTypesOrdered = channelBindingTypes
|
||||||
|
lock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func markDowngradeProtectionDetected() {
|
||||||
|
lock.lock()
|
||||||
|
downgradeProtectionDetected = true
|
||||||
|
lock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDowngradeProtectionDetected: Bool {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
return downgradeProtectionDetected
|
||||||
|
}
|
||||||
|
|
||||||
|
var advertisedSASLMechanismsOrdered: [String] {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
return mechanismsOrdered
|
||||||
|
}
|
||||||
|
|
||||||
|
var advertisedChannelBindingTypesOrdered: [String] {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
return channelBindingTypesOrdered
|
||||||
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
lock.lock()
|
lock.lock()
|
||||||
tlsState = nil
|
tlsState = nil
|
||||||
advertisedTypes = []
|
advertisedTypes = []
|
||||||
|
mechanismsOrdered = []
|
||||||
|
channelBindingTypesOrdered = []
|
||||||
|
downgradeProtectionDetected = false
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +131,13 @@ final class LumaChannelBindingStore: @unchecked Sendable {
|
|||||||
var canUseChannelBinding: Bool {
|
var canUseChannelBinding: Bool {
|
||||||
preferredChannelBindingType != nil
|
preferredChannelBindingType != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Channel-binding types the server advertised in its stream features.
|
||||||
|
var advertisedChannelBindingTypes: Set<String> {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
return advertisedTypes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supplies Luma's OpenSSL-based TLS processor to Martin's legacy
|
/// Supplies Luma's OpenSSL-based TLS processor to Martin's legacy
|
||||||
|
|||||||
@@ -280,6 +280,9 @@ final class XMPPService {
|
|||||||
/// Mechanisms advertised in the stream features (classic SASL + SASL2),
|
/// Mechanisms advertised in the stream features (classic SASL + SASL2),
|
||||||
/// shown on the server-information screen.
|
/// shown on the server-information screen.
|
||||||
private var advertisedSASLMechanisms: [String] = []
|
private var advertisedSASLMechanisms: [String] = []
|
||||||
|
/// SASL2 (RFC 9050) mechanisms from the `<authentication/>` stream
|
||||||
|
/// feature, shown as a dedicated list on the server-information screen.
|
||||||
|
private var advertisedSASL2Mechanisms: [String] = []
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
private var account: AccountConfiguration?
|
private var account: AccountConfiguration?
|
||||||
private var archiveSyncStarted = false
|
private var archiveSyncStarted = false
|
||||||
@@ -435,6 +438,7 @@ final class XMPPService {
|
|||||||
tlsProbeTask = nil
|
tlsProbeTask = nil
|
||||||
channelBindingStore.reset()
|
channelBindingStore.reset()
|
||||||
advertisedSASLMechanisms = []
|
advertisedSASLMechanisms = []
|
||||||
|
advertisedSASL2Mechanisms = []
|
||||||
|
|
||||||
let account = try account.validated()
|
let account = try account.validated()
|
||||||
self.account = account
|
self.account = account
|
||||||
@@ -1628,6 +1632,38 @@ final class XMPPService {
|
|||||||
.filter { $0.name == "mechanism" }
|
.filter { $0.name == "mechanism" }
|
||||||
.compactMap { $0.value } ?? []
|
.compactMap { $0.value } ?? []
|
||||||
}
|
}
|
||||||
|
// Same fallback pattern for SASL2 mechanisms, channel-binding types
|
||||||
|
// and XEP-0474: prefer the values captured from the pre-auth stream
|
||||||
|
// features, and only fall back to the live features element when the
|
||||||
|
// capture never ran (e.g. the session was resumed without a fresh
|
||||||
|
// login).
|
||||||
|
let sasl2ForDisplay: [String]
|
||||||
|
if !advertisedSASL2Mechanisms.isEmpty {
|
||||||
|
sasl2ForDisplay = advertisedSASL2Mechanisms
|
||||||
|
} else {
|
||||||
|
sasl2ForDisplay = streamFeaturesElement?
|
||||||
|
.findChild(name: "authentication", xmlns: "urn:xmpp:sasl:2")?
|
||||||
|
.children
|
||||||
|
.filter { $0.name == "mechanism" }
|
||||||
|
.compactMap { $0.value } ?? []
|
||||||
|
}
|
||||||
|
let channelBindingTypesForDisplay: [String]
|
||||||
|
if !channelBindingStore.advertisedChannelBindingTypes.isEmpty {
|
||||||
|
channelBindingTypesForDisplay = Array(
|
||||||
|
channelBindingStore.advertisedChannelBindingTypes
|
||||||
|
).sorted()
|
||||||
|
} else {
|
||||||
|
channelBindingTypesForDisplay = streamFeaturesElement?
|
||||||
|
.findChild(name: "sasl-channel-binding", xmlns: "urn:xmpp:sasl-cb:0")?
|
||||||
|
.children
|
||||||
|
.filter { $0.name == "channel-binding" }
|
||||||
|
.compactMap { $0.getAttribute("type") } ?? []
|
||||||
|
}
|
||||||
|
// XEP-0474 support is detected during the SCRAM exchange: the server
|
||||||
|
// includes its downgrade-protection hash in the `h` attribute of the
|
||||||
|
// server-first message.
|
||||||
|
let downgradeProtectionForDisplay = channelBindingStore.isDowngradeProtectionDetected
|
||||||
|
|
||||||
let supportsCSI = streamFeaturesElement?
|
let supportsCSI = streamFeaturesElement?
|
||||||
.findChild(name: "csi", xmlns: "urn:xmpp:csi:0") != nil
|
.findChild(name: "csi", xmlns: "urn:xmpp:csi:0") != nil
|
||||||
let supportsRosterVersioning = streamFeaturesElement?
|
let supportsRosterVersioning = streamFeaturesElement?
|
||||||
@@ -1685,6 +1721,14 @@ final class XMPPService {
|
|||||||
received: stats.received
|
received: stats.received
|
||||||
),
|
),
|
||||||
saslMethods: saslMechanisms,
|
saslMethods: saslMechanisms,
|
||||||
|
sasl2Methods: sasl2ForDisplay,
|
||||||
|
channelBindingTypes: channelBindingTypesForDisplay,
|
||||||
|
usedChannelBindingType:
|
||||||
|
(negotiatedSASLMechanism?.hasSuffix("-PLUS") == true)
|
||||||
|
? channelBindingStore.preferredChannelBindingType
|
||||||
|
: nil,
|
||||||
|
supportsSCRAMDowngradeProtection: downgradeProtectionForDisplay,
|
||||||
|
usedSASLMechanism: negotiatedSASLMechanism,
|
||||||
tlsVersion: tlsVersion,
|
tlsVersion: tlsVersion,
|
||||||
tlsCipher: negotiatedTLSCipher,
|
tlsCipher: negotiatedTLSCipher,
|
||||||
saslMechanism: displaySASLMechanism,
|
saslMechanism: displaySASLMechanism,
|
||||||
@@ -1782,6 +1826,22 @@ final class XMPPService {
|
|||||||
// Registered before SaslModule so the raw RFC 6120 failure condition
|
// Registered before SaslModule so the raw RFC 6120 failure condition
|
||||||
// is captured before Martin collapses it into SaslError.
|
// is captured before Martin collapses it into SaslError.
|
||||||
saslFailureModule = client.modulesManager.register(LumaSaslFailureModule())
|
saslFailureModule = client.modulesManager.register(LumaSaslFailureModule())
|
||||||
|
// Watches the raw SASL <challenge/> for the XEP-0474 `h=` attribute so
|
||||||
|
// the server screen can report downgrade protection regardless of the
|
||||||
|
// SCRAM mechanism implementation in use.
|
||||||
|
_ = client.modulesManager.register(
|
||||||
|
LumaSaslChallengeModule { [weak self] challenge in
|
||||||
|
guard let data = Data(base64Encoded: challenge),
|
||||||
|
let text = String(data: data, encoding: .utf8),
|
||||||
|
let parsed = try? SCRAMSHA512.parseServerFirst(
|
||||||
|
text,
|
||||||
|
expectedNoncePrefix: ""
|
||||||
|
),
|
||||||
|
parsed.downgradeProtectionHash != nil
|
||||||
|
else { return }
|
||||||
|
self?.channelBindingStore.markDowngradeProtectionDetected()
|
||||||
|
}
|
||||||
|
)
|
||||||
// Channel-binding SCRAM first (tls-exporter / tls-server-end-point
|
// Channel-binding SCRAM first (tls-exporter / tls-server-end-point
|
||||||
// over the live TLS 1.3 connection), then SCRAM-SHA-512: Martin only
|
// over the live TLS 1.3 connection), then SCRAM-SHA-512: Martin only
|
||||||
// ships SHA-1/SHA-256, while modern servers prefer SHA-512. A
|
// ships SHA-1/SHA-256, while modern servers prefer SHA-512. A
|
||||||
@@ -1800,7 +1860,10 @@ final class XMPPService {
|
|||||||
LumaScramPlusMechanism(hash: .sha512, channelBindingStore: channelBindingStore),
|
LumaScramPlusMechanism(hash: .sha512, channelBindingStore: channelBindingStore),
|
||||||
first: true
|
first: true
|
||||||
)
|
)
|
||||||
sasl.addMechanism(LumaScramSha512Mechanism(), first: true)
|
sasl.addMechanism(
|
||||||
|
LumaScramSha512Mechanism(channelBindingStore: channelBindingStore),
|
||||||
|
first: true
|
||||||
|
)
|
||||||
_ = client.modulesManager.register(ResourceBinderModule())
|
_ = client.modulesManager.register(ResourceBinderModule())
|
||||||
_ = client.modulesManager.register(SessionEstablishmentModule())
|
_ = client.modulesManager.register(SessionEstablishmentModule())
|
||||||
_ = client.modulesManager.register(
|
_ = client.modulesManager.register(
|
||||||
@@ -4202,15 +4265,29 @@ final class XMPPService {
|
|||||||
.children
|
.children
|
||||||
.filter { $0.name == "mechanism" }
|
.filter { $0.name == "mechanism" }
|
||||||
.compactMap { $0.value } ?? []
|
.compactMap { $0.value } ?? []
|
||||||
let channelBindingTypes = Set(
|
let channelBindingTypesOrdered =
|
||||||
features
|
features
|
||||||
.findChild(name: "sasl-channel-binding", xmlns: "urn:xmpp:sasl-cb:0")?
|
.findChild(name: "sasl-channel-binding", xmlns: "urn:xmpp:sasl-cb:0")?
|
||||||
.children
|
.children
|
||||||
.filter { $0.name == "channel-binding" }
|
.filter { $0.name == "channel-binding" }
|
||||||
.compactMap { $0.getAttribute("type") } ?? []
|
.compactMap { $0.getAttribute("type") } ?? []
|
||||||
)
|
let channelBindingTypes = Set(channelBindingTypesOrdered)
|
||||||
channelBindingStore.setAdvertisedChannelBindingTypes(channelBindingTypes)
|
// Post-auth stream features (SMACK resumption) carry no SASL elements;
|
||||||
advertisedSASLMechanisms = Array(Set(mechanisms + sasl2Mechanisms))
|
// refreshing the captured state from them would wipe the screen to
|
||||||
|
// "no methods / no channel binding". Only pre-auth features (those
|
||||||
|
// carrying any SASL-related element) update the state.
|
||||||
|
let hasSASLElements = !mechanisms.isEmpty
|
||||||
|
|| !sasl2Mechanisms.isEmpty
|
||||||
|
|| !channelBindingTypes.isEmpty
|
||||||
|
if hasSASLElements {
|
||||||
|
channelBindingStore.setAdvertisedChannelBindingTypes(channelBindingTypes)
|
||||||
|
channelBindingStore.setSASLContext(
|
||||||
|
mechanisms: mechanisms,
|
||||||
|
channelBindingTypes: channelBindingTypesOrdered
|
||||||
|
)
|
||||||
|
advertisedSASLMechanisms = Array(Set(mechanisms + sasl2Mechanisms))
|
||||||
|
advertisedSASL2Mechanisms = sasl2Mechanisms
|
||||||
|
}
|
||||||
// Record the mechanism the SASL layer picks for this connection: the
|
// Record the mechanism the SASL layer picks for this connection: the
|
||||||
// strongest password-based one the server advertises
|
// strongest password-based one the server advertises
|
||||||
// (SCRAM-*-PLUS > SCRAM-SHA-512 > … > PLAIN). Keep the previous value
|
// (SCRAM-*-PLUS > SCRAM-SHA-512 > … > PLAIN). Keep the previous value
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ final class SASLMechanismPreferenceTests: XCTestCase {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testWeakMechanismsAreFlagged() {
|
||||||
|
XCTAssertTrue(SASLMechanismPreference.weakMechanisms.contains("PLAIN"))
|
||||||
|
XCTAssertTrue(SASLMechanismPreference.weakMechanisms.contains("OAUTHBEARER"))
|
||||||
|
XCTAssertFalse(SASLMechanismPreference.weakMechanisms.contains("SCRAM-SHA-1"))
|
||||||
|
XCTAssertFalse(SASLMechanismPreference.weakMechanisms.contains("SCRAM-SHA-512-PLUS"))
|
||||||
|
XCTAssertFalse(SASLMechanismPreference.weakMechanisms.contains("EXTERNAL"))
|
||||||
|
}
|
||||||
|
|
||||||
func testPlusVariantsSkippedWithoutChannelBinding() {
|
func testPlusVariantsSkippedWithoutChannelBinding() {
|
||||||
let offered = ["SCRAM-SHA-512-PLUS", "SCRAM-SHA-512", "PLAIN"]
|
let offered = ["SCRAM-SHA-512-PLUS", "SCRAM-SHA-512", "PLAIN"]
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import Luma
|
||||||
|
|
||||||
|
final class SCRAMDowngradeProtectionTests: XCTestCase {
|
||||||
|
/// Reference vector from XEP-0474 §6.3: the server's `h` attribute is
|
||||||
|
/// base64(SHA-1('SCRAM-SHA-1\u{1E}SCRAM-SHA-1-PLUS\u{1F}tls-exporter\u{1E}tls-server-end-point')).
|
||||||
|
func testHashMatchesXEP0474Example() {
|
||||||
|
let hash = SCRAMDowngradeProtection.hash(
|
||||||
|
mechanisms: ["SCRAM-SHA-1", "SCRAM-SHA-1-PLUS"],
|
||||||
|
channelBindingTypes: ["tls-exporter", "tls-server-end-point"],
|
||||||
|
using: .sha1
|
||||||
|
)
|
||||||
|
XCTAssertEqual(hash.base64EncodedString(), "G6k/rBLDqgOhRRaCuuatSDFkJ08=")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHashSortsListsPerIOctetCollation() {
|
||||||
|
// XEP-0474 §5.1 requires i;octet-sorted lists, so advertisement order
|
||||||
|
// must not affect the hash.
|
||||||
|
let advertisedOrder = SCRAMDowngradeProtection.hash(
|
||||||
|
mechanisms: ["SCRAM-SHA-1", "SCRAM-SHA-1-PLUS"],
|
||||||
|
channelBindingTypes: ["tls-exporter", "tls-server-end-point"],
|
||||||
|
using: .sha1
|
||||||
|
)
|
||||||
|
let reordered = SCRAMDowngradeProtection.hash(
|
||||||
|
mechanisms: ["SCRAM-SHA-1-PLUS", "SCRAM-SHA-1"],
|
||||||
|
channelBindingTypes: ["tls-server-end-point", "tls-exporter"],
|
||||||
|
using: .sha1
|
||||||
|
)
|
||||||
|
XCTAssertEqual(advertisedOrder, reordered)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHashOmitsBindingSectionWhenNoTypesAdvertised() {
|
||||||
|
let withBindings = SCRAMDowngradeProtection.hash(
|
||||||
|
mechanisms: ["SCRAM-SHA-1"],
|
||||||
|
channelBindingTypes: ["tls-exporter"],
|
||||||
|
using: .sha1
|
||||||
|
)
|
||||||
|
let withoutBindings = SCRAMDowngradeProtection.hash(
|
||||||
|
mechanisms: ["SCRAM-SHA-1"],
|
||||||
|
channelBindingTypes: [],
|
||||||
|
using: .sha1
|
||||||
|
)
|
||||||
|
XCTAssertNotEqual(withBindings, withoutBindings)
|
||||||
|
// Without bindings the input is exactly the sorted mechanisms with no
|
||||||
|
// trailing 0x1F delimiter.
|
||||||
|
let expected = SCRAMHash.sha1.hash(data: Data("SCRAM-SHA-1".utf8))
|
||||||
|
XCTAssertEqual(withoutBindings, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testServerFirstParsesDowngradeProtectionHash() throws {
|
||||||
|
let message = "r=12C4CD5C-E38E-4A98-8F6D-15C38F51CCC6a09117a6-ac50-4f2f-93f1-93799c2bddf6,s=QSXCR+Q6sek8bf92,i=4096,h=G6k/rBLDqgOhRRaCuuatSDFkJ08="
|
||||||
|
let parsed = try SCRAMSHA512.parseServerFirst(
|
||||||
|
message,
|
||||||
|
expectedNoncePrefix: "12C4CD5C-E38E-4A98-8F6D-15C38F51CCC6"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(parsed.downgradeProtectionHash, "G6k/rBLDqgOhRRaCuuatSDFkJ08=")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testServerFirstWithoutHashParsesCleanly() throws {
|
||||||
|
let message = "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"
|
||||||
|
let parsed = try SCRAMSHA512.parseServerFirst(
|
||||||
|
message,
|
||||||
|
expectedNoncePrefix: "fyko+d2lbbFgONRv9qkxdawL"
|
||||||
|
)
|
||||||
|
XCTAssertNil(parsed.downgradeProtectionHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClientFinalIncludesXAttributeWhenHashPresent() throws {
|
||||||
|
let exchange = SCRAMPlusExchange(
|
||||||
|
hash: .sha1,
|
||||||
|
username: "user",
|
||||||
|
password: "pencil",
|
||||||
|
channelBindingType: "tls-server-end-point",
|
||||||
|
channelBindingData: Data(0..<32),
|
||||||
|
clientNonce: "fyko+d2lbbFgONRv9qkxdawL"
|
||||||
|
)
|
||||||
|
let serverFirst = "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096,h=G6k/rBLDqgOhRRaCuuatSDFkJ08="
|
||||||
|
let final = try exchange.clientFinalMessage(
|
||||||
|
serverFirst: serverFirst,
|
||||||
|
downgradeProtectionHashBase64: "G6k/rBLDqgOhRRaCuuatSDFkJ08="
|
||||||
|
)
|
||||||
|
XCTAssertTrue(final.contains(",x=G6k/rBLDqgOhRRaCuuatSDFkJ08=,"))
|
||||||
|
// The same x-attribute must be part of the expected signature's
|
||||||
|
// auth message (server reconstructs client-final-without-proof).
|
||||||
|
let expected = try exchange.expectedServerSignature(
|
||||||
|
serverFirst: serverFirst,
|
||||||
|
downgradeProtectionHashBase64: "G6k/rBLDqgOhRRaCuuatSDFkJ08="
|
||||||
|
)
|
||||||
|
XCTAssertFalse(expected.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user