Snippets

Fantom-Factory SCRAM over SASL for SkySpark v3

Created by Steve Eynon last modified

SCRAM over SASL for SkySpark v3

This is an example program that retrieves an authentication token from SkySpark for use in subsequent calls to the REST API.

Sample usage:

class Example {
    Void main() {
        // get the authToken
        authToken := SkySparkAuth().scram(`http://localhost:8080/ui`, "<username>", "<password>")       
        echo("authToken: ${authToken}")

        // call the REST API
        zincRes := WebClient(`http://localhost:8080/api/<proj>/about`) {
            it.reqHeaders["Authorization"] = "BEARER authToken=${authToken}"
        }.getStr
    }
}

See the SCRAM over SASL for SkySpark v3 article on Alien-Factory for details.

using util::Random
using web::WebClient

** Note this is not a particularly robust implementation, but it does provide an easy to follow
** algorithm that you may use to convert to your language of choice.
class SkySparkAuth {
    private Str? handshakeToken
    private Str? hashAlgorithm
    private Str? authToken
    private Str:Int	dkLens := [
        "SHA-1"		: 20,
        "SHA-256"	: 32,
        "SHA-512"	: 64
    ]

    Str scram(Uri serverUrl, Str userName, Str password) {
        gs2Header        := "n,,"
    
        // ---- Hello! ----
        sendMsg1(serverUrl, userName)
        
        // ---- 1st message ----
        random           := Random.makeSecure
        clientNonce      := Buf().writeI8(random.next).writeI8(random.next).toBase64
        clientFirstMsg   := "n=${userName},r=${clientNonce}"
        serverFirstMsg   := sendMsg2(serverUrl, clientFirstMsg)

        payloadValues    := Str:Str[:].addList(serverFirstMsg.split(',')) { it[0..<1] }.map { it[2..-1] }
        serverNonce      := payloadValues["r"]
        serverSalt       := payloadValues["s"]
        serverIterations := Int.fromStr(payloadValues["i"])

        // ---- 2nd message ----
        dkLen            := dkLens[hashAlgorithm]    // the size of the hash in bytes
        saltedPassword   := Buf.pbk("PBKDF2WithHmac" + hashAlgorithm.replace("-", ""), password, Buf.fromBase64(serverSalt), serverIterations, dkLen)
        clientFinalNoPf  := "c=${gs2Header.toBuf.toBase64},r=${serverNonce}"
        authMessage      := "${clientFirstMsg},${serverFirstMsg},${clientFinalNoPf}"
        clientKey        := "Client Key".toBuf.hmac(hashAlgorithm, saltedPassword)
        storedKey        := clientKey.toDigest(hashAlgorithm)
        clientSignature  := authMessage.toBuf.hmac(hashAlgorithm, storedKey)
        clientProof      := xor(clientKey, clientSignature) 
        clientFinalMsg   := "${clientFinalNoPf},p=${clientProof.toBase64}"
        serverKey        := "Server Key".toBuf.hmac(hashAlgorithm, saltedPassword)
        serverSignature  := authMessage.toBuf.hmac(hashAlgorithm, serverKey).toBase64
        serverSecondMsg  := sendMsg3(serverUrl, clientFinalMsg)

        payloadValues     = Str:Str[:].addList(serverSecondMsg.split(',')) { it[0..<1] }.map { it[2..-1] }
        serverProof      := payloadValues["v"]

        // authenticate the server
        if (serverSignature != serverProof)
            throw Err("SkySpark Auth: Server sent invalid SCRAM signature '${serverProof}' - was expecting '${serverSignature}'")

        return authToken
    }
    
    private Void sendMsg1(Uri serverUrl, Str msg) {
        authMsg          := "HELLO username=${msg.toBuf.toBase64Uri}"
        authParams       := retrieveAuthParams(serverUrl, authMsg, "WWW-Authenticate", "SCRAM ".size)
        handshakeToken   = authParams["handshakeToken"]
        // note this does not handle the return of multiple hash algorithms
        hashAlgorithm    = authParams["hash"]
    }
    
    private Str sendMsg2(Uri serverUrl, Str msg) {
        authMsg          := "SCRAM handshakeToken=${handshakeToken}, data=${msg.toBuf.toBase64Uri}"
        authParams       := retrieveAuthParams(serverUrl, authMsg, "WWW-Authenticate", "SCRAM ".size)
        dataRaw          := authParams["data"]
        serverMsg        := Buf.fromBase64(dataRaw).readAllStr
        return serverMsg
    }
    
    private Str sendMsg3(Uri serverUrl, Str msg) {
        authMsg          := "SCRAM handshakeToken=${handshakeToken}, data=${msg.toBuf.toBase64Uri}"
        authParams       := retrieveAuthParams(serverUrl, authMsg, "Authentication-Info", 0)
        authToken        = authParams["authToken"]
        dataRaw          := authParams["data"]
        serverMsg        := Buf.fromBase64(dataRaw).readAllStr
        return serverMsg
    }
    
    private Str:Str retrieveAuthParams(Uri serverUrl, Str authMsg, Str resHeader, Int stripSize) {
        client := WebClient(serverUrl) {
            it.reqHeaders["Authorization"] = authMsg
        }.writeReq.readRes
                
        authData := client.resHeaders[resHeader][stripSize..-1]

        authParams := Str:Str[:] { it.caseInsensitive = true }
        authData.split(',').each {
            key := it.split('=')[0]
            val := it.split('=')[1]
            authParams[key] = val
        }
        return authParams
    }
    
    private Buf xor(Buf key, Buf sig) {
        out := Buf()
        key.size.times {
            out.write(key.read.xor(sig.read))
        }
        return out.flip
    }
}

Comments (0)