|
| 1 | +import base64, pegs, random, strutils, hmac, nimSHA2, private/utils |
| 2 | + |
| 3 | +type |
| 4 | + ScramError* = object of SystemError |
| 5 | + |
| 6 | + ScramState = enum |
| 7 | + INITIAL |
| 8 | + FIRST_PREPARED |
| 9 | + FINAL_PREPARED |
| 10 | + ENDED |
| 11 | + |
| 12 | + DigestType* = enum |
| 13 | + SHA1 |
| 14 | + SHA256 |
| 15 | + SHA512 |
| 16 | + |
| 17 | + ScramClient = ref object of RootObj |
| 18 | + clientNonce: string |
| 19 | + clientFirstBareMessage: string |
| 20 | + digestType: DigestType |
| 21 | + state: ScramState |
| 22 | + isSuccessful: bool |
| 23 | + serverSignature: string |
| 24 | + |
| 25 | +const |
| 26 | + GS2_HEADER = "n,," |
| 27 | + INT_1 = "\x0\x0\x0\x1" |
| 28 | + CLIENT_KEY = "Client Key" |
| 29 | + SERVER_KEY = "Server Key" |
| 30 | + |
| 31 | +let |
| 32 | + SERVER_FIRST_MESSAGE = peg"'r='{[^,]*}',s='{[^,]*}',i='{\d+}$" |
| 33 | + SERVER_FINAL_MESSAGE = peg"'v='{[^,]*}$" |
| 34 | + |
| 35 | +proc hi(s: ScramClient, password, salt: string, iterations: int): string = |
| 36 | + var previous: string |
| 37 | + result = $hmac_sha256(password, salt & INT_1) |
| 38 | + previous = result |
| 39 | + for _ in 1..<iterations: |
| 40 | + previous = $hmac_sha256(password, previous) |
| 41 | + result ^= previous |
| 42 | + |
| 43 | +proc newScramClient*(digestType: DigestType): ScramClient = |
| 44 | + result = new(ScramClient) |
| 45 | + result.state = INITIAL |
| 46 | + result.digestType = digestType |
| 47 | + result.clientNonce = makeNonce() |
| 48 | + result.isSuccessful = false |
| 49 | + |
| 50 | +proc prepareFirstMessage*(s: ScramClient, username: string): string {.raises: [ScramError]} = |
| 51 | + if username.isNilOrEmpty: |
| 52 | + raise newException(ScramError, "username cannot be nil or empty") |
| 53 | + |
| 54 | + var username = username.replace("=", "=3D").replace(",", "=2C") |
| 55 | + |
| 56 | + |
| 57 | + s.clientFirstBareMessage = "n=" |
| 58 | + s.clientFirstBareMessage.add(username) |
| 59 | + s.clientFirstBareMessage.add(",r=") |
| 60 | + s.clientFirstBareMessage.add(s.clientNonce) |
| 61 | + |
| 62 | + result = GS2_HEADER & s.clientFirstBareMessage |
| 63 | + s.state = FIRST_PREPARED |
| 64 | + |
| 65 | +proc prepareFinalMessage*(s: ScramClient, password, serverFirstMessage: string): string {.raises: [ScramError, OverflowError, ValueError].} = |
| 66 | + var |
| 67 | + nonce, salt: string |
| 68 | + iterations: int |
| 69 | + |
| 70 | + if s.state != FIRST_PREPARED: |
| 71 | + raise newException(ScramError, "First message have not been prepared, call prepareFirstMessage() first") |
| 72 | + |
| 73 | + if serverFirstMessage =~ SERVER_FIRST_MESSAGE: |
| 74 | + nonce = matches[0] |
| 75 | + salt = decode(matches[1]) |
| 76 | + iterations = parseInt(matches[2]) |
| 77 | + else: |
| 78 | + s.state = ENDED |
| 79 | + return nil |
| 80 | + |
| 81 | + if not nonce.startsWith(s.clientNonce) or iterations < 0: |
| 82 | + s.state = ENDED |
| 83 | + return nil |
| 84 | + |
| 85 | + let |
| 86 | + saltedPassword = s.hi(password, salt, iterations) |
| 87 | + clientKey = $hmac_sha256(saltedPassword, CLIENT_KEY) |
| 88 | + storedKey = $computeSHA256(clientKey) |
| 89 | + serverKey = $hmac_sha256(saltedPassword, SERVER_KEY) |
| 90 | + clientFinalMessageWithoutProof = "c=" & encode(GS2_HEADER) & ",r=" & nonce |
| 91 | + authMessage = s.clientFirstBareMessage & "," & serverFirstMessage & "," & clientFinalMessageWithoutProof |
| 92 | + clientSignature = $hmac_sha256(storedKey, authMessage) |
| 93 | + |
| 94 | + s.serverSignature = $hmac_sha256(serverKey, authMessage) |
| 95 | + |
| 96 | + var clientProof = clientKey |
| 97 | + clientProof ^= clientSignature |
| 98 | + s.state = FINAL_PREPARED |
| 99 | + result = clientFinalMessageWithoutProof & ",p=" & encode(clientProof, newLine="") |
| 100 | + |
| 101 | +proc verifyServerFinalMessage*(s: ScramClient, serverFinalMessage: string): bool = |
| 102 | + if s.state != FINAL_PREPARED: |
| 103 | + raise newException(ScramError, "You can call this method only once after calling prepareFinalMessage()") |
| 104 | + s.state = ENDED |
| 105 | + if serverFinalMessage =~ SERVER_FINAL_MESSAGE: |
| 106 | + let proposedServerSignature = decode(matches[0]) |
| 107 | + s.isSuccessful = proposedServerSignature == s.serverSignature |
| 108 | + |
| 109 | + result = s.isSuccessful |
| 110 | + |
| 111 | +proc isSuccessful*(s: ScramClient): bool = |
| 112 | + if s.state != ENDED: |
| 113 | + raise newException(ScramError, "You cannot call this method before authentication is ended") |
| 114 | + return s.isSuccessful |
| 115 | + |
| 116 | +proc isEnded*(s: ScramClient): bool = |
| 117 | + result = s.state == ENDED |
| 118 | + |
| 119 | +proc getState*(s: ScramClient): ScramState = |
| 120 | + result = s.state |
| 121 | + |
| 122 | +when isMainModule: |
| 123 | + var s = newScramClient(SHA256) |
| 124 | + s.clientNonce = "VeAOLsQ22fn/tjalHQIz7cQT" |
| 125 | + |
| 126 | + echo s.prepareFirstMessage("bob") |
| 127 | + let finalMessage = s.prepareFinalMessage("secret", "r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,s=ldZSefTzKxPNJhP73AmW/A==,i=4096") |
| 128 | + echo finalMessage |
| 129 | + assert(finalMessage == "c=biws,r=VeAOLsQ22fn/tjalHQIz7cQTmeE5qJh8qKEe8wALMut1,p=AtNtxGzsMA8evcWBM0MXFjxN8OcG1KRkLkFyoHlupOU=") |
0 commit comments