Coverage Summary for Class: Pairing (it.unibo.tuprolog.argumentation.parallel.federation)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| Pairing |
100%
(10/10)
|
85.7%
(24/28)
|
100%
(34/34)
|
94.7%
(269/284)
|
| Pairing$Admission |
|
| Pairing$Attempts |
100%
(1/1)
|
|
100%
(4/4)
|
100%
(17/17)
|
| Pairing$Companion |
100%
(8/8)
|
62.5%
(10/16)
|
100%
(15/15)
|
94.8%
(182/192)
|
| Pairing$Proven |
100%
(1/1)
|
|
100%
(2/2)
|
100%
(12/12)
|
| Pairing$Refusal |
100%
(1/1)
|
|
100%
(2/2)
|
100%
(7/7)
|
| Total |
100%
(21/21)
|
77.3%
(34/44)
|
100%
(57/57)
|
95.1%
(487/512)
|
package it.unibo.tuprolog.argumentation.parallel.federation
import kotlin.concurrent.Volatile
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeMark
import kotlin.time.TimeSource
/**
* The code of a site, which the sites linking to it prove they know when it requires one, and the pairings made with
* it: a site that proved the code is given a token of its own, which it proves it knows instead from then on (its
* identifier is known to anyone hearing its beacons, the token to the two sites alone), and the code is renewed. A
* secret is never sent but the token, once, to the site just paired: a site proves it knows one with its [proof] on a
* nonce of the other, as a site claiming the identifier of another (a beacon of its own) learns nothing of it.
*
* The wrong codes of an address are counted: after a few, its codes are refused for a while, longer and longer, as
* four digits are soon guessed otherwise; a link with no address is never admitted by its code, as its wrong codes
* could not be counted. The wrong codes of an address are forgotten a while after the last one.
*/
internal class Pairing {
private val lock = Any()
// The wrong codes of an address, until when its codes are refused, and when the last one came
private class Attempts(
val wrong: Int,
val refusedUntil: TimeMark?,
val last: TimeMark,
)
private val attempts = mutableMapOf<String, Attempts>()
/** The code of the site: four digits, random */
@Volatile var code: String = newCode()
private set
// The token of the pairing with each site paired with this one
@Volatile private var tokens: Map<String, String> = emptyMap()
/** The sites paired with this one */
val paired: Set<String> get() = tokens.keys
/** A new code, the one before refused from then on */
fun renew(): String = newCode().also { code = it }
/** The token of the pairing with [site], if they are paired */
fun tokenOf(site: String): String? = tokens[site]
/** [site] paired with this one, by [token] */
fun remember(
site: String,
token: String,
) = locked(lock) { tokens = tokens + (site to token) }
/** What a site linking to this one is told: admitted, or refused */
sealed interface Admission
/** What a site proved: the [secret] it knows (the token of its pairing, or the code), and the [token] given it if it proved the code */
class Proven(
val secret: String,
val token: String? = null,
) : Admission
/** A site refused, and why */
class Refusal(
val reason: String,
) : Admission
/**
* What [site], linking from [address], proved with [proof] on [nonce] (see [proof]), checked as a site requiring a
* code checks it: the token of its pairing, or the code, pairing the two then (the code renewed); why it is refused
* otherwise
*/
fun admission(
site: String,
address: String?,
nonce: String,
proof: String?,
): Admission =
locked(lock) {
val now = TimeSource.Monotonic.markNow()
attempts.entries.removeAll { (it.value.last + FORGOTTEN).hasPassedNow() }
val before = address?.let { attempts[it] }
val paired = tokens[site]
when {
paired != null && proves(proof, paired, site, nonce) -> Proven(paired)
address == null -> Refusal(NO_ADDRESS)
before?.refusedUntil?.hasNotPassedNow() == true -> Refusal(TOO_MANY)
proves(proof, code, site, nonce) -> {
attempts -= address
val token = newToken()
tokens = tokens + (site to token)
Proven(code, token).also { code = newCode() }
}
else -> {
val wrong = (before?.wrong ?: 0) + 1
attempts[address] = Attempts(wrong, if (wrong < FREE) null else now + lockout(wrong), now)
Refusal(WRONG)
}
}
}
/**
* What [site] proved with [proof] on [nonce], checked as a site requiring no code checks it (nothing counted, no
* pairing made): the token of its pairing, or the code; null if neither
*/
fun proven(
site: String,
nonce: String,
proof: String,
): Proven? {
val token = tokens[site]
val code = code
return when {
token != null && proves(proof, token, site, nonce) -> Proven(token)
proves(proof, code, site, nonce) -> Proven(code)
else -> null
}
}
companion object {
// The wrong codes of an address before its codes are refused for a while, the longest while, and how long
// after the last one they are forgotten
private const val FREE = 3
private val LONGEST = 900.seconds
private val FORGOTTEN = 1.hours
private const val WRONG = "Wrong code"
private const val TOO_MANY = "Too many wrong codes: try again later"
private const val NO_ADDRESS = "No code is checked on a link of no address"
/** The proof that [site] knows [secret] (a code, or the token of a pairing), on [nonce]: none of them told */
fun proof(
secret: String,
site: String,
nonce: String,
): String = hmac(secret, "$site|$nonce")
/** Whether [proof] is the proof that [site] knows [secret] on [nonce] */
fun proves(
proof: String?,
secret: String,
site: String,
nonce: String,
): Boolean = proof != null && same(proof, proof(secret, site, nonce))
/** A nonce, for the other end of a link to prove its secret on: random */
fun newNonce(): String = randomHex(16)
// A second after the first wrong codes allowed, twice as long after each one more
private fun lockout(wrong: Int): Duration = minOf(LONGEST, 1.seconds * (1 shl minOf(wrong - FREE, 10)))
// Whether [a] and [b] are the same text, in a time that tells nothing of where they differ
private fun same(
a: String,
b: String,
): Boolean {
var difference = a.length xor b.length
for (i in 0 until maxOf(a.length, b.length)) {
difference = difference or (a.getOrElse(i) { ' ' }.code xor b.getOrElse(i) { ' ' }.code)
}
return difference == 0
}
private fun newCode(): String =
randomBytes(4)
.fold(0L) { acc, byte -> acc * 256 + (byte.toInt() and 0xff) }
.mod(10_000L)
.toString()
.padStart(4, '0')
private fun newToken(): String = randomHex(16)
// [count] random bytes, in hexadecimal
private fun randomHex(count: Int): String =
randomBytes(count).joinToString("") { (it.toInt() and 0xff).toString(16).padStart(2, '0') }
}
}
/** [count] random bytes, from the random source of the platform fit for secrets */
internal expect fun randomBytes(count: Int): ByteArray