Practical Security II: The Mafia IRC Bot (WIP)¶
To demonstrate secure distributed programming in Monte, let’s take the mafia game code developed earlier and make it into an IRC bot.
The mafiabot.mt
module begins by importing the
mafia
module, an irc/client
library, and the same modules for dealing
with entropy that we saw before:
1 2 3 4 5 6 | import "mafia" =~ [=> makeMafia :DeepFrozen]
import "irc/client" =~ [=> makeIRCClient :DeepFrozen,
=> connectIRCClient :DeepFrozen]
import "lib/entropy/entropy" =~ [=> makeEntropy :DeepFrozen]
import "lib/entropy/pcg" =~ [=> makePCG :DeepFrozen]
exports (main)
|
The main
entry point is provided with a number of powerful references as
named arguments:
- To seed our random number generator, we use
currentRuntime
to get a source of true randomness, i.e. secure entropy.- To give
makeIRCService
access to TCP/IP networking and event scheduling, we usemakeTPC4ClientEndPoint
,getAddrInfo
, andTimer
.
192 193 194 195 196 197 198 199 200 201 202 | def main(argv,
=> makeTCP4ClientEndpoint,
=> Timer,
=> currentRuntime,
=> getAddrInfo) as DeepFrozen:
def [_, seed] := currentRuntime.getCrypt().makeSecureEntropy().getEntropy()
def rng := makeEntropy(makePCG(seed, 0))
def [hostname] := argv
def irc := makeIRCService(makeTCP4ClientEndpoint, getAddrInfo, Timer,
hostname)
irc.connect(makeMafiaBot(rng))
|
We can go ahead and run this code from a file by using the monte
commandline
tool:
monte eval mafiabot.mt chat.freenode.net
Everything after the source filename is passed to main in argv
as a list of
strings.
Networking¶
Unlike many other contemporary programming languages, Monte does not need an additional networking library to provide solid primitive and high-level networking operations. This is because Monte was designed to handle networking as easily as any other kind of input or output.
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | def makeIRCService(makeTCP4ClientEndpoint, getAddrInfo, Timer,
hostname :Str) as DeepFrozen:
def port := 6667 # TODO: named arg with default value
return object IRC:
to _printOn(out):
out.print(`IRC($hostname)`)
to connect(handler):
def client := makeIRCClient(handler, Timer)
def addrs := getAddrInfo(b`$hostname`, b``)
return when (addrs) ->
def choices := [
for addr in (addrs)
? (addr.getFamily() == "INET" &&
addr.getSocketType() == "stream") addr.getAddress()]
def [address] + _ := choices
def ep := makeTCP4ClientEndpoint(address, port)
connectIRCClient(client, ep)
client
|
Distributed Systems¶
Monte comes with builtin explicit parallelism suitable for scaling to arbitrary numbers of processes or machines, and a well-defined concurrency system that simplifies and streamlines the task of writing event-driven code.
Monte has one concurrent operation. Monte permits messages to be passed as eventual sends. An eventually-sent message will be passed to the target object at a later time, generating a promise which can have more messages sent to it. Unlike similar mechanisms in Twisted, Node.js, etc., Monte builds promises and eventual sending directly into the language and runtime, removing the need for extraneous libraries.
Monte also has a single primitive for combining isolation and parallelism, the vat. Each vat isolates a collection of objects from objects in other vats. Each eventual send in a vat becomes a distinct turn of execution, and vats execute concurrently with one another. During a turn, a vat delivers a single queued send, which could result in more sends being queued up for subsequent turns.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | def makeChannelVow(client, name) as DeepFrozen:
"Return a vow because say() won't work until we have joined."
def [wait, done] := Ref.promise()
var waitingFor :NullOk[Set[Str]]:= null
object chan:
to _printOn(out):
out.print(`<channel $name>`)
to getName():
return name
to hasJoined():
return client.hasJoined(name)
to say(message) :Void:
client.say(name, message)
to getUsers(notReady):
return client.getUsers(name, notReady)
to waitFor(them :Set[Str]):
waitingFor := them
return wait
to notify():
if (waitingFor != null):
escape oops:
def present := chan.getUsers(oops).getKeys().asSet()
traceln("notify present:", present, waitingFor,
waitingFor - present)
if ((waitingFor - present).size() == 0):
waitingFor := null
done.resolve(present)
to tell(whom, what, notInChannel):
if (chan.getUsers(notInChannel).contains(whom)):
client.say(whom, what)
else:
notInChannel(`cannot tell $whom: not in $name`)
to part(message):
client.part(name, message)
return when(chan.hasJoined()) ->
chan
|
Principle of Least Authority¶
Straightforward object-oriented design results in each object having the least authority it needs:
makeIRCService
provides the full range of IRC client behaviormakeChannelVow
provides access to one channelmakeModerator
encapsulates the play of one gamemakePlayer
represents the role of one player in one gamemakeMafiaBot
starts games on request, routes messages to the relevant moderator during game play, and disposes of moderators when games end.
Even if one of these components is buggy or compromised, its ability to corrupt the system is limited to using the capabilities in its static scope.
Contrast this with traditional identity-based systems, where programs execute with all privileges granted to a user or role. In such a system, any compromise lets the attacker do anything that the user could do. A simple game such as solitaire executes with all authority necessary to corrupt, exfiltrate, or ransom the user’s files.
With object capability discipline, when the time comes for a security inspection, we do not have to consider the possibility that any compromise in any part of our program leaves the whole system wide open in this way. Each component in the system can be reviewed independently and auditing a system for security becomes cost-effective to an extent that is infeasible with other approaches [1].
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | def makeModerator(playerNames :Set[Str], rng,
chan :Near, mafiaChan) as DeepFrozen:
def [=> game, => mafiosos] := makeMafia(playerNames, rng)
var night0 := true
def makePlayer(me :Str):
return object player:
to _printOn(out):
out.print(`<player $me>`)
to voteFor(nominee :Str):
try:
game.vote(me, nominee)
catch _:
# nominee is not (any longer) a player
return
chan.say(game.advance())
def toPlayer := [for nick in (playerNames) nick => makePlayer(nick)]
return object moderator:
to _printOn(out):
out.print(`<moderator in $chan>`)
to begin():
# Night 0
chan.say(`$game`)
when (mafiaChan) ->
escape notHere:
for maf in (mafiosos):
chan.tell(
maf, `You're a mafioso in $chan.`, notHere)
chan.tell(
maf, `Join $mafiaChan to meet the others.`, notHere)
traceln("waiting for", mafiosos, "in", mafiaChan)
when (mafiaChan.waitFor(mafiosos)) ->
traceln("done waiting for", mafiosos)
night0 := false
# Morning of day 1...
chan.say(game.advance())
to said(who :Str, message :Str) :Bool:
"Return true to contine, false if game over."
mafiaChan.notify()
traceln("notifying", mafiaChan)
if (night0):
return true
if (message =~ `lynch @whom!`):
escape notPlaying:
def p := moderator.getPlayer(who, notPlaying)
p.voteFor(whom)
traceln("lynch", who, whom)
if (game.getWinner() =~ winner ? (winner != null)):
moderator.end()
return game.getWinner() == null
to getPlayer(name, notPlaying):
return toPlayer.fetch(name, notPlaying)
to end():
chan.say(`$game`)
chan.part("Good game!")
mafiaChan.part("bye bye")
|
Note the way makeMafiaBot
provides a secret channel for the mafiosos to
collude at night:
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | def makeMafiaBot(rng) as DeepFrozen:
def nick := "mafiaBot"
def chanMod := [].asMap().diverge()
def keys := [].asMap().diverge()
return object mafiaBot:
to getNick():
return nick
to loggedIn(client):
return null
to privmsg(client, user, channel, message):
# traceln("mafiaBot got", message, "on", channel, "from", user,
# "channels", chanMod.getKeys())
def who := user.getNick()
if (message =~ `join @dest` &&
channel == nick &&
!keys.contains(dest)):
mafiaBot.join(client, who, dest)
else if (message == "start" &&
!keys.contains(channel)):
when(def chan := makeChannelVow(client, channel)) ->
mafiaBot.startGame(client, chan, channel)
else if (chanMod.snapshot() =~ [(channel) => m] | _):
if (!m.said(who, message)):
def chKey := keys[channel]
chanMod.removeKey(channel)
chanMod.removeKey(chKey)
keys.removeKey(channel)
keys.removeKey(chKey)
traceln("removed", channel, chKey)
to join(client, who :Str, channel :Str):
when(client.hasJoined(channel)) ->
client.say(channel, `Thank you for inviting me, $who.`)
client.say(channel, `Say "start" to begin.`)
to startGame(client, chan :Near, channel :Str):
def secret := `$channel-${rng.nextInt(2 ** 32)}`
def secretChan := makeChannelVow(client, secret)
escape notReady:
def users := chan.getUsers(notReady)
def playerNames := [
for name => _ in (users)
? (name != nick)
# @chanop -> chanop
(if (name =~ `@@@op`) { op } else { name })]
traceln("players:", playerNames, users)
def m := makeModerator(playerNames.asSet(), rng,
chan, secretChan)
chanMod[channel] := chanMod[secret] := m
keys[channel] := secret
keys[secret] := channel
m.begin()
|
Notes
[1] | As documented in the DarpaBrowser report |