1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
|
<?php
////////////////////////////////////////////////////
//
// Authentication server, reference implementation 0.1
//
// To run authority X, this script needs to be
// adapted and be callable from a web browser as
// http://X/armaauth/0.1
//
// the easiest way to do so is to name the script index.php
// and place it into an appropriate armaauth/0.1
// directory on a web server that supports php.
//
////////////////////////////////////////////////////
// however, before they are used, %u is replaced by the username.
function substitutions( $fix, $user )
{
return str_replace( "%u", $user, $fix );
}
// this function reports the result of the check back to the
// qerying AA server.
function conclude($statusCode, $msg)
{
// if you do want to abuse return error codes (the servers
// work fine with parsing the text response), uncomment
// the next line. The request is technically always successful.
$statusCode = 200;
// the status codes passed in have been chosen somewhat
// meaningfully (200: ok, 404: something not found,
// 401: password failure) but still violate the standard.
// report error code in header
header("Status: $statusCode", true, $statusCode);
header("Content-Type: text/plain");
// print message
die("$msg\n");
}
// read authority from gloval variables
$authority = $_SERVER['HTTP_HOST'];
////////////////////////////////////////////////////
// Bits you need to change follow.
////////////////////////////////////////////////////
// You should definitely check that the hostname the game
// server used to contact you is the one you intend it
// to use; otherwise, there may be problems with web
// servers known under different names. You should uncomment
// this and add your real authority hostname.
/*
if ( $authority != "authority" )
conclude( 404, "WRONG_HOST" );
*/
// The user "database": fetch the password for a user.
// If you are fine with storing plaintext passwords,
// adapt this function; otherwise, adapt getPasswordHash().
function getPassword( $user )
{
// the user/password database. For very small groups
// of users, you may just get away with expanding this
// array.
$passwords= array (
'test' => 'password', // clever choice there, test
'z-män' => 'passwörd' // test for utf8 usernames and passwords
);
$password = $passwords[ $user ];
if ( NULL == $password )
return NULL;
// return a pair of username and password.
// it is important that you return the username
// exactly as it appeared in the database.
// If the username lookup is case insensitive,
// the rest of the script and the game servers
// need to know what the correct form of the name
// is.
return array( $user, $password );
}
// these two functions return prefix and suffix for the md5
// hash method. They are prepended/appended to the password
// before md5 is applied on it. Adapt them to the way your
// md5 password hash is stored in your database.
// IMPORTANT: if you want to keep the %u (a good idea for
// security, prevents precomputation attacks on the passwords)
// user name lookup needs to be case sensitive, or there will
// be unexplainable password failures.
function getPrefix()
{
return "%u:aaauth:";
}
function getSuffix()
{
global $authority;
return ":$authority";
}
// You do not need plain text passwords. The
// checks on the passwords are done to a hash function
// thereof. You can just as well precompute the
// work of this function and store the result,
// and maybe your user database already contains
// the one or other hash. phpBB, for example,
// stores md5( $password ).
function getPasswordHash( $user, $method )
{
// fetch the plaintext password
$userInfo = getPassword( $user );
if ( NULL == $userInfo )
return NULL;
// unpack the data
$trueUser = $userInfo[0];
$password = $userInfo[1];
// check that neither prefix nor suffix conain %u if $trueUser != $user
if ( $trueUser != $user && ( strpos( getPrefix(), '%u' ) !== FALSE || strpos( getPrefix(), '%u' ) !== FALSE ) )
{
conclude(404, 'UNKNOWN_USER ' . $user . ' Do not use %u in pre/suffix if your user database is making case-insensitive lookups.');
}
// two methods are currently supported
// by server and client, bmd5 (broken md5)
// and md5. Both use the md5 hash algorithm.
switch( $method )
{
case 'bmd5':
// bmd5 quirk: the hash is computed for password + a trailing 0
$password = "$password" . chr(0);
break;
case "md5":
// md5 adds the prefix and suffix before calculating
// the hash. The example values are chosen so that the
// resulting hash is the same one you find in .htdigest
// files and that is used in the digest http authentication
// method. If you set the prefix and suffix to empty
// strings, the resulting hash will be the one found
// in phpBB user databases.
$password = substitutions( getPrefix(), $trueUser ) . "$password" . substitutions( getSuffix(), $trueUser );
break;
}
// after that, both methods just calculate the md5 hash
// and return that.
return array( $trueUser, md5( $password ) );
}
// comma separated lists of methods you support. If, for example,
// you cannot generate the hash for the bmd5 method, just
// remove it from this list.
function getMethods()
{
return "md5, bmd5";
}
////////////////////////////////////////////////////
// The rest of the script should not be changed,
////////////////////////////////////////////////////
// Of course, the AA password query passes in some variables.
// The first is the query type; it can either be
// 'methods' for a list of supported methods,
// 'params' for a list of method parameters, or
// 'check' to check a user's identity.
$query = @strtolower($_REQUEST['query'] . '');
switch ( $query )
{
case "methods":
// give lists of supported methods. It's supposed to be "methods",
// followed by a comma and/or whitespace separated list of the methods.
conclude(200, 'METHODS '. getMethods() );
break;
// the other two queries pass in more parameters, both have
// the 'method' parameter that tells you which method should
// be used. Well behaved servers will pick one of the methods
// you support.
case "params":
$method = @strtolower($_REQUEST['method'] . '');
// give method parameter info
if ( $method == 'md5' )
{
// give prefix and suffix
conclude( 200, "PREFIX " . getPrefix() . "\nSUFFIX " . getSuffix() );
}
if ( $method == 'bmd5' )
{
// bmd5 does not support or need extra parameters.
conclude(200 , '' );
}
// we know nothing about this.
conclude(404 , 'UNKNOWN_METHOD' );
break;
case "check":
$method = @strtolower($_REQUEST['method'] . '');
// the last query passes in the username to check,
$user = @$_REQUEST['user'] . '';
// a salt value (128 bit hexcoded)
$salt = @$_REQUEST['salt'] . '';
// and a hash value the AA client has computed.
$hash = @$_REQUEST['hash'] . '';
// we're supposed to check whether that hash is correct;
// to do so, we need to do the same operations to the
// password the client has done.
// first, the client computed a hash of the password
// with method-specific rules. <TV cook mode> we have
// already prepared that here. </TV cook mode>.
$userInfo = getPasswordHash( $user, $method );
// check if user exists in the first place.
if ( $userInfo == NULL )
{
conclude(404, 'UNKNOWN_USER ' . $user );
}
// unpack user info
$trueUser = $userInfo[0];
$correctPasswordHash = $userInfo[1];
// the operations the AA client did were not on the hex-encoded
// hashes we have so far, but on binary packed variants thereof:
$packedSalt = pack("H*", $salt);
$correctPackedPasswordHash = pack('H*', $correctPasswordHash);
// the client then simply calculated the MD5 sum of the two
// concatenated packed values.
$correctHash = md5($correctPackedPasswordHash . $packedSalt);
// well, let's see if the client got it right!
if (strcasecmp($hash, $correctHash) === 0)
{
// he did! Return OK, followed by the user's full name.
// You can also append "blurb" messages in subsequent lines.
// Those messages will be transformed a little (the first word gets
// AUTHORITY_BLURB_ prepended to it and the user's authenticated name
// is added as a second token) and written to the server's ladder log.
conclude(200, 'PASSWORD_OK ' . $trueUser . '@' . $authority . "\nFOO baz" );
// A useful blurb example would be a line containing:
// ALIAS oldusername@oldauthority
// It would get logged as
// AUTHORITY_BLURB_ALIAS user@authority oldusername@oldauthority
// and nice ladder log parsing scripts can choose to believe your
// authority and assume user@authority and oldusername@oldauthority
// are the same person. It is a matter between ladderlog parser writers
// and authority implementers to define more useful standard blurb messages.
}
// he didn't.
conclude(401, 'PASSWORD_FAIL' );
// That's it. Note that any response other that a text beginning
// with "PASSWORD_OK" and a 200 return code will be interpreted
// by the server as an error and authentication will fail.
// String comparisons for the control words (METHODS, PASSWORD_OK)
// are case-insensitive on the server. We just like to return them
// as caps becasue that looks more like CONTOL_CODES.
break;
default:
// we don't know what the server wants from us
// if execution ends up here.
conclude(404, 'UNKNOWN_QUERY');
}
?>
|