Keybase client-side architecture
The Keybase client is a command-line application written in Go. Today it runs on macOS, Linux and Windows. The code lives in the keybase/client GitHub repository.
Client and Service split
The Keybase binary can act as both a command-line tool and a long-running "service" — each time you run a command, both the tool ("client") and the service are involved. The service stays active after each command, whereas a separate client process is created and terminated for each command. If there's work to be done in response to a command (going out on to the network, running PGP, etc), it's done by the service.
On Linux, the service is started in the background when you run a client
command for the first time, and subsequent client commands check
$XDG_RUNTIME_DIR/keybased.sock
for an active service and use it if one
is listening there — this is called "autofork" mode.
On macOS, the service is started by launchd
at boot.
For debugging, you might find it helpful to use --standalone
mode, which
just runs everything in one process.
For our forthcoming KBFS project, the kbfsfuse
binary will act as a client
that talks to the regular Keybase service over the long-running socket.
For iOS and Android, apps can't spawn extra threads, so we'll be using standalone mode there. We can't even have an iOS/Android process and a separate Go process, so we'll need to embed a Go runtime into the single app process and pass messages to it.
For macOS and Electron desktop GUI clients, we'll use separate processes just as we do with the command-line clients.
Secrets
The service caches the user's passphrase to avoid asking the user for it every
time a command is run (so if you use --standalone
you'll have to enter it
every time).
The secrets being cached are:
libkb.PassphraseStream
: an scrypt of the user's Keybase passphrase, cached in the service aslibkb.PassphraseStreamCache
.- A login session salt cached as
libkb.LoginSession.Salt()
.
Interacting with GPG
We try to run crypto operations internally, using Go's openpgp
module, but
there are some situations (such as importing local keys) that require shelling
out to GPG — you can run keybase help gpg
to learn more.
RPC protocol
The client and service communicate by using an RPC protocol called framed-msgpack-rpc (which is a version of msgpack-rpc with message framing added). This is a duplex protocol — the client opens an RPC connection to the service and makes function calls, and the service can do the same back to the client.
Passing the argument --local-rpc-debug-unsafe=csv
allows you to see the content of these RPC transactions. (It's "unsafe"
because private data is emitted to the logs.)
Protocol bindings
We use a language-independent protocol description format to define all of the
available commands and their arguments and return values. The protocol format
is called AVDL and lives in
client/protocol/avdl/*
.
We automatically generate per-language bindings (objc, JS, Golang) from this
protocol. For example, the generated Golang bindings are written to
client/go/protocol/keybase_v1.go
and imported into client code as keybase1
.
The reason to use a language-independent protocol is that we're expecting to have command-line, GUI, and mobile clients using different programming languages and want to be able to update bindings in one place.
Calling a generated function looks like this in Golang:
type CmdTrack struct {
user string
options keybase1.TrackOptions
}
func (v *CmdTrack) Run() error {
cli, err := GetTrackClient()
if err != nil {
return err
}
protocols := []rpc2.Protocol{
NewIdentifyTrackUIProtocol(),
NewSecretUIProtocol(),
}
if err = RegisterProtocols(protocols); err != nil {
return err
}
return cli.Track(keybase1.TrackArg{
UserAssertion: v.user,
Options: v.options,
})
}
Here GetTrackClient()
returns the keybase1.TrackClient
generated function;
protocols
describes which RPC endpoints the client and service want to use
during the request; keybase1.TrackArg
is the binding-generated struct of
arguments to Track
; the return value of Track
is of the built-in Golang
type error
. So cli.Track
will run on the service, returning its result
back to the client.
Adding a new function to the protocol
If we wanted to add a new function to the TrackClient
, we'd add its
definition to client/protocol/avdl/track.avdl
,
and then run make
in client/protocol
with Java installed. (If we wanted to add a new protocol in a new AVDL file,
we'd add it to the build-stamp section of client/protocol/Makefile
too.)
General file structure
client/cmd_*
- client-side handling of commands, e.g.client/cmd_track.go
libkb/
- service-side lower-level library functions, e.g.libkb/track.go
engine/*
- service-side higher-level library functions, e.g.engine/track*.go
. Most calls to the service are just wrappers around engines that do most of the work. This is also where most of the testing occurs.
Messaging
Since the service is doing the real work, it's going to be coming up with
messages to show the user. Messages are sent to the client over the
NewLogUIProtocol
, itself described in keybase1.LogUiProtocol
.
Logs (usually created with G.Log.*()
) are automatically forwarded (again, via
RPC) from the service to the client so that they can be seen in one place.
You can turn on debugging logs with keybase -d <command>
.
Tips for working with the code
Building
On OSX, the supported way to build from source is with brew
:
brew install go # avoid building Go from source
brew --build-from-source keybase/beta/kbstage
On Linux you can build directly from the client
repo:
git clone https://github.com/keybase/client
cd client/packaging
./clean_build_kbstage.sh