name:           "Chris"               # required
email:          ""   # required
username:       "chris"               # required
pwh:            "aabbccee..."         # required
pwh_version:    3                     # required
salt:           "7d34343eeeee..."     # required
invitation_id:  "0000000000123"       # required...for now
pdpka5_kid:     "0120aabb....."       # required
  "status": {
      "code": 0
  "csrf_token": "lgHZIDFjZmY0Nzlj..."

Picking a salt

It is the client app's responsibility to generate a salt. Make sure to use a strong pseudo-random number generator. In the browser, for example, we collect entropy from a combination of window.crypto.getRandomValues and a bunch of extra CPU timing.

Your salt should be a 16-byte number, hex encoded. In other words, it should be sent to Keybase as a 32-character hex string.

Generating the password hash (pwh) and PDPKA5 Key (pdpka5_kid)

For these two fields, compute:

passphraseStream = scrypt(passphrase, unhex(salt), N=215, r=8, p=1, dkLen=256)
pwh = passphraseStream[192:224]
v5 = passphraseStream[224:256]
pdpka5_kid = keybaseKID(edDSAPublicKeyFromSeed(v5))
That is, we stretch the input passphrase using the salt generated on signup. One slice of the output stream is interpreted directly as pwh, the legacy passphrase hash. It's more or less ignored on the server. The next slice is treated like an EdDSA private key, and the corresponding public key is sent to the server in Keybase key ID format. See the login API call for more details on how this is used later for logins.

Pssst, we're hiring.