This screen showed chips and no explanation; iOS has carried one all along, under a comment claiming both apps use the same words. They now do. The last sentence is the boundary: a level decides who can open a file, not where it can be put. Neither app stops somebody filing sensitive material somewhere low, and saying so is the honest half of android#15. Also repairs a comment the emoji sweep truncated, which left the test saying 'the -property is NOT enforced'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
170 lines
5.7 KiB
Kotlin
170 lines
5.7 KiB
Kotlin
package io.flamenet.e2e
|
||
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.assertFailsWith
|
||
import kotlin.test.assertFalse
|
||
import kotlin.test.assertTrue
|
||
|
||
/**
|
||
* The security lattice, and the two Bell–LaPadula rules it exists to enforce.
|
||
*/
|
||
class LabelTest {
|
||
private fun hex(bytes: ByteArray) = bytes.joinToString("") { "%02x".format(it) }
|
||
|
||
// ---- The encoding ----------------------------------------------------.
|
||
|
||
/** `int16be classification || uint64be compartment mask`. */
|
||
@Test
|
||
fun `the ten-byte encoding matches the specification`() {
|
||
assertEquals("00000000000000000000", hex(Label(0).bytes()))
|
||
assertEquals("00010000000000000000", hex(Label(1).bytes()))
|
||
assertEquals("00038000000000000001", hex(Label(3, setOf(0, 63)).bytes()))
|
||
assertEquals("01000000000000000100", hex(Label(256, setOf(8)).bytes()))
|
||
}
|
||
|
||
/**
|
||
* A negative classification and a full compartment mask together, the case
|
||
* where a sign-extension mistake or an `Int` in place of a `Long` shows up.
|
||
*/
|
||
@Test
|
||
fun `the extremes encode without sign-extension damage`() {
|
||
assertEquals("ffffffffffffffffffff", hex(Label(-1, (0..63).toSet()).bytes()))
|
||
}
|
||
|
||
/**
|
||
* Bit 63 is the sign bit of a Long.
|
||
*/
|
||
@Test
|
||
fun `the top compartment bit survives`() {
|
||
assertEquals("00008000000000000000", hex(Label(0, setOf(63)).bytes()))
|
||
assertTrue(Label(0, setOf(63)).mask != 0L)
|
||
}
|
||
|
||
@Test
|
||
fun `the encoding is always ten bytes`() {
|
||
assertEquals(10, Label(0).bytes().size)
|
||
assertEquals(10, Label(-1, (0..63).toSet()).bytes().size)
|
||
}
|
||
|
||
@Test
|
||
fun `a compartment outside the range is refused`() {
|
||
assertFailsWith<IllegalArgumentException> { Label(0, setOf(64)) }
|
||
assertFailsWith<IllegalArgumentException> { Label(0, setOf(-1)) }
|
||
}
|
||
|
||
// ---- Domination ------------------------------------------------------.
|
||
|
||
@Test
|
||
fun `a higher classification dominates a lower one`() {
|
||
assertTrue(Label(3).dominates(Label(1)))
|
||
assertFalse(Label(1).dominates(Label(3)))
|
||
assertTrue(Label(2).dominates(Label(2)))
|
||
}
|
||
|
||
/**
|
||
* The failure that reads as working.
|
||
*/
|
||
@Test
|
||
fun `a higher level does NOT dominate a compartment it is not in`() {
|
||
val topSecretNoAreas = Label(9)
|
||
val secretInArea = Label(5, setOf(2))
|
||
assertFalse(
|
||
topSecretNoAreas.dominates(secretInArea),
|
||
"a higher classification reached into a compartment it does not hold",
|
||
)
|
||
}
|
||
|
||
@Test
|
||
fun `domination needs a superset of areas, not an overlap`() {
|
||
val holder = Label(5, setOf(1, 2))
|
||
assertTrue(holder.dominates(Label(5, setOf(1))))
|
||
assertTrue(holder.dominates(Label(5, setOf(1, 2))))
|
||
assertFalse(
|
||
holder.dominates(Label(5, setOf(1, 3))),
|
||
"an overlapping but incomplete set was treated as domination",
|
||
)
|
||
}
|
||
|
||
/** Two labels can be incomparable, neither dominates the other. */
|
||
@Test
|
||
fun `disjoint compartments are incomparable in both directions`() {
|
||
val a = Label(5, setOf(1))
|
||
val b = Label(5, setOf(2))
|
||
assertFalse(a.dominates(b))
|
||
assertFalse(b.dominates(a))
|
||
}
|
||
|
||
@Test
|
||
fun `bottom is dominated by everything`() {
|
||
assertTrue(Label(0).dominates(Label.bottom))
|
||
assertTrue(Label(9, setOf(3)).dominates(Label.bottom))
|
||
}
|
||
|
||
// ---- Clearance: no read up -------------------------------------------.
|
||
|
||
@Test
|
||
fun `a clearance reads what it dominates`() {
|
||
val cleared = Clearance(maximum = Label(5, setOf(1, 2)))
|
||
assertTrue(cleared.mayRead(Label(3, setOf(1))))
|
||
assertTrue(cleared.mayRead(Label(5, setOf(1, 2))))
|
||
}
|
||
|
||
@Test
|
||
fun `a clearance cannot read above itself`() {
|
||
val cleared = Clearance(maximum = Label(5))
|
||
assertFalse(cleared.mayRead(Label(6)))
|
||
assertFalse(
|
||
cleared.mayRead(Label(5, setOf(1))),
|
||
"a compartment outside the clearance was readable",
|
||
)
|
||
}
|
||
|
||
/**
|
||
* The minimum is a real bound, not decoration.
|
||
*/
|
||
@Test
|
||
fun `a clearance with a floor cannot read below it`() {
|
||
val cleared = Clearance(maximum = Label(5), minimum = Label(2))
|
||
assertTrue(cleared.mayRead(Label(3)))
|
||
assertFalse(cleared.mayRead(Label(1)), "a label below the floor was readable")
|
||
}
|
||
|
||
// ---- Writing ---------------------------------------------------------.
|
||
|
||
/**
|
||
* Writing is gated by the same range check as reading, and no-write-down is
|
||
* NOT enforced, on either platform. Mirrored by ClearanceTests on the Swift
|
||
* side; the two change together or not at all.
|
||
*/
|
||
@Test
|
||
fun `writing uses the same range check as reading, star-property not enforced`() {
|
||
val cleared = Clearance(maximum = Label(5))
|
||
assertTrue(cleared.mayWrite(Label(5)))
|
||
assertTrue(
|
||
cleared.mayWrite(Label(1)),
|
||
"the star-property is now enforced, see android#15, and update the " +
|
||
"iOS side and this test together",
|
||
)
|
||
assertFalse(cleared.mayWrite(Label(8)))
|
||
assertFalse(cleared.mayRead(Label(8)))
|
||
}
|
||
|
||
@Test
|
||
fun `a floor bounds writing as well as reading`() {
|
||
val cleared = Clearance(maximum = Label(5), minimum = Label(2))
|
||
assertTrue(cleared.mayWrite(Label(3)))
|
||
assertFalse(cleared.mayWrite(Label(1)))
|
||
}
|
||
|
||
@Test
|
||
fun `a compartment outside the clearance cannot be written either`() {
|
||
val cleared = Clearance(maximum = Label(5, setOf(1)))
|
||
assertTrue(cleared.mayWrite(Label(5, setOf(1))))
|
||
assertFalse(
|
||
cleared.mayWrite(Label(5, setOf(1, 2))),
|
||
"a compartment outside the clearance was writable",
|
||
)
|
||
}
|
||
}
|