This repository has been archived by the owner on Jan 15, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 151
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP Adds support to enable SCRAM Authentication
- Loading branch information
Cristian Cepeda
committed
Mar 31, 2019
1 parent
32cba17
commit 964a7fa
Showing
4 changed files
with
204 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
module Moped | ||
module Protocol | ||
module Commands | ||
class ScramAuthenticate | ||
SCRAM_SHA_1_MECHANISM = 'SCRAM-SHA-1'.freeze | ||
CLIENT_CONTINUE_MESSAGE = { saslContinue: 1 }.freeze | ||
CLIENT_FIRST_MESSAGE = { saslStart: 1, autoAuthorize: 1 }.freeze | ||
CLIENT_KEY = 'Client Key'.freeze | ||
ID = 'conversationId'.freeze | ||
ITERATIONS = /i=(\d+)/.freeze | ||
MIN_ITER_COUNT = 4096 | ||
PAYLOAD = 'payload'.freeze | ||
RNONCE = /r=([^,]*)/.freeze | ||
SALT = /s=([^,]*)/.freeze | ||
SERVER_KEY = 'Server Key'.freeze | ||
VERIFIER = /v=([^,]*)/.freeze | ||
|
||
attr_reader \ | ||
:database, | ||
:username, | ||
:password, | ||
:nonce, | ||
:result | ||
|
||
def initialize(database, username, password) | ||
@database = database | ||
@username = username | ||
@password = password | ||
end | ||
|
||
def start(result) | ||
@nonce = result[Protocol::NONCE] | ||
Protocol::Command.new(database, { | ||
saslStart: 1, | ||
autoAuthorize: 1, | ||
payload: client_first_message, | ||
mechanism: SCRAM_SHA_1_MECHANISM | ||
}) | ||
end | ||
|
||
def continue(result) | ||
validate_first_message!(result) | ||
salted_password | ||
|
||
Protocol::Command.new(database, { | ||
saslContinue: 1, | ||
payload: client_final_message, | ||
conversationId: result[ID] | ||
}) | ||
end | ||
|
||
def finalize(result) | ||
Protocol::Command.new( | ||
database, | ||
CLIENT_CONTINUE_MESSAGE.merge( | ||
payload: client_empty_message, | ||
conversationId: result[ID] | ||
) | ||
) | ||
end | ||
|
||
private | ||
|
||
def client_empty_message | ||
BSON::Binary.new(:md5, '') | ||
end | ||
|
||
def hmac(data, key) | ||
OpenSSL::HMAC.digest(digest, data, key) | ||
end | ||
|
||
def xor(first, second) | ||
first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('') | ||
end | ||
|
||
def validate_first_message!(result) | ||
validate!(result) | ||
# raise Error::InvalidNonce.new(nonce, rnonce) unless rnonce.start_with?(nonce) | ||
end | ||
|
||
def client_key | ||
@client_key ||= hmac(salted_password, CLIENT_KEY) | ||
end | ||
|
||
def client_proof(key, signature) | ||
@client_proof ||= Base64.strict_encode64(xor(key, signature)) | ||
end | ||
|
||
def client_final | ||
@client_final ||= client_proof(client_key, client_signature(stored_key(client_key), auth_message)) | ||
end | ||
|
||
def auth_message | ||
@auth_message ||= "#{first_bare},#{result[PAYLOAD].data},#{without_proof}" | ||
end | ||
|
||
def stored_key(key) | ||
h(key) | ||
end | ||
|
||
def h(string) | ||
digest.digest(string) | ||
end | ||
|
||
def client_signature(key, message) | ||
@client_signature ||= hmac(key, message) | ||
end | ||
|
||
def without_proof | ||
@without_proof ||= "c=biws,r=#{rnonce}" | ||
end | ||
|
||
def client_final_message | ||
BSON::Binary.new(:md5, "#{without_proof},p=#{client_final}") | ||
end | ||
|
||
def rnonce | ||
@rnonce ||= payload_data.match(RNONCE)[1] | ||
end | ||
|
||
def validate!(result) | ||
if result[Protocol::OK] != 1 | ||
# raise Unauthorized.new(user, full_mechanism) | ||
end | ||
@result = result | ||
end | ||
|
||
def payload_data | ||
result[PAYLOAD].data | ||
end | ||
|
||
def iterations | ||
@iterations ||= payload_data.match(ITERATIONS)[1].to_i.tap do |i| | ||
if i < MIN_ITER_COUNT | ||
# raise Error::InsufficientIterationCount.new( | ||
# Error::InsufficientIterationCount.message(MIN_ITER_COUNT, i)) | ||
end | ||
end | ||
end | ||
|
||
def hi(data) | ||
OpenSSL::PKCS5.pbkdf2_hmac_sha1( | ||
data, | ||
Base64.strict_decode64(salt), | ||
iterations, | ||
digest.size | ||
) | ||
end | ||
|
||
def salt | ||
@salt ||= payload_data.match(SALT)[1] | ||
end | ||
|
||
def digest | ||
@digest ||= OpenSSL::Digest::SHA1.new.freeze | ||
end | ||
|
||
def salted_password | ||
hi(hashed_password) | ||
end | ||
|
||
def hashed_password | ||
# unless password | ||
# raise Error::MissingPassword | ||
# end | ||
@hashed_password ||= Digest::MD5.hexdigest("#{username}:mongo:#{password}").encode('utf-8') | ||
end | ||
|
||
def client_first_message | ||
BSON::Binary.new(:md5, "n,,#{first_bare}") | ||
end | ||
|
||
def first_bare | ||
@first_bare ||= "n=#{username.encode('utf-8').gsub('=','=3D').gsub(',','=2C')},r=#{nonce}" | ||
end | ||
end | ||
end | ||
end | ||
end |