CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/postgres/postgres-pr/scram_sha_256.rb
Views: 1904
1
# -*- coding: binary -*-
2
3
require 'base64'
4
require 'openssl'
5
require 'net/imap'
6
7
# Namespace for Metasploit branch.
8
module Msf
9
module Db
10
module PostgresPR
11
12
# Implements SCRAM-SHA-256 authentication; The caller of #negotiate can additionally wrap the calculated authentication
13
# models with SASL/GSSAPI as appropriate
14
#
15
# https://datatracker.ietf.org/doc/html/rfc7677#section-3
16
class ScramSha256
17
class NormalizeError < ArgumentError
18
end
19
20
# @param [String] user
21
# @param [String] password
22
def negotiate(user, password)
23
random_nonce = b64(SecureRandom.bytes(32))
24
25
# Attributes: https://datatracker.ietf.org/doc/html/rfc5802#section-5
26
client_first_without_gs2_header = "n=#{normalize(user)},r=#{random_nonce}"
27
client_gs2_header = gs2_header(channel_binding: false)
28
client_first = "#{client_gs2_header}#{client_first_without_gs2_header}"
29
30
server_first_string = yield :client_first, client_first
31
32
server_first = parse_server_response(server_first_string)
33
server_nonce = server_first[:r]
34
server_salt = Base64.strict_decode64(server_first[:s])
35
iterations = server_first[:i].to_i
36
37
# https://datatracker.ietf.org/doc/html/rfc5802#section-3
38
salted_password = hi(normalize(password), server_salt, iterations)
39
client_key = hmac(salted_password, "Client Key")
40
stored_key = h(client_key)
41
42
client_final_without_proof = "c=#{b64(client_gs2_header)},r=#{server_nonce}"
43
44
auth_message = [client_first_without_gs2_header, server_first_string, client_final_without_proof].join(',')
45
client_signature = hmac(stored_key, auth_message)
46
client_proof = xor_strings(client_key, client_signature)
47
server_key = hmac(salted_password, "Server Key")
48
expected_server_signature = hmac(server_key, auth_message)
49
50
client_final = "#{client_final_without_proof},p=#{b64(client_proof)}"
51
52
server_final = yield :client_final, client_final
53
raise AuthenticationMethodMismatch, 'Server proof failed' if server_final != "v=#{b64(expected_server_signature)}"
54
55
nil
56
end
57
58
# Implements Normalize from https://datatracker.ietf.org/doc/html/rfc4013 -
59
# Apply the SASLprep profile [RFC4013] of the "stringprep" algorithm [RFC3454]
60
#
61
# @param [String] value
62
# @return [String]
63
def normalize(value)
64
::Net::IMAP::SASL.saslprep(value, exception: true)
65
rescue ArgumentError => e
66
raise NormalizeError, e.message
67
end
68
69
# Hi function implementation from
70
# https://datatracker.ietf.org/doc/html/rfc5802#section-2.2
71
#
72
# @param [String] str
73
# @param [String] salt
74
# @param [Numeric] iteration_count
75
def hi(str, salt, iteration_count)
76
u = hmac(str, "#{salt.b}#{"\x00\x00\x00\x01".b}")
77
u_i = u
78
(iteration_count - 1).times do
79
u_i = hmac(str, u_i)
80
u = xor_strings(u, u_i)
81
end
82
83
u
84
end
85
86
# @return [String]
87
def hash_function_name
88
'SHA256'
89
end
90
91
# H function from
92
# https://datatracker.ietf.org/doc/html/rfc5802#section-2.2
93
#
94
# @param [String] str
95
def h(str)
96
OpenSSL::Digest.digest(hash_function_name, str)
97
end
98
99
# @param [String] key
100
# @param [String] message
101
# @return [String]
102
def hmac(key, message)
103
OpenSSL::HMAC.digest(hash_function_name, key, message)
104
end
105
106
# Implements https://datatracker.ietf.org/doc/html/rfc5801#section-4
107
# @return [String] The bytes for a gs2 header
108
def gs2_header(channel_binding: false)
109
# Specified as gs2-cb-flag
110
if channel_binding
111
# gs2_channel_binding_flag = 'y'
112
# Implementation skipped for now, just haven't
113
raise NotImplementedError, 'Channel binding not implemented'
114
else
115
gs2_channel_binding_flag = 'n'
116
end
117
118
gs2_authzid = nil
119
gs2_header = "#{gs2_channel_binding_flag},#{gs2_authzid},"
120
gs2_header
121
end
122
123
private
124
125
# @param [String] value
126
def b64(value)
127
Base64.strict_encode64(value)
128
end
129
130
# @param [String] s1
131
# @param [String] s2
132
# @return [String] the strings XOR'd
133
def xor_strings(s1, s2)
134
s1.bytes.zip(s2.bytes).map { |(b1, b2)| b1 ^ b2 }.pack("C*")
135
end
136
137
# Parses a server response string such as 'r=2kRpTcHEFyoG+UgDEpRBdVcJLTWh5WtxARhYOHcG27i7YxAi,s=GNpgixWS5E4INbrMf665Kw==,i=4096'
138
# into a Ruby hash equivalent { r: '2kRpT...', i: '4096' }
139
# @param [String] string Server string response string
140
def parse_server_response(string)
141
string.split(',')
142
.each_with_object({}) do |key_value, result|
143
key, value = key_value.split('=', 2)
144
result[key.to_sym] = value
145
end
146
end
147
end
148
149
end
150
end
151
end
152
153