Browse Source

SCRAM-SHA-1/256 support

Closes #9
tags/v0.8.0
Chris Smith 5 years ago
parent
commit
9a00e71c37

+ 1
- 0
CHANGELOG View File

@@ -1,5 +1,6 @@
1 1
 vNEXT (in development)
2 2
 
3
+ * Added support for SCRAM-SHA-1 and SCRAM-SHA-256 SASL mechanisms
3 4
 
4 5
 v0.7.0
5 6
 

+ 2
- 2
src/main/kotlin/com/dmdirc/ktirc/Dsl.kt View File

@@ -113,7 +113,7 @@ class ProfileConfig {
113 113
 /**
114 114
  * Dsl for configuring SASL authentication.
115 115
  *
116
- * By default the `PLAIN` method will be enabled if SASL is configured.
116
+ * By default the `PLAIN`, `SCRAM-SHA-1`, and `SCRAM-SHA-256` methods will be enabled if SASL is configured.
117 117
  *
118 118
  * You can modify the mechanisms either by editing the [mechanisms] collection:
119 119
  *
@@ -135,7 +135,7 @@ class ProfileConfig {
135 135
 @IrcClientDsl
136 136
 class SaslConfig {
137 137
     /** The SASL mechanisms to enable. */
138
-    val mechanisms: MutableCollection<String> = mutableSetOf("PLAIN")
138
+    val mechanisms: MutableCollection<String> = mutableSetOf("PLAIN", "SCRAM-SHA-1", "SCRAM-SHA-256")
139 139
     /** The username to provide when authenticating using SASL. */
140 140
     var username: String = ""
141 141
     /** The username to provide when authenticating using SASL. */

+ 1
- 2
src/main/kotlin/com/dmdirc/ktirc/sasl/PlainMechanism.kt View File

@@ -2,7 +2,6 @@ package com.dmdirc.ktirc.sasl
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4 4
 import com.dmdirc.ktirc.SaslConfig
5
-import com.dmdirc.ktirc.messages.sendAuthenticationMessage
6 5
 
7 6
 internal class PlainMechanism(private val saslConfig: SaslConfig) : SaslMechanism {
8 7
 
@@ -11,7 +10,7 @@ internal class PlainMechanism(private val saslConfig: SaslConfig) : SaslMechanis
11 10
 
12 11
     override fun handleAuthenticationEvent(client: IrcClient, data: ByteArray?) {
13 12
         with (saslConfig) {
14
-            client.sendAuthenticationMessage("$username\u0000$username\u0000$password".toByteArray().toBase64())
13
+            client.sendAuthenticationData("$username\u0000$username\u0000$password")
15 14
         }
16 15
     }
17 16
 

+ 11
- 0
src/main/kotlin/com/dmdirc/ktirc/sasl/SaslMechanism.kt View File

@@ -2,6 +2,7 @@ package com.dmdirc.ktirc.sasl
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4 4
 import com.dmdirc.ktirc.SaslConfig
5
+import com.dmdirc.ktirc.messages.sendAuthenticationMessage
5 6
 
6 7
 internal interface SaslMechanism {
7 8
 
@@ -14,8 +15,18 @@ internal interface SaslMechanism {
14 15
 
15 16
 internal fun SaslConfig.createSaslMechanism(): List<SaslMechanism> = mechanisms.mapNotNull {
16 17
     when (it.toUpperCase()) {
18
+        "SCRAM-SHA-1" -> ScramMechanism("SHA-1", 10, this)
19
+        "SCRAM-SHA-256" -> ScramMechanism("SHA-256", 20, this)
17 20
         "EXTERNAL" -> ExternalMechanism()
18 21
         "PLAIN" -> PlainMechanism(this)
19 22
         else -> null
20 23
     }
21 24
 }
25
+
26
+internal fun IrcClient.sendAuthenticationData(data: String) {
27
+    val lines = data.toByteArray().toBase64().chunked(400)
28
+    lines.forEach(this::sendAuthenticationMessage)
29
+    if (lines.last().length == 400) {
30
+        sendAuthenticationMessage("+")
31
+    }
32
+}

+ 183
- 0
src/main/kotlin/com/dmdirc/ktirc/sasl/ScramMechanism.kt View File

@@ -0,0 +1,183 @@
1
+package com.dmdirc.ktirc.sasl
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.SaslConfig
5
+import com.dmdirc.ktirc.messages.sendAuthenticationMessage
6
+import com.dmdirc.ktirc.util.logger
7
+import java.security.MessageDigest
8
+import java.security.SecureRandom
9
+import javax.crypto.Mac
10
+import javax.crypto.spec.SecretKeySpec
11
+import kotlin.experimental.xor
12
+import kotlin.random.asKotlinRandom
13
+
14
+internal class ScramMechanism(private val algorithm: String, override val priority: Int, private val saslConfig: SaslConfig) : SaslMechanism {
15
+
16
+    private val log by logger()
17
+
18
+    override val ircName = "SCRAM-${algorithm.toUpperCase()}"
19
+
20
+    override fun handleAuthenticationEvent(client: IrcClient, data: ByteArray?) {
21
+        val state = client.scramState
22
+        try {
23
+            when (state.scramStage) {
24
+                ScramStage.SendingFirstMessage -> client.sendFirstMessage(state)
25
+                ScramStage.SendingSecondMessage -> client.sendSecondMessage(state, data.parse())
26
+                ScramStage.Finishing -> client.validateAndFinish(state, data.parse())
27
+            }
28
+        } catch (ex: ScramException) {
29
+            client.abortScram(ex.localizedMessage)
30
+        }
31
+    }
32
+
33
+    private fun IrcClient.sendFirstMessage(state: ScramState) {
34
+        state.scramStage = ScramStage.SendingSecondMessage
35
+        sendScramMessage(
36
+                "n,,", // No channel binding, no impersonation
37
+                ScramMessageType.AuthName to saslConfig.username.escape(),
38
+                ScramMessageType.Nonce to state.clientNonce)
39
+    }
40
+
41
+    private fun IrcClient.sendSecondMessage(state: ScramState, data: Map<ScramMessageType, String>) {
42
+        if (ScramMessageType.FutureExtensions in data)
43
+            throw ScramException("Unsupported extension received: ${data[ScramMessageType.FutureExtensions]}")
44
+        if (ScramMessageType.Error in data)
45
+            throw ScramException("Error received from server: ${data[ScramMessageType.Error]}")
46
+
47
+        state.iterCount = data[ScramMessageType.IterationCount]?.toIntOrNull()
48
+                ?: throw ScramException("No iteration count provided")
49
+        state.salt = data[ScramMessageType.Salt]?.fromBase64() ?: throw ScramException("No salt provided")
50
+        state.serverNonce = data[ScramMessageType.Nonce] ?: throw ScramException("No server salt provided")
51
+
52
+        state.saltedPassword = pbkdf2(saslConfig.password.toByteArray(), state.salt, state.iterCount)
53
+        val clientKey = hmac(state.saltedPassword, "Client Key".toByteArray())
54
+        val storedKey = hash(clientKey)
55
+        state.authMessage = buildScramMessage(
56
+                ScramMessageType.AuthName to saslConfig.username.escape(),
57
+                ScramMessageType.Nonce to state.clientNonce,
58
+                ScramMessageType.Nonce to state.serverNonce,
59
+                ScramMessageType.Salt to state.salt.toBase64(),
60
+                ScramMessageType.IterationCount to state.iterCount.toString(),
61
+                ScramMessageType.ChannelBinding to "n,,".toByteArray().toBase64(),
62
+                ScramMessageType.Nonce to state.serverNonce).toByteArray()
63
+        val clientSignature = hmac(storedKey, state.authMessage)
64
+        val clientProof = clientKey.xor(clientSignature)
65
+
66
+        state.scramStage = ScramStage.Finishing
67
+        sendScramMessage(
68
+                "",
69
+                ScramMessageType.ChannelBinding to "n,,".toByteArray().toBase64(),
70
+                ScramMessageType.Nonce to state.serverNonce,
71
+                ScramMessageType.ClientProof to clientProof.toBase64()
72
+        )
73
+    }
74
+
75
+    private fun IrcClient.validateAndFinish(state: ScramState, data: Map<ScramMessageType, String>) {
76
+        if (ScramMessageType.FutureExtensions in data)
77
+            throw ScramException("Unsupported extension received: ${data[ScramMessageType.FutureExtensions]}")
78
+        if (ScramMessageType.Error in data)
79
+            throw ScramException("Error received from server: ${data[ScramMessageType.Error]}")
80
+
81
+        val serverKey = hmac(state.saltedPassword, "Server Key".toByteArray())
82
+        val expectedServerSignature = hmac(serverKey, state.authMessage).toBase64()
83
+        val receivedServerSignature = data[ScramMessageType.ServerVerifier]
84
+                ?: throw ScramException("No server verifier received")
85
+
86
+        if (expectedServerSignature != receivedServerSignature) {
87
+            throw ScramException("Server signature does not match")
88
+        }
89
+        sendAuthenticationMessage("+")
90
+    }
91
+
92
+    private fun IrcClient.abortScram(reason: String) {
93
+        log.warning { "Aborting SCRAM authentication: $reason" }
94
+        sendAuthenticationMessage("*")
95
+    }
96
+
97
+    private fun IrcClient.sendScramMessage(prefix: String = "", vararg entries: Pair<ScramMessageType, String>) =
98
+            sendAuthenticationData("$prefix${buildScramMessage(*entries)}")
99
+
100
+    private fun buildScramMessage(vararg entries: Pair<ScramMessageType, String>) = entries.joinToString(",") { (k, v) -> "${k.prefix}=$v" }
101
+
102
+    private fun ByteArray?.parse(): Map<ScramMessageType, String> {
103
+        return if (this == null || this.isEmpty())
104
+            emptyMap()
105
+        else
106
+            String(this).split(',').map {
107
+                getMessageType(it[0]) to it.substring(2).unescape()
108
+            }.toMap()
109
+    }
110
+
111
+    private fun String.escape() = replace("=", "=3D").replace(",", "=2C")
112
+    private fun String.unescape() = replace("=2C", ",").replace("=3D", "=")
113
+
114
+    private fun hmac(keyMaterial: ByteArray, input: ByteArray): ByteArray {
115
+        return with(Mac.getInstance("hmac${algorithm.replace("-", "")}")) {
116
+            init(SecretKeySpec(keyMaterial, algorithm))
117
+            doFinal(input)
118
+        }
119
+    }
120
+
121
+    private fun hash(input: ByteArray): ByteArray {
122
+        return with(MessageDigest.getInstance(algorithm.replace("-", ""))) {
123
+            digest(input)
124
+        }
125
+    }
126
+
127
+    private fun pbkdf2(keyMaterial: ByteArray, initialSalt: ByteArray, iterations: Int): ByteArray {
128
+        var salt = initialSalt + 0x00 + 0x00 + 0x00 + 0x01
129
+        var result: ByteArray? = null
130
+        for (i in 1..iterations) {
131
+            salt = hmac(keyMaterial, salt)
132
+            result = result?.xor(salt) ?: salt
133
+        }
134
+        return result ?: ByteArray(0)
135
+    }
136
+
137
+    private val IrcClient.scramState: ScramState
138
+        get() = with(serverState.sasl) {
139
+            mechanismState = mechanismState as? ScramState ?: com.dmdirc.ktirc.sasl.ScramState()
140
+            mechanismState as ScramState
141
+        }
142
+
143
+    private fun ByteArray.xor(other: ByteArray): ByteArray = zip(other) { a, b -> a.xor(b) }.toByteArray()
144
+
145
+}
146
+
147
+private class ScramException(message: String) : RuntimeException(message)
148
+
149
+private fun newNonce(): String {
150
+    val charPool: List<Char> = (' '..'~') - ',' - '='
151
+    val random = SecureRandom.getInstanceStrong().asKotlinRandom()
152
+    return (0..31).map { charPool.random(random) }.joinToString("")
153
+}
154
+
155
+internal class ScramState(
156
+        var scramStage: ScramStage = ScramStage.SendingFirstMessage,
157
+        val clientNonce: String = newNonce(),
158
+        var serverNonce: String = "",
159
+        var iterCount: Int = 1,
160
+        var salt: ByteArray = ByteArray(0),
161
+        var saltedPassword: ByteArray = ByteArray(0),
162
+        var authMessage: ByteArray = ByteArray(0))
163
+
164
+internal enum class ScramStage {
165
+    SendingFirstMessage,
166
+    SendingSecondMessage,
167
+    Finishing
168
+}
169
+
170
+internal enum class ScramMessageType(val prefix: Char) {
171
+    AuthName('n'),
172
+    FutureExtensions('m'),
173
+    Nonce('r'),
174
+    ChannelBinding('c'),
175
+    Salt('s'),
176
+    IterationCount('i'),
177
+    ClientProof('p'),
178
+    ServerVerifier('v'),
179
+    Error('e'),
180
+}
181
+
182
+private fun getMessageType(prefix: Char) =
183
+        ScramMessageType.values().firstOrNull { it.prefix == prefix } ?: ScramMessageType.Error

+ 2
- 2
src/test/kotlin/com/dmdirc/ktirc/DslTest.kt View File

@@ -137,9 +137,9 @@ internal class SaslConfigTest {
137 137
     }
138 138
 
139 139
     @Test
140
-    fun `defaults to plain mechanism`() {
140
+    fun `defaults to plain and scram mechanisms`() {
141 141
         val config = SaslConfig()
142
-        assertEquals(setOf("PLAIN"), config.mechanisms)
142
+        assertEquals(setOf("PLAIN", "SCRAM-SHA-1", "SCRAM-SHA-256"), config.mechanisms)
143 143
     }
144 144
 
145 145
 }

+ 33
- 1
src/test/kotlin/com/dmdirc/ktirc/sasl/SaslMechanismTest.kt View File

@@ -1,6 +1,11 @@
1 1
 package com.dmdirc.ktirc.sasl
2 2
 
3
+import com.dmdirc.ktirc.IrcClient
3 4
 import com.dmdirc.ktirc.SaslConfig
5
+import com.nhaarman.mockitokotlin2.inOrder
6
+import com.nhaarman.mockitokotlin2.mock
7
+import com.nhaarman.mockitokotlin2.times
8
+import com.nhaarman.mockitokotlin2.verify
4 9
 import org.junit.jupiter.api.Assertions.assertEquals
5 10
 import org.junit.jupiter.api.Assertions.assertTrue
6 11
 import org.junit.jupiter.api.Test
@@ -22,4 +27,31 @@ internal class SaslMechanismTest {
22 27
         assertTrue(mechanisms[0] is PlainMechanism)
23 28
     }
24 29
 
25
-}
30
+    @Test
31
+    fun `base64 encodes authentication data`() {
32
+        val client = mock<IrcClient>()
33
+        client.sendAuthenticationData("abcdef")
34
+        verify(client).send("AUTHENTICATE YWJjZGVm")
35
+    }
36
+
37
+    @Test
38
+    fun `chunks authentication data into 400 byte lines`() {
39
+        val client = mock<IrcClient>()
40
+        client.sendAuthenticationData("abcdef".repeat(120))
41
+        with (inOrder(client)) {
42
+            verify(client, times(2)).send("AUTHENTICATE ${"YWJjZGVm".repeat(50)}")
43
+            verify(client).send("AUTHENTICATE ${"YWJjZGVm".repeat(20)}")
44
+        }
45
+    }
46
+
47
+    @Test
48
+    fun `sends blank line if data is exactly 400 bytes`() {
49
+        val client = mock<IrcClient>()
50
+        client.sendAuthenticationData("abcdef".repeat(50))
51
+        with (inOrder(client)) {
52
+            verify(client).send("AUTHENTICATE ${"YWJjZGVm".repeat(50)}")
53
+            verify(client).send("AUTHENTICATE +")
54
+        }
55
+    }
56
+
57
+}

+ 230
- 0
src/test/kotlin/com/dmdirc/ktirc/sasl/ScramMechanismTest.kt View File

@@ -0,0 +1,230 @@
1
+package com.dmdirc.ktirc.sasl
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.SaslConfig
5
+import com.dmdirc.ktirc.model.ServerState
6
+import com.nhaarman.mockitokotlin2.doReturn
7
+import com.nhaarman.mockitokotlin2.mock
8
+import com.nhaarman.mockitokotlin2.verify
9
+import org.junit.jupiter.api.Assertions.assertEquals
10
+import org.junit.jupiter.api.Test
11
+
12
+internal class ScramMechanismTest {
13
+
14
+    private val serverState = ServerState("", "")
15
+    private val ircClient = mock<IrcClient> {
16
+        on { serverState } doReturn serverState
17
+    }
18
+
19
+    @Test
20
+    fun `sends first message when no state is present`() {
21
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
22
+            username = "user"
23
+            password = "pencil"
24
+        })
25
+
26
+        mechanism.handleAuthenticationEvent(ircClient, "+".toByteArray())
27
+
28
+        val nonce = (serverState.sasl.mechanismState as ScramState).clientNonce
29
+        verify(ircClient).send("AUTHENTICATE ${"n,,n=user,r=$nonce".toByteArray().toBase64()}")
30
+    }
31
+
32
+    @Test
33
+    fun `aborts if the server's first message contains extensions`() {
34
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
35
+            username = "user"
36
+            password = "pencil"
37
+        })
38
+
39
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.SendingSecondMessage)
40
+
41
+        mechanism.handleAuthenticationEvent(ircClient, "m=future".toByteArray())
42
+
43
+        verify(ircClient).send("AUTHENTICATE *")
44
+    }
45
+
46
+    @Test
47
+    fun `aborts if the server's first message contains an error`() {
48
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
49
+            username = "user"
50
+            password = "pencil"
51
+        })
52
+
53
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.SendingSecondMessage)
54
+
55
+        mechanism.handleAuthenticationEvent(ircClient, "e=whoops".toByteArray())
56
+
57
+        verify(ircClient).send("AUTHENTICATE *")
58
+    }
59
+
60
+    @Test
61
+    fun `aborts if the server's first message lacks an iteration count`() {
62
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
63
+            username = "user"
64
+            password = "pencil"
65
+        })
66
+
67
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.SendingSecondMessage)
68
+
69
+        mechanism.handleAuthenticationEvent(ircClient, "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92".toByteArray())
70
+
71
+        verify(ircClient).send("AUTHENTICATE *")
72
+    }
73
+
74
+    @Test
75
+    fun `aborts if the server's first message has an invalid iteration count`() {
76
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
77
+            username = "user"
78
+            password = "pencil"
79
+        })
80
+
81
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.SendingSecondMessage)
82
+
83
+        mechanism.handleAuthenticationEvent(ircClient, "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=leet".toByteArray())
84
+
85
+        verify(ircClient).send("AUTHENTICATE *")
86
+    }
87
+
88
+    @Test
89
+    fun `aborts if the server's first message lacks a nonce`() {
90
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
91
+            username = "user"
92
+            password = "pencil"
93
+        })
94
+
95
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.SendingSecondMessage)
96
+
97
+        mechanism.handleAuthenticationEvent(ircClient, "rs=QSXCR+Q6sek8bf92,i=4096".toByteArray())
98
+
99
+        verify(ircClient).send("AUTHENTICATE *")
100
+    }
101
+
102
+    @Test
103
+    fun `aborts if the server's first message lacks a salt`() {
104
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
105
+            username = "user"
106
+            password = "pencil"
107
+        })
108
+
109
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.SendingSecondMessage)
110
+
111
+        mechanism.handleAuthenticationEvent(ircClient, "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,i=4096".toByteArray())
112
+
113
+        verify(ircClient).send("AUTHENTICATE *")
114
+    }
115
+
116
+    @Test
117
+    fun `updates state after receiving server's first message`() {
118
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
119
+            username = "user"
120
+            password = "pencil"
121
+        })
122
+
123
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.SendingSecondMessage, clientNonce = "fyko+d2lbbFgONRv9qkxdawL")
124
+
125
+        mechanism.handleAuthenticationEvent(ircClient, "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096".toByteArray())
126
+
127
+        (serverState.sasl.mechanismState as ScramState).let {
128
+            assertEquals(ScramStage.Finishing, it.scramStage)
129
+            assertEquals("fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j", it.serverNonce)
130
+            assertEquals(4096, it.iterCount)
131
+            assertEquals("QSXCR+Q6sek8bf92", it.salt.toBase64())
132
+            assertEquals("HZbuOlKbWl+eR8AfIposuKbhX30=", it.saltedPassword.toBase64())
133
+            assertEquals("n=user,r=fyko+d2lbbFgONRv9qkxdawL,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096,c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j", String(it.authMessage))
134
+        }
135
+    }
136
+
137
+    @Test
138
+    fun `responds to server's first message`() {
139
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
140
+            username = "user"
141
+            password = "pencil"
142
+        })
143
+
144
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.SendingSecondMessage, clientNonce = "fyko+d2lbbFgONRv9qkxdawL")
145
+
146
+        mechanism.handleAuthenticationEvent(ircClient, "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096".toByteArray())
147
+
148
+        verify(ircClient).send("AUTHENTICATE ${"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=".toByteArray().toBase64()}")
149
+    }
150
+
151
+    @Test
152
+    fun `aborts if the server's final message contains extensions`() {
153
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
154
+            username = "user"
155
+            password = "pencil"
156
+        })
157
+
158
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.Finishing)
159
+
160
+        mechanism.handleAuthenticationEvent(ircClient, "m=future".toByteArray())
161
+
162
+        verify(ircClient).send("AUTHENTICATE *")
163
+    }
164
+
165
+    @Test
166
+    fun `aborts if the server's final message contains an error`() {
167
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
168
+            username = "user"
169
+            password = "pencil"
170
+        })
171
+
172
+        serverState.sasl.mechanismState = ScramState(scramStage = ScramStage.Finishing)
173
+
174
+        mechanism.handleAuthenticationEvent(ircClient, "e=whoops".toByteArray())
175
+
176
+        verify(ircClient).send("AUTHENTICATE *")
177
+    }
178
+
179
+    @Test
180
+    fun `aborts if the server's final message doesn't contain a verifier`() {
181
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
182
+            username = "user"
183
+            password = "pencil"
184
+        })
185
+
186
+        serverState.sasl.mechanismState = ScramState(
187
+                scramStage = ScramStage.Finishing,
188
+                saltedPassword = "HZbuOlKbWl+eR8AfIposuKbhX30=".fromBase64(),
189
+                authMessage = "n=user,r=fyko+d2lbbFgONRv9qkxdawL,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096,c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j".toByteArray())
190
+
191
+        mechanism.handleAuthenticationEvent(ircClient, "".toByteArray())
192
+
193
+        verify(ircClient).send("AUTHENTICATE *")
194
+    }
195
+
196
+    @Test
197
+    fun `aborts if the server's verifier doesn't match`() {
198
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
199
+            username = "user"
200
+            password = "pencil"
201
+        })
202
+
203
+        serverState.sasl.mechanismState = ScramState(
204
+                scramStage = ScramStage.Finishing,
205
+                saltedPassword = "HZbuOlKbWl+eR8AfIposuKbhX30=".fromBase64(),
206
+                authMessage = "n=user,r=fyko+d2lbbFgONRv9qkxdawL,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096,c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j".toByteArray())
207
+
208
+        mechanism.handleAuthenticationEvent(ircClient, "v=rmF9pqV8S7suAoZWja4dJRkF=".toByteArray())
209
+
210
+        verify(ircClient).send("AUTHENTICATE *")
211
+    }
212
+
213
+    @Test
214
+    fun `sends final message if server's verifier matches`() {
215
+        val mechanism = ScramMechanism("SHA-1", 1, SaslConfig().apply {
216
+            username = "user"
217
+            password = "pencil"
218
+        })
219
+
220
+        serverState.sasl.mechanismState = ScramState(
221
+                scramStage = ScramStage.Finishing,
222
+                saltedPassword = "HZbuOlKbWl+eR8AfIposuKbhX30=".fromBase64(),
223
+                authMessage = "n=user,r=fyko+d2lbbFgONRv9qkxdawL,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096,c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j".toByteArray())
224
+
225
+        mechanism.handleAuthenticationEvent(ircClient, "v=rmF9pqV8S7suAoZWja4dJRkFsKQ=".toByteArray())
226
+
227
+        verify(ircClient).send("AUTHENTICATE +")
228
+    }
229
+
230
+}

Loading…
Cancel
Save