Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Merge the experimental password changes into the trunk. |
|---|---|
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA1: |
596f3c10feba7ebe31b0aa46038214a7 |
| User & Date: | drh 2010-01-12 13:55:57.000 |
Context
|
2010-01-12
| ||
| 14:10 | Transfer SHA1-encoded passwords on a "configure push|pull user" when the client has Admin privilege. check-in: 9c5322463b user: drh tags: trunk | |
| 13:55 | Merge the experimental password changes into the trunk. check-in: 596f3c10fe user: drh tags: trunk | |
| 13:47 | Reverted previous commit [1bf6cf832d] as it contains a major flaw of wiki links not being rendered. I tested on simple cases only, will reimplement in a way that allows wiki links to be rendered properly. check-in: b9897bb934 user: jeremy_c tags: trunk | |
|
2010-01-11
| ||
| 16:21 | Additional clarification in the Password Management document. Closed-Leaf check-in: 261e55346d user: drh tags: experimental | |
Changes
Changes to src/cgi.c.
| ︙ | ︙ | |||
396 397 398 399 400 401 402 403 404 405 406 407 408 409 |
if( nAllocQP<=nUsedQP ){
nAllocQP = nAllocQP*2 + 10;
aParamQP = realloc( aParamQP, nAllocQP*sizeof(aParamQP[0]) );
if( aParamQP==0 ) exit(1);
}
aParamQP[nUsedQP].zName = zName;
aParamQP[nUsedQP].zValue = zValue;
aParamQP[nUsedQP].seq = seqQP++;
nUsedQP++;
sortQP = 1;
}
/*
** Add another query parameter or cookie to the parameter set.
| > > > | 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
if( nAllocQP<=nUsedQP ){
nAllocQP = nAllocQP*2 + 10;
aParamQP = realloc( aParamQP, nAllocQP*sizeof(aParamQP[0]) );
if( aParamQP==0 ) exit(1);
}
aParamQP[nUsedQP].zName = zName;
aParamQP[nUsedQP].zValue = zValue;
if( g.fHttpTrace ){
fprintf(stderr, "# cgi: %s = [%s]\n", zName, zValue);
}
aParamQP[nUsedQP].seq = seqQP++;
nUsedQP++;
sortQP = 1;
}
/*
** Add another query parameter or cookie to the parameter set.
|
| ︙ | ︙ | |||
1251 1252 1253 1254 1255 1256 1257 |
if( child>0 ) nchildren++;
close(connection);
}else{
close(0);
dup(connection);
close(1);
dup(connection);
| | | 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 |
if( child>0 ) nchildren++;
close(connection);
}else{
close(0);
dup(connection);
close(1);
dup(connection);
if( !g.fHttpTrace && !g.fSqlTrace ){
close(2);
dup(connection);
}
close(connection);
return 0;
}
}
|
| ︙ | ︙ |
Changes to src/db.c.
| ︙ | ︙ | |||
1177 1178 1179 1180 1181 1182 1183 |
}
/*
** This function registers auxiliary functions when the SQLite
** database connection is first established.
*/
LOCAL void db_connection_init(void){
| < < | | | | | | | | | | < < | 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 |
}
/*
** This function registers auxiliary functions when the SQLite
** database connection is first established.
*/
LOCAL void db_connection_init(void){
sqlite3_exec(g.db, "PRAGMA foreign_keys=OFF;", 0, 0, 0);
sqlite3_create_function(g.db, "user", 0, SQLITE_ANY, 0, db_sql_user, 0, 0);
sqlite3_create_function(g.db, "cgi", 1, SQLITE_ANY, 0, db_sql_cgi, 0, 0);
sqlite3_create_function(g.db, "cgi", 2, SQLITE_ANY, 0, db_sql_cgi, 0, 0);
sqlite3_create_function(g.db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0);
sqlite3_create_function(
g.db, "file_is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0
);
if( g.fSqlTrace ){
sqlite3_trace(g.db, db_sql_trace, 0);
}
}
/*
** Return true if the string zVal represents "true" (or "false").
*/
int is_truth(const char *zVal){
|
| ︙ | ︙ |
Changes to src/http.c.
| ︙ | ︙ | |||
36 37 38 39 40 41 42 |
** of all payload that follows the login card. SIGNATURE is the sha1
** checksum of the nonce followed by the user password.
**
** Write the constructed login card into pLogin. pLogin is initialized
** by this routine.
*/
static void http_build_login_card(Blob *pPayload, Blob *pLogin){
| | > | > | | < | > > > > > > > > > > > > > > > > > > > > | | | < | | | | 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 |
** of all payload that follows the login card. SIGNATURE is the sha1
** checksum of the nonce followed by the user password.
**
** Write the constructed login card into pLogin. pLogin is initialized
** by this routine.
*/
static void http_build_login_card(Blob *pPayload, Blob *pLogin){
Blob nonce; /* The nonce */
const char *zLogin; /* The user login name */
const char *zPw; /* The user password */
Blob pw; /* The nonce with user password appended */
Blob sig; /* The signature field */
blob_zero(&nonce);
blob_zero(&pw);
sha1sum_blob(pPayload, &nonce);
blob_copy(&pw, &nonce);
blob_zero(pLogin);
if( g.urlUser==0 ){
user_select();
zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", g.userUid);
zLogin = g.zLogin;
}else{
if( g.urlPasswd==0 ){
if( strcmp(g.urlUser,"anonymous")!=0 ){
char *zPrompt = mprintf("password for %s: ", g.urlUser);
Blob x;
prompt_for_password(zPrompt, &x, 0);
free(zPrompt);
g.urlPasswd = blob_str(&x);
}else{
g.urlPasswd = "";
}
}
zPw = g.urlPasswd;
zLogin = g.urlUser;
}
/* The login card wants the SHA1 hash of the password, so convert the
** password to its SHA1 hash it it isn't already a SHA1 hash.
**
** Except, if the password begins with "*" then use the characters
** after the "*" as a cleartext password. Put an "*" at the beginning
** of the password to trick a newer client to use the cleartext password
** protocol required by legacy servers.
*/
if( zPw && zPw[0] ){
if( zPw[0]=='*' ){
zPw++;
}else{
zPw = sha1_shared_secret(zPw, zLogin);
}
}
blob_append(&pw, zPw, -1);
sha1sum_blob(&pw, &sig);
blob_appendf(pLogin, "login %F %b %b\n", zLogin, &nonce, &sig);
blob_reset(&pw);
blob_reset(&sig);
blob_reset(&nonce);
}
/*
** Construct an appropriate HTTP request header. Write the header
** into pHdr. This routine initializes the pHdr blob. pPayload is
** the complete payload (including the login card) already compressed.
*/
|
| ︙ | ︙ |
Changes to src/login.c.
| ︙ | ︙ | |||
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
void login_page(void){
const char *zUsername, *zPasswd;
const char *zNew1, *zNew2;
const char *zAnonPw = 0;
int anonFlag;
char *zErrMsg = "";
int uid; /* User id loged in user */
login_check_credentials();
zUsername = P("u");
zPasswd = P("p");
anonFlag = P("anon")!=0;
if( P("out")!=0 ){
const char *zCookieName = login_cookie_name();
cgi_set_cookie(zCookieName, "", 0, -86400);
redirect_to_g();
}
if( g.okPassword && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){
if( db_int(1, "SELECT 0 FROM user"
| > > | > > | | 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 |
void login_page(void){
const char *zUsername, *zPasswd;
const char *zNew1, *zNew2;
const char *zAnonPw = 0;
int anonFlag;
char *zErrMsg = "";
int uid; /* User id loged in user */
char *zSha1Pw;
login_check_credentials();
zUsername = P("u");
zPasswd = P("p");
anonFlag = P("anon")!=0;
if( P("out")!=0 ){
const char *zCookieName = login_cookie_name();
cgi_set_cookie(zCookieName, "", 0, -86400);
redirect_to_g();
}
if( g.okPassword && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){
zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin);
if( db_int(1, "SELECT 0 FROM user"
" WHERE uid=%d AND (pw=%Q OR pw=%Q)",
g.userUid, zPasswd, zSha1Pw) ){
sleep(1);
zErrMsg =
@ <p><font color="red">
@ You entered an incorrect old password while attempting to change
@ your password. Your password is unchanged.
@ </font></p>
;
}else if( strcmp(zNew1,zNew2)!=0 ){
zErrMsg =
@ <p><font color="red">
@ The two copies of your new passwords do not match.
@ Your password is unchanged.
@ </font></p>
;
}else{
char *zNewPw = sha1_shared_secret(zNew1, g.zLogin);
db_multi_exec(
"UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
);
redirect_to_g();
return;
}
}
uid = isValidAnonymousLogin(zUsername, zPasswd);
if( uid>0 ){
|
| ︙ | ︙ | |||
194 195 196 197 198 199 200 201 202 203 204 |
zCookie = sqlite3_mprintf("anon/%s/%s", zNow, blob_buffer(&b));
blob_reset(&b);
free(zNow);
cgi_set_cookie(zCookieName, zCookie, 0, 6*3600);
redirect_to_g();
}
if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
uid = db_int(0,
"SELECT uid FROM user"
" WHERE login=%Q"
" AND login NOT IN ('anonymous','nobody','developer','reader')"
| > | | | 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
zCookie = sqlite3_mprintf("anon/%s/%s", zNow, blob_buffer(&b));
blob_reset(&b);
free(zNow);
cgi_set_cookie(zCookieName, zCookie, 0, 6*3600);
redirect_to_g();
}
if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
zSha1Pw = sha1_shared_secret(zPasswd, zUsername);
uid = db_int(0,
"SELECT uid FROM user"
" WHERE login=%Q"
" AND login NOT IN ('anonymous','nobody','developer','reader')"
" AND (pw=%Q OR pw=%Q)",
zUsername, zPasswd, zSha1Pw
);
if( uid<=0 ){
sleep(1);
zErrMsg =
@ <p><font color="red">
@ You entered an unknown user or an incorrect password.
@ </font></p>
|
| ︙ | ︙ | |||
418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
zCap = db_column_malloc(&s, 1);
}
db_finalize(&s);
if( zCap==0 ){
zCap = "";
}
}
/* Set the global variables recording the userid and login. The
** "nobody" user is a special case in that g.zLogin==0.
*/
g.userUid = uid;
if( g.zLogin && strcmp(g.zLogin,"nobody")==0 ){
g.zLogin = 0;
| > > > | 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
zCap = db_column_malloc(&s, 1);
}
db_finalize(&s);
if( zCap==0 ){
zCap = "";
}
}
if( g.fHttpTrace && g.zLogin ){
fprintf(stderr, "# login: [%s] with capabilities [%s]\n", g.zLogin, zCap);
}
/* Set the global variables recording the userid and login. The
** "nobody" user is a special case in that g.zLogin==0.
*/
g.userUid = uid;
if( g.zLogin && strcmp(g.zLogin,"nobody")==0 ){
g.zLogin = 0;
|
| ︙ | ︙ |
Changes to src/main.c.
| ︙ | ︙ | |||
824 825 826 827 828 829 830 |
}
db_close();
if( cgi_http_server(iPort, mxPort, zBrowserCmd) ){
fossil_fatal("unable to listen on TCP socket %d", iPort);
}
g.httpIn = stdin;
g.httpOut = stdout;
| | | 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 |
}
db_close();
if( cgi_http_server(iPort, mxPort, zBrowserCmd) ){
fossil_fatal("unable to listen on TCP socket %d", iPort);
}
g.httpIn = stdin;
g.httpOut = stdout;
if( g.fHttpTrace || g.fSqlTrace ){
fprintf(stderr, "====== SERVER pid %d =======\n", getpid());
}
g.cgiPanic = 1;
if( g.argc==2 ){
db_must_be_within_tree();
}else{
db_open_repository(g.argv[2]);
|
| ︙ | ︙ |
Changes to src/schema.c.
| ︙ | ︙ | |||
93 94 95 96 97 98 99 100 101 102 103 104 105 106 | @ uid INTEGER REFERENCES user, -- User login @ mtime DATETIME, -- Time or receipt @ nonce TEXT UNIQUE, -- Nonce used for login @ ipaddr TEXT -- Remote IP address. NULL for direct. @ ); @ @ -- Information about users @ -- @ CREATE TABLE user( @ uid INTEGER PRIMARY KEY, -- User ID @ login TEXT, -- login name of the user @ pw TEXT, -- password @ cap TEXT, -- Capabilities of this user @ cookie TEXT, -- WWW login cookie | > > > > > > > | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | @ uid INTEGER REFERENCES user, -- User login @ mtime DATETIME, -- Time or receipt @ nonce TEXT UNIQUE, -- Nonce used for login @ ipaddr TEXT -- Remote IP address. NULL for direct. @ ); @ @ -- Information about users @ -- @ -- The user.pw field can be either cleartext of the password, or @ -- a SHA1 hash of the password. If the user.pw field is exactly 40 @ -- characters long we assume it is a SHA1 hash. Otherwise, it is @ -- cleartext. The sha1_shared_secret() routine computes the password @ -- hash based on the project-code, the user login, and the cleartext @ -- password. @ -- @ CREATE TABLE user( @ uid INTEGER PRIMARY KEY, -- User ID @ login TEXT, -- login name of the user @ pw TEXT, -- password @ cap TEXT, -- Capabilities of this user @ cookie TEXT, -- WWW login cookie |
| ︙ | ︙ |
Changes to src/setup.c.
| ︙ | ︙ | |||
316 317 318 319 320 321 322 |
if( au ){ zCap[i++] = 'u'; }
if( av ){ zCap[i++] = 'v'; }
if( aw ){ zCap[i++] = 'w'; }
if( az ){ zCap[i++] = 'z'; }
zCap[i] = 0;
zPw = P("pw");
| > | > > < | 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 |
if( au ){ zCap[i++] = 'u'; }
if( av ){ zCap[i++] = 'v'; }
if( aw ){ zCap[i++] = 'w'; }
if( az ){ zCap[i++] = 'z'; }
zCap[i] = 0;
zPw = P("pw");
zLogin = P("login");
if( isValidPwString(zPw) ){
zPw = sha1_shared_secret(zPw, zLogin);
}else{
zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid);
}
if( uid>0 &&
db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d", zLogin, uid)
){
style_header("User Creation Error");
@ <font color="red">Login "%h(zLogin)" is already used by a different
@ user.</font>
@
|
| ︙ | ︙ | |||
471 472 473 474 475 476 477 |
@ <input type="checkbox" name="aw"%s(oaw)/>%s(B('w'))Write Tkt<br>
@ <input type="checkbox" name="at"%s(oat)/>%s(B('t'))Tkt Report<br>
@ <input type="checkbox" name="az"%s(oaz)/>%s(B('z'))Download Zip
@ </td>
@ </tr>
@ <tr>
@ <td align="right">Password:</td>
| < < | | | 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
@ <input type="checkbox" name="aw"%s(oaw)/>%s(B('w'))Write Tkt<br>
@ <input type="checkbox" name="at"%s(oat)/>%s(B('t'))Tkt Report<br>
@ <input type="checkbox" name="az"%s(oaz)/>%s(B('z'))Download Zip
@ </td>
@ </tr>
@ <tr>
@ <td align="right">Password:</td>
if( zPw[0] ){
/* Obscure the password for all users */
@ <td><input type="password" name="pw" value="**********"></td>
}else{
/* Show an empty password as an empty input field */
@ <td><input type="password" name="pw" value=""></td>
}
@ </tr>
if( !higherUser ){
|
| ︙ | ︙ |
Changes to src/sha1.c.
| ︙ | ︙ | |||
532 533 534 535 536 537 538 539 540 541 542 543 544 545 | } blob_resize(pCksum, 40); SHA1Result(&ctx, zResult); DigestToBase16(zResult, blob_buffer(pCksum)); return 0; } /* ** COMMAND: sha1sum ** %fossil sha1sum FILE... ** ** Compute an SHA1 checksum of all files named on the command-line. ** If an file is named "-" then take its content from standard input. | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 |
}
blob_resize(pCksum, 40);
SHA1Result(&ctx, zResult);
DigestToBase16(zResult, blob_buffer(pCksum));
return 0;
}
/*
** Compute the SHA1 checksum of a zero-terminated string. The
** result is held in memory obtained from mprintf().
*/
char *sha1sum(const char *zIn){
SHA1Context ctx;
unsigned char zResult[20];
char zDigest[41];
SHA1Reset(&ctx);
SHA1Input(&ctx, (unsigned const char*)zIn, strlen(zIn));
SHA1Result(&ctx, zResult);
DigestToBase16(zResult, zDigest);
return mprintf("%s", zDigest);
}
/*
** Convert a cleartext password for a specific user into a SHA1 hash.
**
** The algorithm here is:
**
** SHA1( project-code + "/" + login + "/" + password )
**
** In words: The users login name and password are appended to the
** project ID code and the SHA1 hash of the result is computed.
**
** The result of this function is the shared secret used by a client
** to authenticate to a server for the sync protocol. It is also the
** value stored in the USER.PW field of the database. By mixing in the
** login name and the project id with the hash, different shared secrets
** are obtained even if two users select the same password, or if a
** single user selects the same password for multiple projects.
*/
char *sha1_shared_secret(const char *zPw, const char *zLogin){
static char *zProjectId = 0;
SHA1Context ctx;
unsigned char zResult[20];
char zDigest[41];
SHA1Reset(&ctx);
if( zProjectId==0 ){
zProjectId = db_get("project-code", 0);
/* On the first xfer request of a clone, the project-code is not yet
** known. Use the cleartext password, since that is all we have.
*/
if( zProjectId==0 ){
return mprintf("%s", zPw);
}
}
SHA1Input(&ctx, (unsigned char*)zProjectId, strlen(zProjectId));
SHA1Input(&ctx, (unsigned char*)"/", 1);
SHA1Input(&ctx, (unsigned char*)zLogin, strlen(zLogin));
SHA1Input(&ctx, (unsigned char*)"/", 1);
SHA1Input(&ctx, (unsigned const char*)zPw, strlen(zPw));
SHA1Result(&ctx, zResult);
DigestToBase16(zResult, zDigest);
return mprintf("%s", zDigest);
}
/*
** COMMAND: sha1sum
** %fossil sha1sum FILE...
**
** Compute an SHA1 checksum of all files named on the command-line.
** If an file is named "-" then take its content from standard input.
|
| ︙ | ︙ |
Changes to src/user.c.
| ︙ | ︙ | |||
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 |
db_find_and_open_repository(1);
if( g.argc<3 ){
usage("capabilities|default|list|new|password ...");
}
n = strlen(g.argv[2]);
if( n>=2 && strncmp(g.argv[2],"new",n)==0 ){
Blob passwd, login, contact;
if( g.argc>=4 ){
blob_init(&login, g.argv[3], -1);
}else{
prompt_user("login: ", &login);
}
if( db_exists("SELECT 1 FROM user WHERE login=%B", &login) ){
fossil_fatal("user %b already exists", &login);
}
if( g.argc>=5 ){
blob_init(&contact, g.argv[4], -1);
}else{
prompt_user("contact-info: ", &contact);
}
if( g.argc>=6 ){
blob_init(&passwd, g.argv[5], -1);
}else{
prompt_for_password("password: ", &passwd, 1);
}
db_multi_exec(
"INSERT INTO user(login,pw,cap,info)"
| > > | | > | 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 |
db_find_and_open_repository(1);
if( g.argc<3 ){
usage("capabilities|default|list|new|password ...");
}
n = strlen(g.argv[2]);
if( n>=2 && strncmp(g.argv[2],"new",n)==0 ){
Blob passwd, login, contact;
char *zPw;
if( g.argc>=4 ){
blob_init(&login, g.argv[3], -1);
}else{
prompt_user("login: ", &login);
}
if( db_exists("SELECT 1 FROM user WHERE login=%B", &login) ){
fossil_fatal("user %b already exists", &login);
}
if( g.argc>=5 ){
blob_init(&contact, g.argv[4], -1);
}else{
prompt_user("contact-info: ", &contact);
}
if( g.argc>=6 ){
blob_init(&passwd, g.argv[5], -1);
}else{
prompt_for_password("password: ", &passwd, 1);
}
zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login));
db_multi_exec(
"INSERT INTO user(login,pw,cap,info)"
"VALUES(%B,%Q,'v',%B)",
&login, zPw, &contact
);
free(zPw);
}else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){
user_select();
if( g.argc==3 ){
printf("%s\n", g.zLogin);
}else{
if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){
fossil_fatal("no such user: %s", g.argv[3]);
|
| ︙ | ︙ | |||
245 246 247 248 249 250 251 |
}else{
zPrompt = mprintf("new passwd for %s: ", g.argv[3]);
prompt_for_password(zPrompt, &pw, 1);
}
if( blob_size(&pw)==0 ){
printf("password unchanged\n");
}else{
| > | > | 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
}else{
zPrompt = mprintf("new passwd for %s: ", g.argv[3]);
prompt_for_password(zPrompt, &pw, 1);
}
if( blob_size(&pw)==0 ){
printf("password unchanged\n");
}else{
char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3]);
db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid);
free(zSecret);
}
}else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
int uid;
if( g.argc!=4 && g.argc!=5 ){
usage("user capabilities USERNAME ?PERMISSIONS?");
}
uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.argv[3]);
|
| ︙ | ︙ | |||
342 343 344 345 346 347 348 |
"INSERT INTO user(login, pw, cap, info)"
"VALUES('anonymous', '', 'cfghjkmnoqw', '')"
);
g.userUid = db_last_insert_rowid();
g.zLogin = "anonymous";
}
}
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 |
"INSERT INTO user(login, pw, cap, info)"
"VALUES('anonymous', '', 'cfghjkmnoqw', '')"
);
g.userUid = db_last_insert_rowid();
g.zLogin = "anonymous";
}
}
/*
** Compute the shared secret for a user.
*/
static void user_sha1_shared_secret_func(
sqlite3_context *context,
int argc,
sqlite3_value **argv
){
char *zPw;
char *zLogin;
assert( argc==2 );
zPw = (char*)sqlite3_value_text(argv[0]);
zLogin = (char*)sqlite3_value_text(argv[1]);
if( zPw && zLogin ){
sqlite3_result_text(context, sha1_shared_secret(zPw, zLogin), -1, free);
}
}
/*
** COMMAND: test-hash-passwords
**
** Usage: %fossil test-hash-passwords REPOSITORY
**
** Convert all local password storage to use a SHA1 hash of the password
** rather than cleartext. Passwords that are already stored as the SHA1
** has are unchanged.
*/
void user_hash_passwords_cmd(void){
if( g.argc!=3 ) usage("REPOSITORY");
db_open_repository(g.argv[2]);
sqlite3_create_function(g.db, "sha1_shared_secret", 2, SQLITE_UTF8, 0,
user_sha1_shared_secret_func, 0, 0);
db_multi_exec(
"UPDATE user SET pw=sha1_shared_secret(pw,login)"
" WHERE length(pw)>0 AND length(pw)!=40"
);
}
|
Changes to src/xfer.c.
| ︙ | ︙ | |||
355 356 357 358 359 360 361 | sha1sum_blob(&tail, &h2); rc = blob_compare(pHash, &h2); blob_reset(&h2); blob_reset(&tail); return rc==0; } | < | 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | sha1sum_blob(&tail, &h2); rc = blob_compare(pHash, &h2); blob_reset(&h2); blob_reset(&tail); return rc==0; } /* ** Check the signature on an application/x-fossil payload received by ** the HTTP server. The signature is a line of the following form: ** ** login LOGIN NONCE SIGNATURE ** ** The NONCE is the SHA1 hash of the remainder of the input. |
| ︙ | ︙ | |||
392 393 394 395 396 397 398 399 400 401 402 403 |
"SELECT pw, cap, uid FROM user"
" WHERE login=%Q"
" AND login NOT IN ('anonymous','nobody','developer','reader')"
" AND length(pw)>0",
zLogin
);
if( db_step(&q)==SQLITE_ROW ){
Blob pw, combined, hash;
blob_zero(&pw);
db_ephemeral_blob(&q, 0, &pw);
blob_zero(&combined);
blob_copy(&combined, pNonce);
| > > | < > > > > > > > > > > > > > > > > > > > > | 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 |
"SELECT pw, cap, uid FROM user"
" WHERE login=%Q"
" AND login NOT IN ('anonymous','nobody','developer','reader')"
" AND length(pw)>0",
zLogin
);
if( db_step(&q)==SQLITE_ROW ){
int szPw;
Blob pw, combined, hash;
blob_zero(&pw);
db_ephemeral_blob(&q, 0, &pw);
szPw = blob_size(&pw);
blob_zero(&combined);
blob_copy(&combined, pNonce);
blob_append(&combined, blob_buffer(&pw), szPw);
sha1sum_blob(&combined, &hash);
assert( blob_size(&hash)==40 );
rc = blob_compare(&hash, pSig);
blob_reset(&hash);
blob_reset(&combined);
if( rc!=0 && szPw!=40 ){
/* If this server stores cleartext passwords and the password did not
** match, then perhaps the client is sending SHA1 passwords. Try
** again with the SHA1 password.
*/
const char *zPw = db_column_text(&q, 0);
char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin));
blob_zero(&combined);
blob_copy(&combined, pNonce);
blob_append(&combined, zSecret, -1);
free(zSecret);
sha1sum_blob(&combined, &hash);
rc = blob_compare(&hash, pSig);
blob_reset(&hash);
blob_reset(&combined);
}
if( rc==0 ){
const char *zCap;
zCap = db_column_text(&q, 1);
login_set_capabilities(zCap);
g.userUid = db_column_int(&q, 2);
g.zLogin = mprintf("%b", pLogin);
g.zNonce = mprintf("%b", pNonce);
if( g.fHttpTrace ){
fprintf(stderr, "# login [%s] with capabilities [%s]\n", g.zLogin,zCap);
}
}
}
db_finalize(&q);
if( rc==0 ){
/* If the login was successful. */
login_set_anon_nobody_capabilities();
|
| ︙ | ︙ | |||
648 649 650 651 652 653 654 |
}else
/* pull SERVERCODE PROJECTCODE
** push SERVERCODE PROJECTCODE
**
** The client wants either send or receive. The server should
| | < < < < < < < < < < < < < < < < < < < < < < < | 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 |
}else
/* pull SERVERCODE PROJECTCODE
** push SERVERCODE PROJECTCODE
**
** The client wants either send or receive. The server should
** verify that the project code matches.
*/
if( xfer.nToken==3
&& (blob_eq(&xfer.aToken[0], "pull") || blob_eq(&xfer.aToken[0], "push"))
&& blob_is_uuid(&xfer.aToken[1])
&& blob_is_uuid(&xfer.aToken[2])
){
const char *zPCode;
zPCode = db_get("project-code", 0);
if( zPCode==0 ){
fossil_panic("missing project code");
}
if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){
cgi_reset_content();
@ error wrong\sproject
|
| ︙ | ︙ | |||
721 722 723 724 725 726 727 728 729 730 731 732 733 734 |
**
** The client knows nothing. Tell all.
*/
if( blob_eq(&xfer.aToken[0], "clone") ){
login_check_credentials();
if( !g.okClone ){
cgi_reset_content();
@ error not\sauthorized\sto\sclone
nErr++;
break;
}
isClone = 1;
isPull = 1;
deltaFlag = 1;
| > | 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 |
**
** The client knows nothing. Tell all.
*/
if( blob_eq(&xfer.aToken[0], "clone") ){
login_check_credentials();
if( !g.okClone ){
cgi_reset_content();
@ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x"))
@ error not\sauthorized\sto\sclone
nErr++;
break;
}
isClone = 1;
isPull = 1;
deltaFlag = 1;
|
| ︙ | ︙ | |||
958 959 960 961 962 963 964 |
"CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);"
);
blobarray_zero(xfer.aToken, count(xfer.aToken));
blob_zero(&send);
blob_zero(&recv);
blob_zero(&xfer.err);
blob_zero(&xfer.line);
| | | 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 |
"CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);"
);
blobarray_zero(xfer.aToken, count(xfer.aToken));
blob_zero(&send);
blob_zero(&recv);
blob_zero(&xfer.err);
blob_zero(&xfer.line);
origConfigRcvMask = 0;
/*
** Always begin with a clone, pull, or push message
*/
if( cloneFlag ){
blob_appendf(&send, "clone\n");
pushFlag = 0;
|
| ︙ | ︙ | |||
1003 1004 1005 1006 1007 1008 1009 |
request_phantoms(&xfer, mxPhantomReq);
}
if( pushFlag ){
send_unsent(&xfer);
nCardSent += send_unclustered(&xfer);
}
| | > > > | > | > > > | 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 |
request_phantoms(&xfer, mxPhantomReq);
}
if( pushFlag ){
send_unsent(&xfer);
nCardSent += send_unclustered(&xfer);
}
/* Send configuration parameter requests. On a clone, delay sending
** this until the second cycle since the login card might fail on
** the first cycle.
*/
if( configRcvMask && (cloneFlag==0 || nCycle>0) ){
const char *zName;
zName = configure_first_name(configRcvMask);
while( zName ){
blob_appendf(&send, "reqconfig %s\n", zName);
zName = configure_next_name(configRcvMask);
nCardSent++;
}
if( configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT) ){
configure_prepare_to_receive(0);
}
origConfigRcvMask = configRcvMask;
configRcvMask = 0;
}
/* Send configuration parameters being pushed */
if( configSendMask ){
const char *zName;
zName = configure_first_name(configSendMask);
while( zName ){
send_config_card(&xfer, zName);
zName = configure_next_name(configSendMask);
nCardSent++;
}
configSendMask = 0;
}
/* Append randomness to the end of the message. This makes all
** messages unique so that that the login-card nonce will always
** be unique.
*/
zRandomness = db_text(0, "SELECT hex(randomblob(20))");
blob_appendf(&send, "# %s\n", zRandomness);
free(zRandomness);
/* Exchange messages with the server */
nFileSend = xfer.nFileSent + xfer.nDeltaSent;
printf(zValueFormat, "Send:",
|
| ︙ | ︙ | |||
1211 1212 1213 1214 1215 1216 1217 |
char *zMsg = blob_terminate(&xfer.aToken[1]);
defossilize(zMsg);
printf("\rServer says: %s\n", zMsg);
}else
/* error MESSAGE
**
| | > > > > > > > > | | | | > | 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 |
char *zMsg = blob_terminate(&xfer.aToken[1]);
defossilize(zMsg);
printf("\rServer says: %s\n", zMsg);
}else
/* error MESSAGE
**
** Report an error and abandon the sync session.
**
** Except, when cloning we will sometimes get an error on the
** first message exchange because the project-code is unknown
** and so the login card on the request was invalid. The project-code
** is returned in the reply before the error card, so second and
** subsequent messages should be OK. Nevertheless, we need to ignore
** the error card on the first message of a clone.
*/
if( blob_eq(&xfer.aToken[0],"error") && xfer.nToken==2 ){
if( !cloneFlag || nCycle>0 ){
char *zMsg = blob_terminate(&xfer.aToken[1]);
defossilize(zMsg);
blob_appendf(&xfer.err, "server says: %s", zMsg);
printf("Server Error: %s\n", zMsg);
}
}else
/* Unknown message */
{
if( blob_str(&xfer.aToken[0])[0]=='<' ){
fossil_fatal(
"server replies with HTML instead of fossil sync protocol:\n%b",
|
| ︙ | ︙ | |||
1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 |
/* If we have one or more files queued to send, then go
** another round
*/
if( xfer.nFileSent+xfer.nDeltaSent>0 ){
go = 1;
}
};
transport_stats(&nSent, &nRcvd, 1);
printf("Total network traffic: %d bytes sent, %d bytes received\n",
nSent, nRcvd);
transport_close();
socket_global_shutdown();
db_multi_exec("DROP TABLE onremote");
manifest_crosslink_end();
db_end_transaction(0);
}
| > > > | 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 |
/* If we have one or more files queued to send, then go
** another round
*/
if( xfer.nFileSent+xfer.nDeltaSent>0 ){
go = 1;
}
/* If this is a clone, the go at least two rounds */
if( cloneFlag && nCycle==1 ) go = 1;
};
transport_stats(&nSent, &nRcvd, 1);
printf("Total network traffic: %d bytes sent, %d bytes received\n",
nSent, nRcvd);
transport_close();
socket_global_shutdown();
db_multi_exec("DROP TABLE onremote");
manifest_crosslink_end();
db_end_transaction(0);
}
|
Added www/password.wiki.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <title>Fossil Password Management</title> <h1 align="center">Password Management</h1> Fossil handles user authentication using passwords. Passwords are unique to each repository. Passwords are not part of the persistent state of a project. Passwords are not versioned and are not transmitted from one repository to another during a sync. Passwords are local configuration information that can (and usually does) vary from one repository to the next within the same project. Passwords are stored in the PW field of the USER table. In older versions of Fossil (prior to [/timeline?c=2010-01-10+20:56:30 | 2010-01-11]) the password is stored as cleartext. In newer versions of Fossil, the password can be either cleartext or an SHA1 hash (written as a 40-character lower-case hexadecimal number). If the USER.PW field contains a 40-character string, that string is assumed to be a SHA1 hash. If the size of USER.PW is anything other than 40 characters, then it is understood as a plain-text password. The SHA1 hash in the USER.PW field is a hash of a string composed of the project-code, the user login, and the user cleartext password. Suppose user "alice" with password "asdfg" had an account on the Fossil self-hosting repository. Then the value of USER.PW for alice would be the SHA1 hash of <blockquote> CE59BB9F186226D80E49D1FA2DB29F935CCA0333/alice/asdfg </blockquote> That hash value is "f1b699cc9af3eeb98e5de244ca7802ae38e77bae". Note that by including the project-code and the login as part of the hash, a different USER.PW value results even if two or more users on the repository select the same "asdfg" password or if user alice reuses the same password on multiple projects. Whenever a password is changed using the web interface or using the "user" command-line method, the new password is stored using the SHA1 encoding. Thus, cleartext passwords will gradually migrate to become SHA1 passwords. All remaining cleartext passwords can be converted to SHA1 passwords using the following command: <blockquote><pre> fossil test-hash-passwords <i>REPOSITORY-NAME</i> </pre></blockquote> Remember that converting from cleartext to SHA1 passwords is an irreversible operation. The only way to insert a new cleartext password into the USER table is to do so manually using SQL commands. For example: <blockquote><pre> UPDATE user SET pw='asdfg' WHERE login='alice'; </pre></blockquote> Note that an password that is an empty string or NULL will disable all login for that user. Thus, to lock a user out of the system, one has only to set their password to an empty string, using either the web interface or direct SQL manipulation of the USER table. Note also that the password field is essentially ignored for the special users named "anonymous", "developer", "reader", and "nobody". It is not possible to authenticate as users "developer", "reader", or "nobody" and the authentication protocol for "anonymous" use one-time captchas not persistent passwords. <h2>Web Interface Authtentication</h2> When a user logs into Fossil using the web interface, the login name and password are sent in the clear to the server. The server then hashes the password and compares it against the value stored in USER.PW. If they match, the server sets a cookie on the client to record the login. This cookie contains a large amount of high-quality randomness and is thus impossible to guess. The value of the cookie and the IP address of the client is stored in the USER.COOKIE and USER.IPADDR fields of the USER table on the server. The USER.CEXPIRE field holds an expiration date for the cookie, encoded as a julian day number. On all subsequent HTTP requests, the cookie value is matched against the USER table to enable access to the repository. A login cookie will only work if the IP address matches. This feature is designed to make it more difficult for an attacker to sniff the cookie and take over the connection. A cookie-sniffing attack will only work if the attacker is able to send and receive from the same IP address as the original login. However, we found that doing an exact IP match caused problems for some users who are behind proxy firewalls where the proxy might use a different IP address for each query. To work around this problem, newer versions of fossil only check the first 16 bits of the 32-bit IP address. This makes a cookie sniffing attack easier since now the attacker only has to send and receive from any IP address in a range of IPs that are similar to the initial login. But that is seen as an acceptable compromise in exchange for ease of use. If higher security is really needed, then HTTPS can be used instead of HTTP. Note that in order to log into a Fossil server, it is necessary to write information into the repository database. Hence, login is not possible on a Fossil repository with a read-only database file. The user password is sent over the wire as cleartext on the initial login attempt. The plan moving forward is to compute the SHA1 hash of the password on the client using javascript and then send only the hash over the wire, but that plan has not yet been set in code. <h2>Sync Protocol Authentication</h2> A different authentication mechanism is used when one repository wants to sync (or push or pull or clone) another respository. When two respositories are syncing, the one that initiates the transaction is the client and the repository that responds is the server. The client works by sending HTTP requests to the server with a method of "xfer" and a content-type of "application/x-fossil". The content is Zlib-compressed text consisting of "cards" of instructions. The first card of this content is a "login" card responsible for authentication. The login card contains the login name of the user and a "signature" where the signature is the SHA1 hash of a nonce and the value of USER.PW. The nonce is the SHA1 hash of the remainder of the request content after the newline (ASCII 14) character that terminates the login card. Using this approach, the USER.PW value is treated as a shared secret between the client and server. The USER.PW value is never sent over the wire, but the protocol establishes that both client and server know the value of USER.PW. Furthermore, the use of a SHA1 hash over the entire message prevents an attacker from sniffing a valid login from a legitimate users and then replaying the message modified content. If the USER.PW on the server holds a cleartext password, then the server will also accept a login-card signature that is constructed using either the cleartext password or the SHA1 hash of the password. This means that when USER.PW holds a cleartext password, the login card will work for both older and newer clients. If the USER.PW on the server only holds the SHA1 hash of the password, then only newer clients will be able to authenticate to the server. The client normally gets the login and password from the "remote URL". <blockquote><pre> http://<font color="blue">login:password</font>@servername.org/path </pre></blockquote> For older clients, the password is used for the shared secret as stated in the URL and with no encoding. For newer clients, the shared secret is derived from the password by transformed the password using the SHA1 hash encoding described above. However, if the first character of the password is "*" (ASCII 0x2a) then the "*" is skipped and the rest of the password is used directly as the share secret without the SHA1 encoding. <blockquote><pre> http://<font color="blue">login:*password</font>@servername.org/path </pre></blockquote> This *-before-the-password trick can be used by newer clients to sync against a legacy server that does not understand the new SHA1 password encoding. |