Fossil SCM
Merge the experimental password changes into the trunk.
Commit
596f3c10feba7ebe31b0aa46038214a748e2dbe8
Parent
b9897bb934c4de7…
11 files changed
+4
-1
+10
-14
+31
-11
+12
-4
+1
-1
+7
+6
-6
+59
+46
-3
+53
-36
+66
+4
-1
| --- src/cgi.c | ||
| +++ src/cgi.c | ||
| @@ -398,10 +398,13 @@ | ||
| 398 | 398 | aParamQP = realloc( aParamQP, nAllocQP*sizeof(aParamQP[0]) ); |
| 399 | 399 | if( aParamQP==0 ) exit(1); |
| 400 | 400 | } |
| 401 | 401 | aParamQP[nUsedQP].zName = zName; |
| 402 | 402 | aParamQP[nUsedQP].zValue = zValue; |
| 403 | + if( g.fHttpTrace ){ | |
| 404 | + fprintf(stderr, "# cgi: %s = [%s]\n", zName, zValue); | |
| 405 | + } | |
| 403 | 406 | aParamQP[nUsedQP].seq = seqQP++; |
| 404 | 407 | nUsedQP++; |
| 405 | 408 | sortQP = 1; |
| 406 | 409 | } |
| 407 | 410 | |
| @@ -1253,11 +1256,11 @@ | ||
| 1253 | 1256 | }else{ |
| 1254 | 1257 | close(0); |
| 1255 | 1258 | dup(connection); |
| 1256 | 1259 | close(1); |
| 1257 | 1260 | dup(connection); |
| 1258 | - if( !g.fHttpTrace ){ | |
| 1261 | + if( !g.fHttpTrace && !g.fSqlTrace ){ | |
| 1259 | 1262 | close(2); |
| 1260 | 1263 | dup(connection); |
| 1261 | 1264 | } |
| 1262 | 1265 | close(connection); |
| 1263 | 1266 | return 0; |
| 1264 | 1267 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -398,10 +398,13 @@ | |
| 398 | aParamQP = realloc( aParamQP, nAllocQP*sizeof(aParamQP[0]) ); |
| 399 | if( aParamQP==0 ) exit(1); |
| 400 | } |
| 401 | aParamQP[nUsedQP].zName = zName; |
| 402 | aParamQP[nUsedQP].zValue = zValue; |
| 403 | aParamQP[nUsedQP].seq = seqQP++; |
| 404 | nUsedQP++; |
| 405 | sortQP = 1; |
| 406 | } |
| 407 | |
| @@ -1253,11 +1256,11 @@ | |
| 1253 | }else{ |
| 1254 | close(0); |
| 1255 | dup(connection); |
| 1256 | close(1); |
| 1257 | dup(connection); |
| 1258 | if( !g.fHttpTrace ){ |
| 1259 | close(2); |
| 1260 | dup(connection); |
| 1261 | } |
| 1262 | close(connection); |
| 1263 | return 0; |
| 1264 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -398,10 +398,13 @@ | |
| 398 | aParamQP = realloc( aParamQP, nAllocQP*sizeof(aParamQP[0]) ); |
| 399 | if( aParamQP==0 ) exit(1); |
| 400 | } |
| 401 | aParamQP[nUsedQP].zName = zName; |
| 402 | aParamQP[nUsedQP].zValue = zValue; |
| 403 | if( g.fHttpTrace ){ |
| 404 | fprintf(stderr, "# cgi: %s = [%s]\n", zName, zValue); |
| 405 | } |
| 406 | aParamQP[nUsedQP].seq = seqQP++; |
| 407 | nUsedQP++; |
| 408 | sortQP = 1; |
| 409 | } |
| 410 | |
| @@ -1253,11 +1256,11 @@ | |
| 1256 | }else{ |
| 1257 | close(0); |
| 1258 | dup(connection); |
| 1259 | close(1); |
| 1260 | dup(connection); |
| 1261 | if( !g.fHttpTrace && !g.fSqlTrace ){ |
| 1262 | close(2); |
| 1263 | dup(connection); |
| 1264 | } |
| 1265 | close(connection); |
| 1266 | return 0; |
| 1267 |
M
src/db.c
+10
-14
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -1179,24 +1179,20 @@ | ||
| 1179 | 1179 | /* |
| 1180 | 1180 | ** This function registers auxiliary functions when the SQLite |
| 1181 | 1181 | ** database connection is first established. |
| 1182 | 1182 | */ |
| 1183 | 1183 | LOCAL void db_connection_init(void){ |
| 1184 | - static int once = 1; | |
| 1185 | - if( once ){ | |
| 1186 | - sqlite3_exec(g.db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); | |
| 1187 | - sqlite3_create_function(g.db, "user", 0, SQLITE_ANY, 0, db_sql_user, 0, 0); | |
| 1188 | - sqlite3_create_function(g.db, "cgi", 1, SQLITE_ANY, 0, db_sql_cgi, 0, 0); | |
| 1189 | - sqlite3_create_function(g.db, "cgi", 2, SQLITE_ANY, 0, db_sql_cgi, 0, 0); | |
| 1190 | - sqlite3_create_function(g.db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); | |
| 1191 | - sqlite3_create_function( | |
| 1192 | - g.db, "file_is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 | |
| 1193 | - ); | |
| 1194 | - if( g.fSqlTrace ){ | |
| 1195 | - sqlite3_trace(g.db, db_sql_trace, 0); | |
| 1196 | - } | |
| 1197 | - once = 0; | |
| 1184 | + sqlite3_exec(g.db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); | |
| 1185 | + sqlite3_create_function(g.db, "user", 0, SQLITE_ANY, 0, db_sql_user, 0, 0); | |
| 1186 | + sqlite3_create_function(g.db, "cgi", 1, SQLITE_ANY, 0, db_sql_cgi, 0, 0); | |
| 1187 | + sqlite3_create_function(g.db, "cgi", 2, SQLITE_ANY, 0, db_sql_cgi, 0, 0); | |
| 1188 | + sqlite3_create_function(g.db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); | |
| 1189 | + sqlite3_create_function( | |
| 1190 | + g.db, "file_is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 | |
| 1191 | + ); | |
| 1192 | + if( g.fSqlTrace ){ | |
| 1193 | + sqlite3_trace(g.db, db_sql_trace, 0); | |
| 1198 | 1194 | } |
| 1199 | 1195 | } |
| 1200 | 1196 | |
| 1201 | 1197 | /* |
| 1202 | 1198 | ** Return true if the string zVal represents "true" (or "false"). |
| 1203 | 1199 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -1179,24 +1179,20 @@ | |
| 1179 | /* |
| 1180 | ** This function registers auxiliary functions when the SQLite |
| 1181 | ** database connection is first established. |
| 1182 | */ |
| 1183 | LOCAL void db_connection_init(void){ |
| 1184 | static int once = 1; |
| 1185 | if( once ){ |
| 1186 | sqlite3_exec(g.db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); |
| 1187 | sqlite3_create_function(g.db, "user", 0, SQLITE_ANY, 0, db_sql_user, 0, 0); |
| 1188 | sqlite3_create_function(g.db, "cgi", 1, SQLITE_ANY, 0, db_sql_cgi, 0, 0); |
| 1189 | sqlite3_create_function(g.db, "cgi", 2, SQLITE_ANY, 0, db_sql_cgi, 0, 0); |
| 1190 | sqlite3_create_function(g.db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); |
| 1191 | sqlite3_create_function( |
| 1192 | g.db, "file_is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1193 | ); |
| 1194 | if( g.fSqlTrace ){ |
| 1195 | sqlite3_trace(g.db, db_sql_trace, 0); |
| 1196 | } |
| 1197 | once = 0; |
| 1198 | } |
| 1199 | } |
| 1200 | |
| 1201 | /* |
| 1202 | ** Return true if the string zVal represents "true" (or "false"). |
| 1203 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -1179,24 +1179,20 @@ | |
| 1179 | /* |
| 1180 | ** This function registers auxiliary functions when the SQLite |
| 1181 | ** database connection is first established. |
| 1182 | */ |
| 1183 | LOCAL void db_connection_init(void){ |
| 1184 | sqlite3_exec(g.db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); |
| 1185 | sqlite3_create_function(g.db, "user", 0, SQLITE_ANY, 0, db_sql_user, 0, 0); |
| 1186 | sqlite3_create_function(g.db, "cgi", 1, SQLITE_ANY, 0, db_sql_cgi, 0, 0); |
| 1187 | sqlite3_create_function(g.db, "cgi", 2, SQLITE_ANY, 0, db_sql_cgi, 0, 0); |
| 1188 | sqlite3_create_function(g.db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); |
| 1189 | sqlite3_create_function( |
| 1190 | g.db, "file_is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1191 | ); |
| 1192 | if( g.fSqlTrace ){ |
| 1193 | sqlite3_trace(g.db, db_sql_trace, 0); |
| 1194 | } |
| 1195 | } |
| 1196 | |
| 1197 | /* |
| 1198 | ** Return true if the string zVal represents "true" (or "false"). |
| 1199 |
+31
-11
| --- src/http.c | ||
| +++ src/http.c | ||
| @@ -38,24 +38,25 @@ | ||
| 38 | 38 | ** |
| 39 | 39 | ** Write the constructed login card into pLogin. pLogin is initialized |
| 40 | 40 | ** by this routine. |
| 41 | 41 | */ |
| 42 | 42 | static void http_build_login_card(Blob *pPayload, Blob *pLogin){ |
| 43 | - Blob nonce; /* The nonce */ | |
| 44 | - Blob pw; /* The user password */ | |
| 45 | - Blob sig; /* The signature field */ | |
| 43 | + Blob nonce; /* The nonce */ | |
| 44 | + const char *zLogin; /* The user login name */ | |
| 45 | + const char *zPw; /* The user password */ | |
| 46 | + Blob pw; /* The nonce with user password appended */ | |
| 47 | + Blob sig; /* The signature field */ | |
| 46 | 48 | |
| 47 | 49 | blob_zero(&nonce); |
| 48 | 50 | blob_zero(&pw); |
| 49 | 51 | sha1sum_blob(pPayload, &nonce); |
| 50 | 52 | blob_copy(&pw, &nonce); |
| 51 | 53 | blob_zero(pLogin); |
| 52 | 54 | if( g.urlUser==0 ){ |
| 53 | 55 | user_select(); |
| 54 | - db_blob(&pw, "SELECT pw FROM user WHERE uid=%d", g.userUid); | |
| 55 | - sha1sum_blob(&pw, &sig); | |
| 56 | - blob_appendf(pLogin, "login %F %b %b\n", g.zLogin, &nonce, &sig); | |
| 56 | + zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", g.userUid); | |
| 57 | + zLogin = g.zLogin; | |
| 57 | 58 | }else{ |
| 58 | 59 | if( g.urlPasswd==0 ){ |
| 59 | 60 | if( strcmp(g.urlUser,"anonymous")!=0 ){ |
| 60 | 61 | char *zPrompt = mprintf("password for %s: ", g.urlUser); |
| 61 | 62 | Blob x; |
| @@ -64,17 +65,36 @@ | ||
| 64 | 65 | g.urlPasswd = blob_str(&x); |
| 65 | 66 | }else{ |
| 66 | 67 | g.urlPasswd = ""; |
| 67 | 68 | } |
| 68 | 69 | } |
| 69 | - blob_append(&pw, g.urlPasswd, -1); | |
| 70 | - sha1sum_blob(&pw, &sig); | |
| 71 | - blob_appendf(pLogin, "login %F %b %b\n", g.urlUser, &nonce, &sig); | |
| 72 | - } | |
| 73 | - blob_reset(&nonce); | |
| 70 | + zPw = g.urlPasswd; | |
| 71 | + zLogin = g.urlUser; | |
| 72 | + } | |
| 73 | + | |
| 74 | + /* The login card wants the SHA1 hash of the password, so convert the | |
| 75 | + ** password to its SHA1 hash it it isn't already a SHA1 hash. | |
| 76 | + ** | |
| 77 | + ** Except, if the password begins with "*" then use the characters | |
| 78 | + ** after the "*" as a cleartext password. Put an "*" at the beginning | |
| 79 | + ** of the password to trick a newer client to use the cleartext password | |
| 80 | + ** protocol required by legacy servers. | |
| 81 | + */ | |
| 82 | + if( zPw && zPw[0] ){ | |
| 83 | + if( zPw[0]=='*' ){ | |
| 84 | + zPw++; | |
| 85 | + }else{ | |
| 86 | + zPw = sha1_shared_secret(zPw, zLogin); | |
| 87 | + } | |
| 88 | + } | |
| 89 | + | |
| 90 | + blob_append(&pw, zPw, -1); | |
| 91 | + sha1sum_blob(&pw, &sig); | |
| 92 | + blob_appendf(pLogin, "login %F %b %b\n", zLogin, &nonce, &sig); | |
| 74 | 93 | blob_reset(&pw); |
| 75 | 94 | blob_reset(&sig); |
| 95 | + blob_reset(&nonce); | |
| 76 | 96 | } |
| 77 | 97 | |
| 78 | 98 | /* |
| 79 | 99 | ** Construct an appropriate HTTP request header. Write the header |
| 80 | 100 | ** into pHdr. This routine initializes the pHdr blob. pPayload is |
| 81 | 101 |
| --- src/http.c | |
| +++ src/http.c | |
| @@ -38,24 +38,25 @@ | |
| 38 | ** |
| 39 | ** Write the constructed login card into pLogin. pLogin is initialized |
| 40 | ** by this routine. |
| 41 | */ |
| 42 | static void http_build_login_card(Blob *pPayload, Blob *pLogin){ |
| 43 | Blob nonce; /* The nonce */ |
| 44 | Blob pw; /* The user password */ |
| 45 | Blob sig; /* The signature field */ |
| 46 | |
| 47 | blob_zero(&nonce); |
| 48 | blob_zero(&pw); |
| 49 | sha1sum_blob(pPayload, &nonce); |
| 50 | blob_copy(&pw, &nonce); |
| 51 | blob_zero(pLogin); |
| 52 | if( g.urlUser==0 ){ |
| 53 | user_select(); |
| 54 | db_blob(&pw, "SELECT pw FROM user WHERE uid=%d", g.userUid); |
| 55 | sha1sum_blob(&pw, &sig); |
| 56 | blob_appendf(pLogin, "login %F %b %b\n", g.zLogin, &nonce, &sig); |
| 57 | }else{ |
| 58 | if( g.urlPasswd==0 ){ |
| 59 | if( strcmp(g.urlUser,"anonymous")!=0 ){ |
| 60 | char *zPrompt = mprintf("password for %s: ", g.urlUser); |
| 61 | Blob x; |
| @@ -64,17 +65,36 @@ | |
| 64 | g.urlPasswd = blob_str(&x); |
| 65 | }else{ |
| 66 | g.urlPasswd = ""; |
| 67 | } |
| 68 | } |
| 69 | blob_append(&pw, g.urlPasswd, -1); |
| 70 | sha1sum_blob(&pw, &sig); |
| 71 | blob_appendf(pLogin, "login %F %b %b\n", g.urlUser, &nonce, &sig); |
| 72 | } |
| 73 | blob_reset(&nonce); |
| 74 | blob_reset(&pw); |
| 75 | blob_reset(&sig); |
| 76 | } |
| 77 | |
| 78 | /* |
| 79 | ** Construct an appropriate HTTP request header. Write the header |
| 80 | ** into pHdr. This routine initializes the pHdr blob. pPayload is |
| 81 |
| --- src/http.c | |
| +++ src/http.c | |
| @@ -38,24 +38,25 @@ | |
| 38 | ** |
| 39 | ** Write the constructed login card into pLogin. pLogin is initialized |
| 40 | ** by this routine. |
| 41 | */ |
| 42 | static void http_build_login_card(Blob *pPayload, Blob *pLogin){ |
| 43 | Blob nonce; /* The nonce */ |
| 44 | const char *zLogin; /* The user login name */ |
| 45 | const char *zPw; /* The user password */ |
| 46 | Blob pw; /* The nonce with user password appended */ |
| 47 | Blob sig; /* The signature field */ |
| 48 | |
| 49 | blob_zero(&nonce); |
| 50 | blob_zero(&pw); |
| 51 | sha1sum_blob(pPayload, &nonce); |
| 52 | blob_copy(&pw, &nonce); |
| 53 | blob_zero(pLogin); |
| 54 | if( g.urlUser==0 ){ |
| 55 | user_select(); |
| 56 | zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", g.userUid); |
| 57 | zLogin = g.zLogin; |
| 58 | }else{ |
| 59 | if( g.urlPasswd==0 ){ |
| 60 | if( strcmp(g.urlUser,"anonymous")!=0 ){ |
| 61 | char *zPrompt = mprintf("password for %s: ", g.urlUser); |
| 62 | Blob x; |
| @@ -64,17 +65,36 @@ | |
| 65 | g.urlPasswd = blob_str(&x); |
| 66 | }else{ |
| 67 | g.urlPasswd = ""; |
| 68 | } |
| 69 | } |
| 70 | zPw = g.urlPasswd; |
| 71 | zLogin = g.urlUser; |
| 72 | } |
| 73 | |
| 74 | /* The login card wants the SHA1 hash of the password, so convert the |
| 75 | ** password to its SHA1 hash it it isn't already a SHA1 hash. |
| 76 | ** |
| 77 | ** Except, if the password begins with "*" then use the characters |
| 78 | ** after the "*" as a cleartext password. Put an "*" at the beginning |
| 79 | ** of the password to trick a newer client to use the cleartext password |
| 80 | ** protocol required by legacy servers. |
| 81 | */ |
| 82 | if( zPw && zPw[0] ){ |
| 83 | if( zPw[0]=='*' ){ |
| 84 | zPw++; |
| 85 | }else{ |
| 86 | zPw = sha1_shared_secret(zPw, zLogin); |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | blob_append(&pw, zPw, -1); |
| 91 | sha1sum_blob(&pw, &sig); |
| 92 | blob_appendf(pLogin, "login %F %b %b\n", zLogin, &nonce, &sig); |
| 93 | blob_reset(&pw); |
| 94 | blob_reset(&sig); |
| 95 | blob_reset(&nonce); |
| 96 | } |
| 97 | |
| 98 | /* |
| 99 | ** Construct an appropriate HTTP request header. Write the header |
| 100 | ** into pHdr. This routine initializes the pHdr blob. pPayload is |
| 101 |
+12
-4
| --- src/login.c | ||
| +++ src/login.c | ||
| @@ -140,10 +140,11 @@ | ||
| 140 | 140 | const char *zNew1, *zNew2; |
| 141 | 141 | const char *zAnonPw = 0; |
| 142 | 142 | int anonFlag; |
| 143 | 143 | char *zErrMsg = ""; |
| 144 | 144 | int uid; /* User id loged in user */ |
| 145 | + char *zSha1Pw; | |
| 145 | 146 | |
| 146 | 147 | login_check_credentials(); |
| 147 | 148 | zUsername = P("u"); |
| 148 | 149 | zPasswd = P("p"); |
| 149 | 150 | anonFlag = P("anon")!=0; |
| @@ -151,12 +152,14 @@ | ||
| 151 | 152 | const char *zCookieName = login_cookie_name(); |
| 152 | 153 | cgi_set_cookie(zCookieName, "", 0, -86400); |
| 153 | 154 | redirect_to_g(); |
| 154 | 155 | } |
| 155 | 156 | if( g.okPassword && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){ |
| 157 | + zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin); | |
| 156 | 158 | if( db_int(1, "SELECT 0 FROM user" |
| 157 | - " WHERE uid=%d AND pw=%Q", g.userUid, zPasswd) ){ | |
| 159 | + " WHERE uid=%d AND (pw=%Q OR pw=%Q)", | |
| 160 | + g.userUid, zPasswd, zSha1Pw) ){ | |
| 158 | 161 | sleep(1); |
| 159 | 162 | zErrMsg = |
| 160 | 163 | @ <p><font color="red"> |
| 161 | 164 | @ You entered an incorrect old password while attempting to change |
| 162 | 165 | @ your password. Your password is unchanged. |
| @@ -168,12 +171,13 @@ | ||
| 168 | 171 | @ The two copies of your new passwords do not match. |
| 169 | 172 | @ Your password is unchanged. |
| 170 | 173 | @ </font></p> |
| 171 | 174 | ; |
| 172 | 175 | }else{ |
| 176 | + char *zNewPw = sha1_shared_secret(zNew1, g.zLogin); | |
| 173 | 177 | db_multi_exec( |
| 174 | - "UPDATE user SET pw=%Q WHERE uid=%d", zNew1, g.userUid | |
| 178 | + "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid | |
| 175 | 179 | ); |
| 176 | 180 | redirect_to_g(); |
| 177 | 181 | return; |
| 178 | 182 | } |
| 179 | 183 | } |
| @@ -196,16 +200,17 @@ | ||
| 196 | 200 | free(zNow); |
| 197 | 201 | cgi_set_cookie(zCookieName, zCookie, 0, 6*3600); |
| 198 | 202 | redirect_to_g(); |
| 199 | 203 | } |
| 200 | 204 | if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){ |
| 205 | + zSha1Pw = sha1_shared_secret(zPasswd, zUsername); | |
| 201 | 206 | uid = db_int(0, |
| 202 | 207 | "SELECT uid FROM user" |
| 203 | 208 | " WHERE login=%Q" |
| 204 | 209 | " AND login NOT IN ('anonymous','nobody','developer','reader')" |
| 205 | - " AND pw=%Q", | |
| 206 | - zUsername, zPasswd | |
| 210 | + " AND (pw=%Q OR pw=%Q)", | |
| 211 | + zUsername, zPasswd, zSha1Pw | |
| 207 | 212 | ); |
| 208 | 213 | if( uid<=0 ){ |
| 209 | 214 | sleep(1); |
| 210 | 215 | zErrMsg = |
| 211 | 216 | @ <p><font color="red"> |
| @@ -420,10 +425,13 @@ | ||
| 420 | 425 | db_finalize(&s); |
| 421 | 426 | if( zCap==0 ){ |
| 422 | 427 | zCap = ""; |
| 423 | 428 | } |
| 424 | 429 | } |
| 430 | + if( g.fHttpTrace && g.zLogin ){ | |
| 431 | + fprintf(stderr, "# login: [%s] with capabilities [%s]\n", g.zLogin, zCap); | |
| 432 | + } | |
| 425 | 433 | |
| 426 | 434 | /* Set the global variables recording the userid and login. The |
| 427 | 435 | ** "nobody" user is a special case in that g.zLogin==0. |
| 428 | 436 | */ |
| 429 | 437 | g.userUid = uid; |
| 430 | 438 |
| --- src/login.c | |
| +++ src/login.c | |
| @@ -140,10 +140,11 @@ | |
| 140 | const char *zNew1, *zNew2; |
| 141 | const char *zAnonPw = 0; |
| 142 | int anonFlag; |
| 143 | char *zErrMsg = ""; |
| 144 | int uid; /* User id loged in user */ |
| 145 | |
| 146 | login_check_credentials(); |
| 147 | zUsername = P("u"); |
| 148 | zPasswd = P("p"); |
| 149 | anonFlag = P("anon")!=0; |
| @@ -151,12 +152,14 @@ | |
| 151 | const char *zCookieName = login_cookie_name(); |
| 152 | cgi_set_cookie(zCookieName, "", 0, -86400); |
| 153 | redirect_to_g(); |
| 154 | } |
| 155 | if( g.okPassword && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){ |
| 156 | if( db_int(1, "SELECT 0 FROM user" |
| 157 | " WHERE uid=%d AND pw=%Q", g.userUid, zPasswd) ){ |
| 158 | sleep(1); |
| 159 | zErrMsg = |
| 160 | @ <p><font color="red"> |
| 161 | @ You entered an incorrect old password while attempting to change |
| 162 | @ your password. Your password is unchanged. |
| @@ -168,12 +171,13 @@ | |
| 168 | @ The two copies of your new passwords do not match. |
| 169 | @ Your password is unchanged. |
| 170 | @ </font></p> |
| 171 | ; |
| 172 | }else{ |
| 173 | db_multi_exec( |
| 174 | "UPDATE user SET pw=%Q WHERE uid=%d", zNew1, g.userUid |
| 175 | ); |
| 176 | redirect_to_g(); |
| 177 | return; |
| 178 | } |
| 179 | } |
| @@ -196,16 +200,17 @@ | |
| 196 | free(zNow); |
| 197 | cgi_set_cookie(zCookieName, zCookie, 0, 6*3600); |
| 198 | redirect_to_g(); |
| 199 | } |
| 200 | if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){ |
| 201 | uid = db_int(0, |
| 202 | "SELECT uid FROM user" |
| 203 | " WHERE login=%Q" |
| 204 | " AND login NOT IN ('anonymous','nobody','developer','reader')" |
| 205 | " AND pw=%Q", |
| 206 | zUsername, zPasswd |
| 207 | ); |
| 208 | if( uid<=0 ){ |
| 209 | sleep(1); |
| 210 | zErrMsg = |
| 211 | @ <p><font color="red"> |
| @@ -420,10 +425,13 @@ | |
| 420 | db_finalize(&s); |
| 421 | if( zCap==0 ){ |
| 422 | zCap = ""; |
| 423 | } |
| 424 | } |
| 425 | |
| 426 | /* Set the global variables recording the userid and login. The |
| 427 | ** "nobody" user is a special case in that g.zLogin==0. |
| 428 | */ |
| 429 | g.userUid = uid; |
| 430 |
| --- src/login.c | |
| +++ src/login.c | |
| @@ -140,10 +140,11 @@ | |
| 140 | const char *zNew1, *zNew2; |
| 141 | const char *zAnonPw = 0; |
| 142 | int anonFlag; |
| 143 | char *zErrMsg = ""; |
| 144 | int uid; /* User id loged in user */ |
| 145 | char *zSha1Pw; |
| 146 | |
| 147 | login_check_credentials(); |
| 148 | zUsername = P("u"); |
| 149 | zPasswd = P("p"); |
| 150 | anonFlag = P("anon")!=0; |
| @@ -151,12 +152,14 @@ | |
| 152 | const char *zCookieName = login_cookie_name(); |
| 153 | cgi_set_cookie(zCookieName, "", 0, -86400); |
| 154 | redirect_to_g(); |
| 155 | } |
| 156 | if( g.okPassword && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){ |
| 157 | zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin); |
| 158 | if( db_int(1, "SELECT 0 FROM user" |
| 159 | " WHERE uid=%d AND (pw=%Q OR pw=%Q)", |
| 160 | g.userUid, zPasswd, zSha1Pw) ){ |
| 161 | sleep(1); |
| 162 | zErrMsg = |
| 163 | @ <p><font color="red"> |
| 164 | @ You entered an incorrect old password while attempting to change |
| 165 | @ your password. Your password is unchanged. |
| @@ -168,12 +171,13 @@ | |
| 171 | @ The two copies of your new passwords do not match. |
| 172 | @ Your password is unchanged. |
| 173 | @ </font></p> |
| 174 | ; |
| 175 | }else{ |
| 176 | char *zNewPw = sha1_shared_secret(zNew1, g.zLogin); |
| 177 | db_multi_exec( |
| 178 | "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid |
| 179 | ); |
| 180 | redirect_to_g(); |
| 181 | return; |
| 182 | } |
| 183 | } |
| @@ -196,16 +200,17 @@ | |
| 200 | free(zNow); |
| 201 | cgi_set_cookie(zCookieName, zCookie, 0, 6*3600); |
| 202 | redirect_to_g(); |
| 203 | } |
| 204 | if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){ |
| 205 | zSha1Pw = sha1_shared_secret(zPasswd, zUsername); |
| 206 | uid = db_int(0, |
| 207 | "SELECT uid FROM user" |
| 208 | " WHERE login=%Q" |
| 209 | " AND login NOT IN ('anonymous','nobody','developer','reader')" |
| 210 | " AND (pw=%Q OR pw=%Q)", |
| 211 | zUsername, zPasswd, zSha1Pw |
| 212 | ); |
| 213 | if( uid<=0 ){ |
| 214 | sleep(1); |
| 215 | zErrMsg = |
| 216 | @ <p><font color="red"> |
| @@ -420,10 +425,13 @@ | |
| 425 | db_finalize(&s); |
| 426 | if( zCap==0 ){ |
| 427 | zCap = ""; |
| 428 | } |
| 429 | } |
| 430 | if( g.fHttpTrace && g.zLogin ){ |
| 431 | fprintf(stderr, "# login: [%s] with capabilities [%s]\n", g.zLogin, zCap); |
| 432 | } |
| 433 | |
| 434 | /* Set the global variables recording the userid and login. The |
| 435 | ** "nobody" user is a special case in that g.zLogin==0. |
| 436 | */ |
| 437 | g.userUid = uid; |
| 438 |
+1
-1
| --- src/main.c | ||
| +++ src/main.c | ||
| @@ -826,11 +826,11 @@ | ||
| 826 | 826 | if( cgi_http_server(iPort, mxPort, zBrowserCmd) ){ |
| 827 | 827 | fossil_fatal("unable to listen on TCP socket %d", iPort); |
| 828 | 828 | } |
| 829 | 829 | g.httpIn = stdin; |
| 830 | 830 | g.httpOut = stdout; |
| 831 | - if( g.fHttpTrace ){ | |
| 831 | + if( g.fHttpTrace || g.fSqlTrace ){ | |
| 832 | 832 | fprintf(stderr, "====== SERVER pid %d =======\n", getpid()); |
| 833 | 833 | } |
| 834 | 834 | g.cgiPanic = 1; |
| 835 | 835 | if( g.argc==2 ){ |
| 836 | 836 | db_must_be_within_tree(); |
| 837 | 837 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -826,11 +826,11 @@ | |
| 826 | if( cgi_http_server(iPort, mxPort, zBrowserCmd) ){ |
| 827 | fossil_fatal("unable to listen on TCP socket %d", iPort); |
| 828 | } |
| 829 | g.httpIn = stdin; |
| 830 | g.httpOut = stdout; |
| 831 | if( g.fHttpTrace ){ |
| 832 | fprintf(stderr, "====== SERVER pid %d =======\n", getpid()); |
| 833 | } |
| 834 | g.cgiPanic = 1; |
| 835 | if( g.argc==2 ){ |
| 836 | db_must_be_within_tree(); |
| 837 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -826,11 +826,11 @@ | |
| 826 | if( cgi_http_server(iPort, mxPort, zBrowserCmd) ){ |
| 827 | fossil_fatal("unable to listen on TCP socket %d", iPort); |
| 828 | } |
| 829 | g.httpIn = stdin; |
| 830 | g.httpOut = stdout; |
| 831 | if( g.fHttpTrace || g.fSqlTrace ){ |
| 832 | fprintf(stderr, "====== SERVER pid %d =======\n", getpid()); |
| 833 | } |
| 834 | g.cgiPanic = 1; |
| 835 | if( g.argc==2 ){ |
| 836 | db_must_be_within_tree(); |
| 837 |
+7
| --- src/schema.c | ||
| +++ src/schema.c | ||
| @@ -95,10 +95,17 @@ | ||
| 95 | 95 | @ nonce TEXT UNIQUE, -- Nonce used for login |
| 96 | 96 | @ ipaddr TEXT -- Remote IP address. NULL for direct. |
| 97 | 97 | @ ); |
| 98 | 98 | @ |
| 99 | 99 | @ -- Information about users |
| 100 | +@ -- | |
| 101 | +@ -- The user.pw field can be either cleartext of the password, or | |
| 102 | +@ -- a SHA1 hash of the password. If the user.pw field is exactly 40 | |
| 103 | +@ -- characters long we assume it is a SHA1 hash. Otherwise, it is | |
| 104 | +@ -- cleartext. The sha1_shared_secret() routine computes the password | |
| 105 | +@ -- hash based on the project-code, the user login, and the cleartext | |
| 106 | +@ -- password. | |
| 100 | 107 | @ -- |
| 101 | 108 | @ CREATE TABLE user( |
| 102 | 109 | @ uid INTEGER PRIMARY KEY, -- User ID |
| 103 | 110 | @ login TEXT, -- login name of the user |
| 104 | 111 | @ pw TEXT, -- password |
| 105 | 112 |
| --- src/schema.c | |
| +++ src/schema.c | |
| @@ -95,10 +95,17 @@ | |
| 95 | @ nonce TEXT UNIQUE, -- Nonce used for login |
| 96 | @ ipaddr TEXT -- Remote IP address. NULL for direct. |
| 97 | @ ); |
| 98 | @ |
| 99 | @ -- Information about users |
| 100 | @ -- |
| 101 | @ CREATE TABLE user( |
| 102 | @ uid INTEGER PRIMARY KEY, -- User ID |
| 103 | @ login TEXT, -- login name of the user |
| 104 | @ pw TEXT, -- password |
| 105 |
| --- src/schema.c | |
| +++ src/schema.c | |
| @@ -95,10 +95,17 @@ | |
| 95 | @ nonce TEXT UNIQUE, -- Nonce used for login |
| 96 | @ ipaddr TEXT -- Remote IP address. NULL for direct. |
| 97 | @ ); |
| 98 | @ |
| 99 | @ -- Information about users |
| 100 | @ -- |
| 101 | @ -- The user.pw field can be either cleartext of the password, or |
| 102 | @ -- a SHA1 hash of the password. If the user.pw field is exactly 40 |
| 103 | @ -- characters long we assume it is a SHA1 hash. Otherwise, it is |
| 104 | @ -- cleartext. The sha1_shared_secret() routine computes the password |
| 105 | @ -- hash based on the project-code, the user login, and the cleartext |
| 106 | @ -- password. |
| 107 | @ -- |
| 108 | @ CREATE TABLE user( |
| 109 | @ uid INTEGER PRIMARY KEY, -- User ID |
| 110 | @ login TEXT, -- login name of the user |
| 111 | @ pw TEXT, -- password |
| 112 |
+6
-6
| --- src/setup.c | ||
| +++ src/setup.c | ||
| @@ -318,14 +318,16 @@ | ||
| 318 | 318 | if( aw ){ zCap[i++] = 'w'; } |
| 319 | 319 | if( az ){ zCap[i++] = 'z'; } |
| 320 | 320 | |
| 321 | 321 | zCap[i] = 0; |
| 322 | 322 | zPw = P("pw"); |
| 323 | - if( !isValidPwString(zPw) ){ | |
| 323 | + zLogin = P("login"); | |
| 324 | + if( isValidPwString(zPw) ){ | |
| 325 | + zPw = sha1_shared_secret(zPw, zLogin); | |
| 326 | + }else{ | |
| 324 | 327 | zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid); |
| 325 | 328 | } |
| 326 | - zLogin = P("login"); | |
| 327 | 329 | if( uid>0 && |
| 328 | 330 | db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d", zLogin, uid) |
| 329 | 331 | ){ |
| 330 | 332 | style_header("User Creation Error"); |
| 331 | 333 | @ <font color="red">Login "%h(zLogin)" is already used by a different |
| @@ -473,14 +475,12 @@ | ||
| 473 | 475 | @ <input type="checkbox" name="az"%s(oaz)/>%s(B('z'))Download Zip |
| 474 | 476 | @ </td> |
| 475 | 477 | @ </tr> |
| 476 | 478 | @ <tr> |
| 477 | 479 | @ <td align="right">Password:</td> |
| 478 | - if( strcmp(zLogin, "anonymous")==0 ){ | |
| 479 | - @ <td><input type="text" name="pw" value="%h(zPw)"></td> | |
| 480 | - }else if( zPw[0] ){ | |
| 481 | - /* Obscure the password for all other users */ | |
| 480 | + if( zPw[0] ){ | |
| 481 | + /* Obscure the password for all users */ | |
| 482 | 482 | @ <td><input type="password" name="pw" value="**********"></td> |
| 483 | 483 | }else{ |
| 484 | 484 | /* Show an empty password as an empty input field */ |
| 485 | 485 | @ <td><input type="password" name="pw" value=""></td> |
| 486 | 486 | } |
| 487 | 487 |
| --- src/setup.c | |
| +++ src/setup.c | |
| @@ -318,14 +318,16 @@ | |
| 318 | if( aw ){ zCap[i++] = 'w'; } |
| 319 | if( az ){ zCap[i++] = 'z'; } |
| 320 | |
| 321 | zCap[i] = 0; |
| 322 | zPw = P("pw"); |
| 323 | if( !isValidPwString(zPw) ){ |
| 324 | zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid); |
| 325 | } |
| 326 | zLogin = P("login"); |
| 327 | if( uid>0 && |
| 328 | db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d", zLogin, uid) |
| 329 | ){ |
| 330 | style_header("User Creation Error"); |
| 331 | @ <font color="red">Login "%h(zLogin)" is already used by a different |
| @@ -473,14 +475,12 @@ | |
| 473 | @ <input type="checkbox" name="az"%s(oaz)/>%s(B('z'))Download Zip |
| 474 | @ </td> |
| 475 | @ </tr> |
| 476 | @ <tr> |
| 477 | @ <td align="right">Password:</td> |
| 478 | if( strcmp(zLogin, "anonymous")==0 ){ |
| 479 | @ <td><input type="text" name="pw" value="%h(zPw)"></td> |
| 480 | }else if( zPw[0] ){ |
| 481 | /* Obscure the password for all other users */ |
| 482 | @ <td><input type="password" name="pw" value="**********"></td> |
| 483 | }else{ |
| 484 | /* Show an empty password as an empty input field */ |
| 485 | @ <td><input type="password" name="pw" value=""></td> |
| 486 | } |
| 487 |
| --- src/setup.c | |
| +++ src/setup.c | |
| @@ -318,14 +318,16 @@ | |
| 318 | if( aw ){ zCap[i++] = 'w'; } |
| 319 | if( az ){ zCap[i++] = 'z'; } |
| 320 | |
| 321 | zCap[i] = 0; |
| 322 | zPw = P("pw"); |
| 323 | zLogin = P("login"); |
| 324 | if( isValidPwString(zPw) ){ |
| 325 | zPw = sha1_shared_secret(zPw, zLogin); |
| 326 | }else{ |
| 327 | zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid); |
| 328 | } |
| 329 | if( uid>0 && |
| 330 | db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d", zLogin, uid) |
| 331 | ){ |
| 332 | style_header("User Creation Error"); |
| 333 | @ <font color="red">Login "%h(zLogin)" is already used by a different |
| @@ -473,14 +475,12 @@ | |
| 475 | @ <input type="checkbox" name="az"%s(oaz)/>%s(B('z'))Download Zip |
| 476 | @ </td> |
| 477 | @ </tr> |
| 478 | @ <tr> |
| 479 | @ <td align="right">Password:</td> |
| 480 | if( zPw[0] ){ |
| 481 | /* Obscure the password for all users */ |
| 482 | @ <td><input type="password" name="pw" value="**********"></td> |
| 483 | }else{ |
| 484 | /* Show an empty password as an empty input field */ |
| 485 | @ <td><input type="password" name="pw" value=""></td> |
| 486 | } |
| 487 |
+59
| --- src/sha1.c | ||
| +++ src/sha1.c | ||
| @@ -534,10 +534,69 @@ | ||
| 534 | 534 | SHA1Result(&ctx, zResult); |
| 535 | 535 | DigestToBase16(zResult, blob_buffer(pCksum)); |
| 536 | 536 | return 0; |
| 537 | 537 | } |
| 538 | 538 | |
| 539 | +/* | |
| 540 | +** Compute the SHA1 checksum of a zero-terminated string. The | |
| 541 | +** result is held in memory obtained from mprintf(). | |
| 542 | +*/ | |
| 543 | +char *sha1sum(const char *zIn){ | |
| 544 | + SHA1Context ctx; | |
| 545 | + unsigned char zResult[20]; | |
| 546 | + char zDigest[41]; | |
| 547 | + | |
| 548 | + SHA1Reset(&ctx); | |
| 549 | + SHA1Input(&ctx, (unsigned const char*)zIn, strlen(zIn)); | |
| 550 | + SHA1Result(&ctx, zResult); | |
| 551 | + DigestToBase16(zResult, zDigest); | |
| 552 | + return mprintf("%s", zDigest); | |
| 553 | +} | |
| 554 | + | |
| 555 | +/* | |
| 556 | +** Convert a cleartext password for a specific user into a SHA1 hash. | |
| 557 | +** | |
| 558 | +** The algorithm here is: | |
| 559 | +** | |
| 560 | +** SHA1( project-code + "/" + login + "/" + password ) | |
| 561 | +** | |
| 562 | +** In words: The users login name and password are appended to the | |
| 563 | +** project ID code and the SHA1 hash of the result is computed. | |
| 564 | +** | |
| 565 | +** The result of this function is the shared secret used by a client | |
| 566 | +** to authenticate to a server for the sync protocol. It is also the | |
| 567 | +** value stored in the USER.PW field of the database. By mixing in the | |
| 568 | +** login name and the project id with the hash, different shared secrets | |
| 569 | +** are obtained even if two users select the same password, or if a | |
| 570 | +** single user selects the same password for multiple projects. | |
| 571 | +*/ | |
| 572 | +char *sha1_shared_secret(const char *zPw, const char *zLogin){ | |
| 573 | + static char *zProjectId = 0; | |
| 574 | + SHA1Context ctx; | |
| 575 | + unsigned char zResult[20]; | |
| 576 | + char zDigest[41]; | |
| 577 | + | |
| 578 | + SHA1Reset(&ctx); | |
| 579 | + if( zProjectId==0 ){ | |
| 580 | + zProjectId = db_get("project-code", 0); | |
| 581 | + | |
| 582 | + /* On the first xfer request of a clone, the project-code is not yet | |
| 583 | + ** known. Use the cleartext password, since that is all we have. | |
| 584 | + */ | |
| 585 | + if( zProjectId==0 ){ | |
| 586 | + return mprintf("%s", zPw); | |
| 587 | + } | |
| 588 | + } | |
| 589 | + SHA1Input(&ctx, (unsigned char*)zProjectId, strlen(zProjectId)); | |
| 590 | + SHA1Input(&ctx, (unsigned char*)"/", 1); | |
| 591 | + SHA1Input(&ctx, (unsigned char*)zLogin, strlen(zLogin)); | |
| 592 | + SHA1Input(&ctx, (unsigned char*)"/", 1); | |
| 593 | + SHA1Input(&ctx, (unsigned const char*)zPw, strlen(zPw)); | |
| 594 | + SHA1Result(&ctx, zResult); | |
| 595 | + DigestToBase16(zResult, zDigest); | |
| 596 | + return mprintf("%s", zDigest); | |
| 597 | +} | |
| 539 | 598 | |
| 540 | 599 | /* |
| 541 | 600 | ** COMMAND: sha1sum |
| 542 | 601 | ** %fossil sha1sum FILE... |
| 543 | 602 | ** |
| 544 | 603 |
| --- src/sha1.c | |
| +++ src/sha1.c | |
| @@ -534,10 +534,69 @@ | |
| 534 | SHA1Result(&ctx, zResult); |
| 535 | DigestToBase16(zResult, blob_buffer(pCksum)); |
| 536 | return 0; |
| 537 | } |
| 538 | |
| 539 | |
| 540 | /* |
| 541 | ** COMMAND: sha1sum |
| 542 | ** %fossil sha1sum FILE... |
| 543 | ** |
| 544 |
| --- src/sha1.c | |
| +++ src/sha1.c | |
| @@ -534,10 +534,69 @@ | |
| 534 | SHA1Result(&ctx, zResult); |
| 535 | DigestToBase16(zResult, blob_buffer(pCksum)); |
| 536 | return 0; |
| 537 | } |
| 538 | |
| 539 | /* |
| 540 | ** Compute the SHA1 checksum of a zero-terminated string. The |
| 541 | ** result is held in memory obtained from mprintf(). |
| 542 | */ |
| 543 | char *sha1sum(const char *zIn){ |
| 544 | SHA1Context ctx; |
| 545 | unsigned char zResult[20]; |
| 546 | char zDigest[41]; |
| 547 | |
| 548 | SHA1Reset(&ctx); |
| 549 | SHA1Input(&ctx, (unsigned const char*)zIn, strlen(zIn)); |
| 550 | SHA1Result(&ctx, zResult); |
| 551 | DigestToBase16(zResult, zDigest); |
| 552 | return mprintf("%s", zDigest); |
| 553 | } |
| 554 | |
| 555 | /* |
| 556 | ** Convert a cleartext password for a specific user into a SHA1 hash. |
| 557 | ** |
| 558 | ** The algorithm here is: |
| 559 | ** |
| 560 | ** SHA1( project-code + "/" + login + "/" + password ) |
| 561 | ** |
| 562 | ** In words: The users login name and password are appended to the |
| 563 | ** project ID code and the SHA1 hash of the result is computed. |
| 564 | ** |
| 565 | ** The result of this function is the shared secret used by a client |
| 566 | ** to authenticate to a server for the sync protocol. It is also the |
| 567 | ** value stored in the USER.PW field of the database. By mixing in the |
| 568 | ** login name and the project id with the hash, different shared secrets |
| 569 | ** are obtained even if two users select the same password, or if a |
| 570 | ** single user selects the same password for multiple projects. |
| 571 | */ |
| 572 | char *sha1_shared_secret(const char *zPw, const char *zLogin){ |
| 573 | static char *zProjectId = 0; |
| 574 | SHA1Context ctx; |
| 575 | unsigned char zResult[20]; |
| 576 | char zDigest[41]; |
| 577 | |
| 578 | SHA1Reset(&ctx); |
| 579 | if( zProjectId==0 ){ |
| 580 | zProjectId = db_get("project-code", 0); |
| 581 | |
| 582 | /* On the first xfer request of a clone, the project-code is not yet |
| 583 | ** known. Use the cleartext password, since that is all we have. |
| 584 | */ |
| 585 | if( zProjectId==0 ){ |
| 586 | return mprintf("%s", zPw); |
| 587 | } |
| 588 | } |
| 589 | SHA1Input(&ctx, (unsigned char*)zProjectId, strlen(zProjectId)); |
| 590 | SHA1Input(&ctx, (unsigned char*)"/", 1); |
| 591 | SHA1Input(&ctx, (unsigned char*)zLogin, strlen(zLogin)); |
| 592 | SHA1Input(&ctx, (unsigned char*)"/", 1); |
| 593 | SHA1Input(&ctx, (unsigned const char*)zPw, strlen(zPw)); |
| 594 | SHA1Result(&ctx, zResult); |
| 595 | DigestToBase16(zResult, zDigest); |
| 596 | return mprintf("%s", zDigest); |
| 597 | } |
| 598 | |
| 599 | /* |
| 600 | ** COMMAND: sha1sum |
| 601 | ** %fossil sha1sum FILE... |
| 602 | ** |
| 603 |
+46
-3
| --- src/user.c | ||
| +++ src/user.c | ||
| @@ -184,10 +184,11 @@ | ||
| 184 | 184 | usage("capabilities|default|list|new|password ..."); |
| 185 | 185 | } |
| 186 | 186 | n = strlen(g.argv[2]); |
| 187 | 187 | if( n>=2 && strncmp(g.argv[2],"new",n)==0 ){ |
| 188 | 188 | Blob passwd, login, contact; |
| 189 | + char *zPw; | |
| 189 | 190 | |
| 190 | 191 | if( g.argc>=4 ){ |
| 191 | 192 | blob_init(&login, g.argv[3], -1); |
| 192 | 193 | }else{ |
| 193 | 194 | prompt_user("login: ", &login); |
| @@ -203,15 +204,17 @@ | ||
| 203 | 204 | if( g.argc>=6 ){ |
| 204 | 205 | blob_init(&passwd, g.argv[5], -1); |
| 205 | 206 | }else{ |
| 206 | 207 | prompt_for_password("password: ", &passwd, 1); |
| 207 | 208 | } |
| 209 | + zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login)); | |
| 208 | 210 | db_multi_exec( |
| 209 | 211 | "INSERT INTO user(login,pw,cap,info)" |
| 210 | - "VALUES(%B,%B,'v',%B)", | |
| 211 | - &login, &passwd, &contact | |
| 212 | + "VALUES(%B,%Q,'v',%B)", | |
| 213 | + &login, zPw, &contact | |
| 212 | 214 | ); |
| 215 | + free(zPw); | |
| 213 | 216 | }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){ |
| 214 | 217 | user_select(); |
| 215 | 218 | if( g.argc==3 ){ |
| 216 | 219 | printf("%s\n", g.zLogin); |
| 217 | 220 | }else{ |
| @@ -247,11 +250,13 @@ | ||
| 247 | 250 | prompt_for_password(zPrompt, &pw, 1); |
| 248 | 251 | } |
| 249 | 252 | if( blob_size(&pw)==0 ){ |
| 250 | 253 | printf("password unchanged\n"); |
| 251 | 254 | }else{ |
| 252 | - db_multi_exec("UPDATE user SET pw=%B WHERE uid=%d", &pw, uid); | |
| 255 | + char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3]); | |
| 256 | + db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid); | |
| 257 | + free(zSecret); | |
| 253 | 258 | } |
| 254 | 259 | }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ |
| 255 | 260 | int uid; |
| 256 | 261 | if( g.argc!=4 && g.argc!=5 ){ |
| 257 | 262 | usage("user capabilities USERNAME ?PERMISSIONS?"); |
| @@ -344,5 +349,43 @@ | ||
| 344 | 349 | ); |
| 345 | 350 | g.userUid = db_last_insert_rowid(); |
| 346 | 351 | g.zLogin = "anonymous"; |
| 347 | 352 | } |
| 348 | 353 | } |
| 354 | + | |
| 355 | +/* | |
| 356 | +** Compute the shared secret for a user. | |
| 357 | +*/ | |
| 358 | +static void user_sha1_shared_secret_func( | |
| 359 | + sqlite3_context *context, | |
| 360 | + int argc, | |
| 361 | + sqlite3_value **argv | |
| 362 | +){ | |
| 363 | + char *zPw; | |
| 364 | + char *zLogin; | |
| 365 | + assert( argc==2 ); | |
| 366 | + zPw = (char*)sqlite3_value_text(argv[0]); | |
| 367 | + zLogin = (char*)sqlite3_value_text(argv[1]); | |
| 368 | + if( zPw && zLogin ){ | |
| 369 | + sqlite3_result_text(context, sha1_shared_secret(zPw, zLogin), -1, free); | |
| 370 | + } | |
| 371 | +} | |
| 372 | + | |
| 373 | +/* | |
| 374 | +** COMMAND: test-hash-passwords | |
| 375 | +** | |
| 376 | +** Usage: %fossil test-hash-passwords REPOSITORY | |
| 377 | +** | |
| 378 | +** Convert all local password storage to use a SHA1 hash of the password | |
| 379 | +** rather than cleartext. Passwords that are already stored as the SHA1 | |
| 380 | +** has are unchanged. | |
| 381 | +*/ | |
| 382 | +void user_hash_passwords_cmd(void){ | |
| 383 | + if( g.argc!=3 ) usage("REPOSITORY"); | |
| 384 | + db_open_repository(g.argv[2]); | |
| 385 | + sqlite3_create_function(g.db, "sha1_shared_secret", 2, SQLITE_UTF8, 0, | |
| 386 | + user_sha1_shared_secret_func, 0, 0); | |
| 387 | + db_multi_exec( | |
| 388 | + "UPDATE user SET pw=sha1_shared_secret(pw,login)" | |
| 389 | + " WHERE length(pw)>0 AND length(pw)!=40" | |
| 390 | + ); | |
| 391 | +} | |
| 349 | 392 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -184,10 +184,11 @@ | |
| 184 | usage("capabilities|default|list|new|password ..."); |
| 185 | } |
| 186 | n = strlen(g.argv[2]); |
| 187 | if( n>=2 && strncmp(g.argv[2],"new",n)==0 ){ |
| 188 | Blob passwd, login, contact; |
| 189 | |
| 190 | if( g.argc>=4 ){ |
| 191 | blob_init(&login, g.argv[3], -1); |
| 192 | }else{ |
| 193 | prompt_user("login: ", &login); |
| @@ -203,15 +204,17 @@ | |
| 203 | if( g.argc>=6 ){ |
| 204 | blob_init(&passwd, g.argv[5], -1); |
| 205 | }else{ |
| 206 | prompt_for_password("password: ", &passwd, 1); |
| 207 | } |
| 208 | db_multi_exec( |
| 209 | "INSERT INTO user(login,pw,cap,info)" |
| 210 | "VALUES(%B,%B,'v',%B)", |
| 211 | &login, &passwd, &contact |
| 212 | ); |
| 213 | }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){ |
| 214 | user_select(); |
| 215 | if( g.argc==3 ){ |
| 216 | printf("%s\n", g.zLogin); |
| 217 | }else{ |
| @@ -247,11 +250,13 @@ | |
| 247 | prompt_for_password(zPrompt, &pw, 1); |
| 248 | } |
| 249 | if( blob_size(&pw)==0 ){ |
| 250 | printf("password unchanged\n"); |
| 251 | }else{ |
| 252 | db_multi_exec("UPDATE user SET pw=%B WHERE uid=%d", &pw, uid); |
| 253 | } |
| 254 | }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ |
| 255 | int uid; |
| 256 | if( g.argc!=4 && g.argc!=5 ){ |
| 257 | usage("user capabilities USERNAME ?PERMISSIONS?"); |
| @@ -344,5 +349,43 @@ | |
| 344 | ); |
| 345 | g.userUid = db_last_insert_rowid(); |
| 346 | g.zLogin = "anonymous"; |
| 347 | } |
| 348 | } |
| 349 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -184,10 +184,11 @@ | |
| 184 | usage("capabilities|default|list|new|password ..."); |
| 185 | } |
| 186 | n = strlen(g.argv[2]); |
| 187 | if( n>=2 && strncmp(g.argv[2],"new",n)==0 ){ |
| 188 | Blob passwd, login, contact; |
| 189 | char *zPw; |
| 190 | |
| 191 | if( g.argc>=4 ){ |
| 192 | blob_init(&login, g.argv[3], -1); |
| 193 | }else{ |
| 194 | prompt_user("login: ", &login); |
| @@ -203,15 +204,17 @@ | |
| 204 | if( g.argc>=6 ){ |
| 205 | blob_init(&passwd, g.argv[5], -1); |
| 206 | }else{ |
| 207 | prompt_for_password("password: ", &passwd, 1); |
| 208 | } |
| 209 | zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login)); |
| 210 | db_multi_exec( |
| 211 | "INSERT INTO user(login,pw,cap,info)" |
| 212 | "VALUES(%B,%Q,'v',%B)", |
| 213 | &login, zPw, &contact |
| 214 | ); |
| 215 | free(zPw); |
| 216 | }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){ |
| 217 | user_select(); |
| 218 | if( g.argc==3 ){ |
| 219 | printf("%s\n", g.zLogin); |
| 220 | }else{ |
| @@ -247,11 +250,13 @@ | |
| 250 | prompt_for_password(zPrompt, &pw, 1); |
| 251 | } |
| 252 | if( blob_size(&pw)==0 ){ |
| 253 | printf("password unchanged\n"); |
| 254 | }else{ |
| 255 | char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3]); |
| 256 | db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid); |
| 257 | free(zSecret); |
| 258 | } |
| 259 | }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ |
| 260 | int uid; |
| 261 | if( g.argc!=4 && g.argc!=5 ){ |
| 262 | usage("user capabilities USERNAME ?PERMISSIONS?"); |
| @@ -344,5 +349,43 @@ | |
| 349 | ); |
| 350 | g.userUid = db_last_insert_rowid(); |
| 351 | g.zLogin = "anonymous"; |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | /* |
| 356 | ** Compute the shared secret for a user. |
| 357 | */ |
| 358 | static void user_sha1_shared_secret_func( |
| 359 | sqlite3_context *context, |
| 360 | int argc, |
| 361 | sqlite3_value **argv |
| 362 | ){ |
| 363 | char *zPw; |
| 364 | char *zLogin; |
| 365 | assert( argc==2 ); |
| 366 | zPw = (char*)sqlite3_value_text(argv[0]); |
| 367 | zLogin = (char*)sqlite3_value_text(argv[1]); |
| 368 | if( zPw && zLogin ){ |
| 369 | sqlite3_result_text(context, sha1_shared_secret(zPw, zLogin), -1, free); |
| 370 | } |
| 371 | } |
| 372 | |
| 373 | /* |
| 374 | ** COMMAND: test-hash-passwords |
| 375 | ** |
| 376 | ** Usage: %fossil test-hash-passwords REPOSITORY |
| 377 | ** |
| 378 | ** Convert all local password storage to use a SHA1 hash of the password |
| 379 | ** rather than cleartext. Passwords that are already stored as the SHA1 |
| 380 | ** has are unchanged. |
| 381 | */ |
| 382 | void user_hash_passwords_cmd(void){ |
| 383 | if( g.argc!=3 ) usage("REPOSITORY"); |
| 384 | db_open_repository(g.argv[2]); |
| 385 | sqlite3_create_function(g.db, "sha1_shared_secret", 2, SQLITE_UTF8, 0, |
| 386 | user_sha1_shared_secret_func, 0, 0); |
| 387 | db_multi_exec( |
| 388 | "UPDATE user SET pw=sha1_shared_secret(pw,login)" |
| 389 | " WHERE length(pw)>0 AND length(pw)!=40" |
| 390 | ); |
| 391 | } |
| 392 |
+53
-36
| --- src/xfer.c | ||
| +++ src/xfer.c | ||
| @@ -357,11 +357,10 @@ | ||
| 357 | 357 | blob_reset(&h2); |
| 358 | 358 | blob_reset(&tail); |
| 359 | 359 | return rc==0; |
| 360 | 360 | } |
| 361 | 361 | |
| 362 | - | |
| 363 | 362 | /* |
| 364 | 363 | ** Check the signature on an application/x-fossil payload received by |
| 365 | 364 | ** the HTTP server. The signature is a line of the following form: |
| 366 | 365 | ** |
| 367 | 366 | ** login LOGIN NONCE SIGNATURE |
| @@ -394,28 +393,49 @@ | ||
| 394 | 393 | " AND login NOT IN ('anonymous','nobody','developer','reader')" |
| 395 | 394 | " AND length(pw)>0", |
| 396 | 395 | zLogin |
| 397 | 396 | ); |
| 398 | 397 | if( db_step(&q)==SQLITE_ROW ){ |
| 398 | + int szPw; | |
| 399 | 399 | Blob pw, combined, hash; |
| 400 | 400 | blob_zero(&pw); |
| 401 | 401 | db_ephemeral_blob(&q, 0, &pw); |
| 402 | + szPw = blob_size(&pw); | |
| 402 | 403 | blob_zero(&combined); |
| 403 | 404 | blob_copy(&combined, pNonce); |
| 404 | - blob_append(&combined, blob_buffer(&pw), blob_size(&pw)); | |
| 405 | - /* CGIDEBUG(("presig=[%s]\n", blob_str(&combined))); */ | |
| 405 | + blob_append(&combined, blob_buffer(&pw), szPw); | |
| 406 | 406 | sha1sum_blob(&combined, &hash); |
| 407 | + assert( blob_size(&hash)==40 ); | |
| 407 | 408 | rc = blob_compare(&hash, pSig); |
| 408 | 409 | blob_reset(&hash); |
| 409 | 410 | blob_reset(&combined); |
| 411 | + if( rc!=0 && szPw!=40 ){ | |
| 412 | + /* If this server stores cleartext passwords and the password did not | |
| 413 | + ** match, then perhaps the client is sending SHA1 passwords. Try | |
| 414 | + ** again with the SHA1 password. | |
| 415 | + */ | |
| 416 | + const char *zPw = db_column_text(&q, 0); | |
| 417 | + char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin)); | |
| 418 | + blob_zero(&combined); | |
| 419 | + blob_copy(&combined, pNonce); | |
| 420 | + blob_append(&combined, zSecret, -1); | |
| 421 | + free(zSecret); | |
| 422 | + sha1sum_blob(&combined, &hash); | |
| 423 | + rc = blob_compare(&hash, pSig); | |
| 424 | + blob_reset(&hash); | |
| 425 | + blob_reset(&combined); | |
| 426 | + } | |
| 410 | 427 | if( rc==0 ){ |
| 411 | 428 | const char *zCap; |
| 412 | 429 | zCap = db_column_text(&q, 1); |
| 413 | 430 | login_set_capabilities(zCap); |
| 414 | 431 | g.userUid = db_column_int(&q, 2); |
| 415 | 432 | g.zLogin = mprintf("%b", pLogin); |
| 416 | 433 | g.zNonce = mprintf("%b", pNonce); |
| 434 | + if( g.fHttpTrace ){ | |
| 435 | + fprintf(stderr, "# login [%s] with capabilities [%s]\n", g.zLogin,zCap); | |
| 436 | + } | |
| 417 | 437 | } |
| 418 | 438 | } |
| 419 | 439 | db_finalize(&q); |
| 420 | 440 | |
| 421 | 441 | if( rc==0 ){ |
| @@ -650,41 +670,18 @@ | ||
| 650 | 670 | |
| 651 | 671 | /* pull SERVERCODE PROJECTCODE |
| 652 | 672 | ** push SERVERCODE PROJECTCODE |
| 653 | 673 | ** |
| 654 | 674 | ** The client wants either send or receive. The server should |
| 655 | - ** verify that the project code matches and that the server code | |
| 656 | - ** does not match. | |
| 675 | + ** verify that the project code matches. | |
| 657 | 676 | */ |
| 658 | 677 | if( xfer.nToken==3 |
| 659 | 678 | && (blob_eq(&xfer.aToken[0], "pull") || blob_eq(&xfer.aToken[0], "push")) |
| 660 | 679 | && blob_is_uuid(&xfer.aToken[1]) |
| 661 | 680 | && blob_is_uuid(&xfer.aToken[2]) |
| 662 | 681 | ){ |
| 663 | 682 | const char *zPCode; |
| 664 | - | |
| 665 | -#if 0 | |
| 666 | - /* This block checks to see if a server is trying to sync with itself. | |
| 667 | - ** This used to be disallowed, but I cannot think of any significant | |
| 668 | - ** harm, so I have disabled the check. | |
| 669 | - ** | |
| 670 | - ** With this check disabled, it is sufficient to copy the repository | |
| 671 | - ** database. No need to run clone. | |
| 672 | - */ | |
| 673 | - const char *zSCode; | |
| 674 | - zSCode = db_get("server-code", 0); | |
| 675 | - if( zSCode==0 ){ | |
| 676 | - fossil_panic("missing server code"); | |
| 677 | - } | |
| 678 | - if( blob_eq_str(&xfer.aToken[1], zSCode, -1) ){ | |
| 679 | - cgi_reset_content(); | |
| 680 | - @ error server\sloop | |
| 681 | - nErr++; | |
| 682 | - break; | |
| 683 | - } | |
| 684 | -#endif | |
| 685 | - | |
| 686 | 683 | zPCode = db_get("project-code", 0); |
| 687 | 684 | if( zPCode==0 ){ |
| 688 | 685 | fossil_panic("missing project code"); |
| 689 | 686 | } |
| 690 | 687 | if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){ |
| @@ -723,10 +720,11 @@ | ||
| 723 | 720 | */ |
| 724 | 721 | if( blob_eq(&xfer.aToken[0], "clone") ){ |
| 725 | 722 | login_check_credentials(); |
| 726 | 723 | if( !g.okClone ){ |
| 727 | 724 | cgi_reset_content(); |
| 725 | + @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x")) | |
| 728 | 726 | @ error not\sauthorized\sto\sclone |
| 729 | 727 | nErr++; |
| 730 | 728 | break; |
| 731 | 729 | } |
| 732 | 730 | isClone = 1; |
| @@ -960,11 +958,11 @@ | ||
| 960 | 958 | blobarray_zero(xfer.aToken, count(xfer.aToken)); |
| 961 | 959 | blob_zero(&send); |
| 962 | 960 | blob_zero(&recv); |
| 963 | 961 | blob_zero(&xfer.err); |
| 964 | 962 | blob_zero(&xfer.line); |
| 965 | - origConfigRcvMask = configRcvMask; | |
| 963 | + origConfigRcvMask = 0; | |
| 966 | 964 | |
| 967 | 965 | /* |
| 968 | 966 | ** Always begin with a clone, pull, or push message |
| 969 | 967 | */ |
| 970 | 968 | if( cloneFlag ){ |
| @@ -1005,12 +1003,15 @@ | ||
| 1005 | 1003 | if( pushFlag ){ |
| 1006 | 1004 | send_unsent(&xfer); |
| 1007 | 1005 | nCardSent += send_unclustered(&xfer); |
| 1008 | 1006 | } |
| 1009 | 1007 | |
| 1010 | - /* Send configuration parameter requests */ | |
| 1011 | - if( configRcvMask ){ | |
| 1008 | + /* Send configuration parameter requests. On a clone, delay sending | |
| 1009 | + ** this until the second cycle since the login card might fail on | |
| 1010 | + ** the first cycle. | |
| 1011 | + */ | |
| 1012 | + if( configRcvMask && (cloneFlag==0 || nCycle>0) ){ | |
| 1012 | 1013 | const char *zName; |
| 1013 | 1014 | zName = configure_first_name(configRcvMask); |
| 1014 | 1015 | while( zName ){ |
| 1015 | 1016 | blob_appendf(&send, "reqconfig %s\n", zName); |
| 1016 | 1017 | zName = configure_next_name(configRcvMask); |
| @@ -1017,10 +1018,11 @@ | ||
| 1017 | 1018 | nCardSent++; |
| 1018 | 1019 | } |
| 1019 | 1020 | if( configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT) ){ |
| 1020 | 1021 | configure_prepare_to_receive(0); |
| 1021 | 1022 | } |
| 1023 | + origConfigRcvMask = configRcvMask; | |
| 1022 | 1024 | configRcvMask = 0; |
| 1023 | 1025 | } |
| 1024 | 1026 | |
| 1025 | 1027 | /* Send configuration parameters being pushed */ |
| 1026 | 1028 | if( configSendMask ){ |
| @@ -1032,11 +1034,14 @@ | ||
| 1032 | 1034 | nCardSent++; |
| 1033 | 1035 | } |
| 1034 | 1036 | configSendMask = 0; |
| 1035 | 1037 | } |
| 1036 | 1038 | |
| 1037 | - /* Append randomness to the end of the message */ | |
| 1039 | + /* Append randomness to the end of the message. This makes all | |
| 1040 | + ** messages unique so that that the login-card nonce will always | |
| 1041 | + ** be unique. | |
| 1042 | + */ | |
| 1038 | 1043 | zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
| 1039 | 1044 | blob_appendf(&send, "# %s\n", zRandomness); |
| 1040 | 1045 | free(zRandomness); |
| 1041 | 1046 | |
| 1042 | 1047 | /* Exchange messages with the server */ |
| @@ -1213,17 +1218,26 @@ | ||
| 1213 | 1218 | printf("\rServer says: %s\n", zMsg); |
| 1214 | 1219 | }else |
| 1215 | 1220 | |
| 1216 | 1221 | /* error MESSAGE |
| 1217 | 1222 | ** |
| 1218 | - ** Report an error and abandon the sync session | |
| 1223 | + ** Report an error and abandon the sync session. | |
| 1224 | + ** | |
| 1225 | + ** Except, when cloning we will sometimes get an error on the | |
| 1226 | + ** first message exchange because the project-code is unknown | |
| 1227 | + ** and so the login card on the request was invalid. The project-code | |
| 1228 | + ** is returned in the reply before the error card, so second and | |
| 1229 | + ** subsequent messages should be OK. Nevertheless, we need to ignore | |
| 1230 | + ** the error card on the first message of a clone. | |
| 1219 | 1231 | */ |
| 1220 | 1232 | if( blob_eq(&xfer.aToken[0],"error") && xfer.nToken==2 ){ |
| 1221 | - char *zMsg = blob_terminate(&xfer.aToken[1]); | |
| 1222 | - defossilize(zMsg); | |
| 1223 | - blob_appendf(&xfer.err, "server says: %s", zMsg); | |
| 1224 | - printf("Server Error: %s\n", zMsg); | |
| 1233 | + if( !cloneFlag || nCycle>0 ){ | |
| 1234 | + char *zMsg = blob_terminate(&xfer.aToken[1]); | |
| 1235 | + defossilize(zMsg); | |
| 1236 | + blob_appendf(&xfer.err, "server says: %s", zMsg); | |
| 1237 | + printf("Server Error: %s\n", zMsg); | |
| 1238 | + } | |
| 1225 | 1239 | }else |
| 1226 | 1240 | |
| 1227 | 1241 | /* Unknown message */ |
| 1228 | 1242 | { |
| 1229 | 1243 | if( blob_str(&xfer.aToken[0])[0]=='<' ){ |
| @@ -1272,10 +1286,13 @@ | ||
| 1272 | 1286 | ** another round |
| 1273 | 1287 | */ |
| 1274 | 1288 | if( xfer.nFileSent+xfer.nDeltaSent>0 ){ |
| 1275 | 1289 | go = 1; |
| 1276 | 1290 | } |
| 1291 | + | |
| 1292 | + /* If this is a clone, the go at least two rounds */ | |
| 1293 | + if( cloneFlag && nCycle==1 ) go = 1; | |
| 1277 | 1294 | }; |
| 1278 | 1295 | transport_stats(&nSent, &nRcvd, 1); |
| 1279 | 1296 | printf("Total network traffic: %d bytes sent, %d bytes received\n", |
| 1280 | 1297 | nSent, nRcvd); |
| 1281 | 1298 | transport_close(); |
| 1282 | 1299 | |
| 1283 | 1300 | ADDED www/password.wiki |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -357,11 +357,10 @@ | |
| 357 | blob_reset(&h2); |
| 358 | blob_reset(&tail); |
| 359 | return rc==0; |
| 360 | } |
| 361 | |
| 362 | |
| 363 | /* |
| 364 | ** Check the signature on an application/x-fossil payload received by |
| 365 | ** the HTTP server. The signature is a line of the following form: |
| 366 | ** |
| 367 | ** login LOGIN NONCE SIGNATURE |
| @@ -394,28 +393,49 @@ | |
| 394 | " AND login NOT IN ('anonymous','nobody','developer','reader')" |
| 395 | " AND length(pw)>0", |
| 396 | zLogin |
| 397 | ); |
| 398 | if( db_step(&q)==SQLITE_ROW ){ |
| 399 | Blob pw, combined, hash; |
| 400 | blob_zero(&pw); |
| 401 | db_ephemeral_blob(&q, 0, &pw); |
| 402 | blob_zero(&combined); |
| 403 | blob_copy(&combined, pNonce); |
| 404 | blob_append(&combined, blob_buffer(&pw), blob_size(&pw)); |
| 405 | /* CGIDEBUG(("presig=[%s]\n", blob_str(&combined))); */ |
| 406 | sha1sum_blob(&combined, &hash); |
| 407 | rc = blob_compare(&hash, pSig); |
| 408 | blob_reset(&hash); |
| 409 | blob_reset(&combined); |
| 410 | if( rc==0 ){ |
| 411 | const char *zCap; |
| 412 | zCap = db_column_text(&q, 1); |
| 413 | login_set_capabilities(zCap); |
| 414 | g.userUid = db_column_int(&q, 2); |
| 415 | g.zLogin = mprintf("%b", pLogin); |
| 416 | g.zNonce = mprintf("%b", pNonce); |
| 417 | } |
| 418 | } |
| 419 | db_finalize(&q); |
| 420 | |
| 421 | if( rc==0 ){ |
| @@ -650,41 +670,18 @@ | |
| 650 | |
| 651 | /* pull SERVERCODE PROJECTCODE |
| 652 | ** push SERVERCODE PROJECTCODE |
| 653 | ** |
| 654 | ** The client wants either send or receive. The server should |
| 655 | ** verify that the project code matches and that the server code |
| 656 | ** does not match. |
| 657 | */ |
| 658 | if( xfer.nToken==3 |
| 659 | && (blob_eq(&xfer.aToken[0], "pull") || blob_eq(&xfer.aToken[0], "push")) |
| 660 | && blob_is_uuid(&xfer.aToken[1]) |
| 661 | && blob_is_uuid(&xfer.aToken[2]) |
| 662 | ){ |
| 663 | const char *zPCode; |
| 664 | |
| 665 | #if 0 |
| 666 | /* This block checks to see if a server is trying to sync with itself. |
| 667 | ** This used to be disallowed, but I cannot think of any significant |
| 668 | ** harm, so I have disabled the check. |
| 669 | ** |
| 670 | ** With this check disabled, it is sufficient to copy the repository |
| 671 | ** database. No need to run clone. |
| 672 | */ |
| 673 | const char *zSCode; |
| 674 | zSCode = db_get("server-code", 0); |
| 675 | if( zSCode==0 ){ |
| 676 | fossil_panic("missing server code"); |
| 677 | } |
| 678 | if( blob_eq_str(&xfer.aToken[1], zSCode, -1) ){ |
| 679 | cgi_reset_content(); |
| 680 | @ error server\sloop |
| 681 | nErr++; |
| 682 | break; |
| 683 | } |
| 684 | #endif |
| 685 | |
| 686 | zPCode = db_get("project-code", 0); |
| 687 | if( zPCode==0 ){ |
| 688 | fossil_panic("missing project code"); |
| 689 | } |
| 690 | if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){ |
| @@ -723,10 +720,11 @@ | |
| 723 | */ |
| 724 | if( blob_eq(&xfer.aToken[0], "clone") ){ |
| 725 | login_check_credentials(); |
| 726 | if( !g.okClone ){ |
| 727 | cgi_reset_content(); |
| 728 | @ error not\sauthorized\sto\sclone |
| 729 | nErr++; |
| 730 | break; |
| 731 | } |
| 732 | isClone = 1; |
| @@ -960,11 +958,11 @@ | |
| 960 | blobarray_zero(xfer.aToken, count(xfer.aToken)); |
| 961 | blob_zero(&send); |
| 962 | blob_zero(&recv); |
| 963 | blob_zero(&xfer.err); |
| 964 | blob_zero(&xfer.line); |
| 965 | origConfigRcvMask = configRcvMask; |
| 966 | |
| 967 | /* |
| 968 | ** Always begin with a clone, pull, or push message |
| 969 | */ |
| 970 | if( cloneFlag ){ |
| @@ -1005,12 +1003,15 @@ | |
| 1005 | if( pushFlag ){ |
| 1006 | send_unsent(&xfer); |
| 1007 | nCardSent += send_unclustered(&xfer); |
| 1008 | } |
| 1009 | |
| 1010 | /* Send configuration parameter requests */ |
| 1011 | if( configRcvMask ){ |
| 1012 | const char *zName; |
| 1013 | zName = configure_first_name(configRcvMask); |
| 1014 | while( zName ){ |
| 1015 | blob_appendf(&send, "reqconfig %s\n", zName); |
| 1016 | zName = configure_next_name(configRcvMask); |
| @@ -1017,10 +1018,11 @@ | |
| 1017 | nCardSent++; |
| 1018 | } |
| 1019 | if( configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT) ){ |
| 1020 | configure_prepare_to_receive(0); |
| 1021 | } |
| 1022 | configRcvMask = 0; |
| 1023 | } |
| 1024 | |
| 1025 | /* Send configuration parameters being pushed */ |
| 1026 | if( configSendMask ){ |
| @@ -1032,11 +1034,14 @@ | |
| 1032 | nCardSent++; |
| 1033 | } |
| 1034 | configSendMask = 0; |
| 1035 | } |
| 1036 | |
| 1037 | /* Append randomness to the end of the message */ |
| 1038 | zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
| 1039 | blob_appendf(&send, "# %s\n", zRandomness); |
| 1040 | free(zRandomness); |
| 1041 | |
| 1042 | /* Exchange messages with the server */ |
| @@ -1213,17 +1218,26 @@ | |
| 1213 | printf("\rServer says: %s\n", zMsg); |
| 1214 | }else |
| 1215 | |
| 1216 | /* error MESSAGE |
| 1217 | ** |
| 1218 | ** Report an error and abandon the sync session |
| 1219 | */ |
| 1220 | if( blob_eq(&xfer.aToken[0],"error") && xfer.nToken==2 ){ |
| 1221 | char *zMsg = blob_terminate(&xfer.aToken[1]); |
| 1222 | defossilize(zMsg); |
| 1223 | blob_appendf(&xfer.err, "server says: %s", zMsg); |
| 1224 | printf("Server Error: %s\n", zMsg); |
| 1225 | }else |
| 1226 | |
| 1227 | /* Unknown message */ |
| 1228 | { |
| 1229 | if( blob_str(&xfer.aToken[0])[0]=='<' ){ |
| @@ -1272,10 +1286,13 @@ | |
| 1272 | ** another round |
| 1273 | */ |
| 1274 | if( xfer.nFileSent+xfer.nDeltaSent>0 ){ |
| 1275 | go = 1; |
| 1276 | } |
| 1277 | }; |
| 1278 | transport_stats(&nSent, &nRcvd, 1); |
| 1279 | printf("Total network traffic: %d bytes sent, %d bytes received\n", |
| 1280 | nSent, nRcvd); |
| 1281 | transport_close(); |
| 1282 | |
| 1283 | DDED www/password.wiki |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -357,11 +357,10 @@ | |
| 357 | blob_reset(&h2); |
| 358 | blob_reset(&tail); |
| 359 | return rc==0; |
| 360 | } |
| 361 | |
| 362 | /* |
| 363 | ** Check the signature on an application/x-fossil payload received by |
| 364 | ** the HTTP server. The signature is a line of the following form: |
| 365 | ** |
| 366 | ** login LOGIN NONCE SIGNATURE |
| @@ -394,28 +393,49 @@ | |
| 393 | " AND login NOT IN ('anonymous','nobody','developer','reader')" |
| 394 | " AND length(pw)>0", |
| 395 | zLogin |
| 396 | ); |
| 397 | if( db_step(&q)==SQLITE_ROW ){ |
| 398 | int szPw; |
| 399 | Blob pw, combined, hash; |
| 400 | blob_zero(&pw); |
| 401 | db_ephemeral_blob(&q, 0, &pw); |
| 402 | szPw = blob_size(&pw); |
| 403 | blob_zero(&combined); |
| 404 | blob_copy(&combined, pNonce); |
| 405 | blob_append(&combined, blob_buffer(&pw), szPw); |
| 406 | sha1sum_blob(&combined, &hash); |
| 407 | assert( blob_size(&hash)==40 ); |
| 408 | rc = blob_compare(&hash, pSig); |
| 409 | blob_reset(&hash); |
| 410 | blob_reset(&combined); |
| 411 | if( rc!=0 && szPw!=40 ){ |
| 412 | /* If this server stores cleartext passwords and the password did not |
| 413 | ** match, then perhaps the client is sending SHA1 passwords. Try |
| 414 | ** again with the SHA1 password. |
| 415 | */ |
| 416 | const char *zPw = db_column_text(&q, 0); |
| 417 | char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin)); |
| 418 | blob_zero(&combined); |
| 419 | blob_copy(&combined, pNonce); |
| 420 | blob_append(&combined, zSecret, -1); |
| 421 | free(zSecret); |
| 422 | sha1sum_blob(&combined, &hash); |
| 423 | rc = blob_compare(&hash, pSig); |
| 424 | blob_reset(&hash); |
| 425 | blob_reset(&combined); |
| 426 | } |
| 427 | if( rc==0 ){ |
| 428 | const char *zCap; |
| 429 | zCap = db_column_text(&q, 1); |
| 430 | login_set_capabilities(zCap); |
| 431 | g.userUid = db_column_int(&q, 2); |
| 432 | g.zLogin = mprintf("%b", pLogin); |
| 433 | g.zNonce = mprintf("%b", pNonce); |
| 434 | if( g.fHttpTrace ){ |
| 435 | fprintf(stderr, "# login [%s] with capabilities [%s]\n", g.zLogin,zCap); |
| 436 | } |
| 437 | } |
| 438 | } |
| 439 | db_finalize(&q); |
| 440 | |
| 441 | if( rc==0 ){ |
| @@ -650,41 +670,18 @@ | |
| 670 | |
| 671 | /* pull SERVERCODE PROJECTCODE |
| 672 | ** push SERVERCODE PROJECTCODE |
| 673 | ** |
| 674 | ** The client wants either send or receive. The server should |
| 675 | ** verify that the project code matches. |
| 676 | */ |
| 677 | if( xfer.nToken==3 |
| 678 | && (blob_eq(&xfer.aToken[0], "pull") || blob_eq(&xfer.aToken[0], "push")) |
| 679 | && blob_is_uuid(&xfer.aToken[1]) |
| 680 | && blob_is_uuid(&xfer.aToken[2]) |
| 681 | ){ |
| 682 | const char *zPCode; |
| 683 | zPCode = db_get("project-code", 0); |
| 684 | if( zPCode==0 ){ |
| 685 | fossil_panic("missing project code"); |
| 686 | } |
| 687 | if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){ |
| @@ -723,10 +720,11 @@ | |
| 720 | */ |
| 721 | if( blob_eq(&xfer.aToken[0], "clone") ){ |
| 722 | login_check_credentials(); |
| 723 | if( !g.okClone ){ |
| 724 | cgi_reset_content(); |
| 725 | @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x")) |
| 726 | @ error not\sauthorized\sto\sclone |
| 727 | nErr++; |
| 728 | break; |
| 729 | } |
| 730 | isClone = 1; |
| @@ -960,11 +958,11 @@ | |
| 958 | blobarray_zero(xfer.aToken, count(xfer.aToken)); |
| 959 | blob_zero(&send); |
| 960 | blob_zero(&recv); |
| 961 | blob_zero(&xfer.err); |
| 962 | blob_zero(&xfer.line); |
| 963 | origConfigRcvMask = 0; |
| 964 | |
| 965 | /* |
| 966 | ** Always begin with a clone, pull, or push message |
| 967 | */ |
| 968 | if( cloneFlag ){ |
| @@ -1005,12 +1003,15 @@ | |
| 1003 | if( pushFlag ){ |
| 1004 | send_unsent(&xfer); |
| 1005 | nCardSent += send_unclustered(&xfer); |
| 1006 | } |
| 1007 | |
| 1008 | /* Send configuration parameter requests. On a clone, delay sending |
| 1009 | ** this until the second cycle since the login card might fail on |
| 1010 | ** the first cycle. |
| 1011 | */ |
| 1012 | if( configRcvMask && (cloneFlag==0 || nCycle>0) ){ |
| 1013 | const char *zName; |
| 1014 | zName = configure_first_name(configRcvMask); |
| 1015 | while( zName ){ |
| 1016 | blob_appendf(&send, "reqconfig %s\n", zName); |
| 1017 | zName = configure_next_name(configRcvMask); |
| @@ -1017,10 +1018,11 @@ | |
| 1018 | nCardSent++; |
| 1019 | } |
| 1020 | if( configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT) ){ |
| 1021 | configure_prepare_to_receive(0); |
| 1022 | } |
| 1023 | origConfigRcvMask = configRcvMask; |
| 1024 | configRcvMask = 0; |
| 1025 | } |
| 1026 | |
| 1027 | /* Send configuration parameters being pushed */ |
| 1028 | if( configSendMask ){ |
| @@ -1032,11 +1034,14 @@ | |
| 1034 | nCardSent++; |
| 1035 | } |
| 1036 | configSendMask = 0; |
| 1037 | } |
| 1038 | |
| 1039 | /* Append randomness to the end of the message. This makes all |
| 1040 | ** messages unique so that that the login-card nonce will always |
| 1041 | ** be unique. |
| 1042 | */ |
| 1043 | zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
| 1044 | blob_appendf(&send, "# %s\n", zRandomness); |
| 1045 | free(zRandomness); |
| 1046 | |
| 1047 | /* Exchange messages with the server */ |
| @@ -1213,17 +1218,26 @@ | |
| 1218 | printf("\rServer says: %s\n", zMsg); |
| 1219 | }else |
| 1220 | |
| 1221 | /* error MESSAGE |
| 1222 | ** |
| 1223 | ** Report an error and abandon the sync session. |
| 1224 | ** |
| 1225 | ** Except, when cloning we will sometimes get an error on the |
| 1226 | ** first message exchange because the project-code is unknown |
| 1227 | ** and so the login card on the request was invalid. The project-code |
| 1228 | ** is returned in the reply before the error card, so second and |
| 1229 | ** subsequent messages should be OK. Nevertheless, we need to ignore |
| 1230 | ** the error card on the first message of a clone. |
| 1231 | */ |
| 1232 | if( blob_eq(&xfer.aToken[0],"error") && xfer.nToken==2 ){ |
| 1233 | if( !cloneFlag || nCycle>0 ){ |
| 1234 | char *zMsg = blob_terminate(&xfer.aToken[1]); |
| 1235 | defossilize(zMsg); |
| 1236 | blob_appendf(&xfer.err, "server says: %s", zMsg); |
| 1237 | printf("Server Error: %s\n", zMsg); |
| 1238 | } |
| 1239 | }else |
| 1240 | |
| 1241 | /* Unknown message */ |
| 1242 | { |
| 1243 | if( blob_str(&xfer.aToken[0])[0]=='<' ){ |
| @@ -1272,10 +1286,13 @@ | |
| 1286 | ** another round |
| 1287 | */ |
| 1288 | if( xfer.nFileSent+xfer.nDeltaSent>0 ){ |
| 1289 | go = 1; |
| 1290 | } |
| 1291 | |
| 1292 | /* If this is a clone, the go at least two rounds */ |
| 1293 | if( cloneFlag && nCycle==1 ) go = 1; |
| 1294 | }; |
| 1295 | transport_stats(&nSent, &nRcvd, 1); |
| 1296 | printf("Total network traffic: %d bytes sent, %d bytes received\n", |
| 1297 | nSent, nRcvd); |
| 1298 | transport_close(); |
| 1299 | |
| 1300 | DDED www/password.wiki |
+66
| --- a/www/password.wiki | ||
| +++ b/www/password.wiki | ||
| @@ -0,0 +1,66 @@ | ||
| 1 | +<title>Fossil P<h1 align="center">Password Management</h1 Password Management</title> | |
| 2 | + | |
| 3 | +Fossil handles user authentication using passwords. | |
| 4 | +Passwords are unique to each repository. Passwords are not part of the | |
| 5 | +persistent state of a project. Passwords are not versioned and | |
| 6 | +are not transmitted from one repository to another during a sync. | |
| 7 | +Passwords are local configuration information that can (and usually does) | |
| 8 | +vary from one repository to the next within the same project. | |
| 9 | + | |
| 10 | +Passwords are stored in the PW field of the USER table. | |
| 11 | +In older v ersions of Fossil (prior+ | 2010-01-11]) the password | |
| 12 | +is stored as cleartext. In newer versions of Fossil, the password | |
| 13 | +can be either cleartext or an SHA1 hash (written as a 40-character | |
| 14 | +lower-case hexadecimal number). If the USER.PW field contains | |
| 15 | +a 40-character string, that string is assumed to be a SHA1 hash. | |
| 16 | +If the size of USER.PW is anything other than 40 characters, then | |
| 17 | +it is understood as a plain-text password. | |
| 18 | + | |
| 19 | +The SHA1 hash in the USER.PW field is a hash of a string composed of | |
| 20 | +the project-code, the user login, and the user cleartext password. | |
| 21 | +Suppose user "alice" with password "asdfg" had an account on the | |
| 22 | +Fossil self-hosting repository. Then the vale of USER.PW | |
| 23 | +for alice wblockquotld be the SHA1 fferent USER.PW bloctkquotmore users on | |
| 24 | +the repository seThat hash value is "f1b699cc9af3eeb98e5de244ca7802ae38e77bae". Note | |
| 25 | + hash, | |
| 26 | + multiple projects. | |
| 27 | + | |
| 28 | +Whenever a pas the | |
| 29 | +nterface or using the | |
| 30 | +"user" comm reuses the | |
| 31 | +ored using the SHA1 | |
| 32 | +encoding. Thus, cleartext passwords will gradually migrate to become | |
| 33 | +SHA1 passwords. All remaning cleartext passwords can be convertmpossilowing command: | |
| 34 | + | |
| 35 | +<pre> | |
| 36 | +fossil test-hash-passwords <i>REPOSITORY-NA to SHA1 passwords is an | |
| 37 | +irblockquote>rreversible ojeration. | |
| 38 | + | |
| 39 | +The only wa</blockquotxt password into the USER table | |
| 40 | +is to do so manually using SQL commands. user SEA login cookie will only work if the IP address matches. This feature | |
| 41 | +is designed to make it more difficult for an attacker to sniff the cookie | |
| 42 | +and take over the connection. A cookie-sniffing attack will only work | |
| 43 | +if the attacker is able to send and receive from the same IP address as | |
| 44 | +the original login. However, we found that doing an exact IP match | |
| 45 | +caused problems for some users who are behind proxy firewalls where the proxy | |
| 46 | +might use a different IP address for each query. To work around this | |
| 47 | +problem, newer versions of fossil only check the first 16 bits of the | |
| 48 | +32-bit IP address. This makes a cookie sniffing attack easier since now | |
| 49 | +the attacker only has to send and receive from any IP address in a range | |
| 50 | +of IPs that are similar to the initial login. But that is seen as an | |
| 51 | +acceptable compromise in exchange for ease of use. If higher security | |
| 52 | +is really needed, then HTTPS can be used instead of HTTP SET pw='asdfg' WHERE login='alice'; | |
| 53 | +</pre> | |
| 54 | + | |
| 55 | +Note that an password that is an empty string or NULL will disable | |
| 56 | +ablockquote>Thus, to lock a user out of the s</blockquot system, | |
| 57 | +one has only to set their password to an empty string, using either | |
| 58 | +the web interface or direct SQL manipulation of the USER table. | |
| 59 | +Note also that the password field is | |
| 60 | +essentially ignored for the special users named "anonymous", "developer", | |
| 61 | +"reader", and "nobody". It is not possible to authenticate as users | |
| 62 | +"developer", "reader", or "nobody" and the authentication protocol | |
| 63 | +for "anonymous" uses one-time captchas not persistent passwords. | |
| 64 | + | |
| 65 | +<h2>Web spository. When two | |
| 66 | +res |
| --- a/www/password.wiki | |
| +++ b/www/password.wiki | |
| @@ -0,0 +1,66 @@ | |
| --- a/www/password.wiki | |
| +++ b/www/password.wiki | |
| @@ -0,0 +1,66 @@ | |
| 1 | <title>Fossil P<h1 align="center">Password Management</h1 Password Management</title> |
| 2 | |
| 3 | Fossil handles user authentication using passwords. |
| 4 | Passwords are unique to each repository. Passwords are not part of the |
| 5 | persistent state of a project. Passwords are not versioned and |
| 6 | are not transmitted from one repository to another during a sync. |
| 7 | Passwords are local configuration information that can (and usually does) |
| 8 | vary from one repository to the next within the same project. |
| 9 | |
| 10 | Passwords are stored in the PW field of the USER table. |
| 11 | In older v ersions of Fossil (prior+ | 2010-01-11]) the password |
| 12 | is stored as cleartext. In newer versions of Fossil, the password |
| 13 | can be either cleartext or an SHA1 hash (written as a 40-character |
| 14 | lower-case hexadecimal number). If the USER.PW field contains |
| 15 | a 40-character string, that string is assumed to be a SHA1 hash. |
| 16 | If the size of USER.PW is anything other than 40 characters, then |
| 17 | it is understood as a plain-text password. |
| 18 | |
| 19 | The SHA1 hash in the USER.PW field is a hash of a string composed of |
| 20 | the project-code, the user login, and the user cleartext password. |
| 21 | Suppose user "alice" with password "asdfg" had an account on the |
| 22 | Fossil self-hosting repository. Then the vale of USER.PW |
| 23 | for alice wblockquotld be the SHA1 fferent USER.PW bloctkquotmore users on |
| 24 | the repository seThat hash value is "f1b699cc9af3eeb98e5de244ca7802ae38e77bae". Note |
| 25 | hash, |
| 26 | multiple projects. |
| 27 | |
| 28 | Whenever a pas the |
| 29 | nterface or using the |
| 30 | "user" comm reuses the |
| 31 | ored using the SHA1 |
| 32 | encoding. Thus, cleartext passwords will gradually migrate to become |
| 33 | SHA1 passwords. All remaning cleartext passwords can be convertmpossilowing command: |
| 34 | |
| 35 | <pre> |
| 36 | fossil test-hash-passwords <i>REPOSITORY-NA to SHA1 passwords is an |
| 37 | irblockquote>rreversible ojeration. |
| 38 | |
| 39 | The only wa</blockquotxt password into the USER table |
| 40 | is to do so manually using SQL commands. user SEA login cookie will only work if the IP address matches. This feature |
| 41 | is designed to make it more difficult for an attacker to sniff the cookie |
| 42 | and take over the connection. A cookie-sniffing attack will only work |
| 43 | if the attacker is able to send and receive from the same IP address as |
| 44 | the original login. However, we found that doing an exact IP match |
| 45 | caused problems for some users who are behind proxy firewalls where the proxy |
| 46 | might use a different IP address for each query. To work around this |
| 47 | problem, newer versions of fossil only check the first 16 bits of the |
| 48 | 32-bit IP address. This makes a cookie sniffing attack easier since now |
| 49 | the attacker only has to send and receive from any IP address in a range |
| 50 | of IPs that are similar to the initial login. But that is seen as an |
| 51 | acceptable compromise in exchange for ease of use. If higher security |
| 52 | is really needed, then HTTPS can be used instead of HTTP SET pw='asdfg' WHERE login='alice'; |
| 53 | </pre> |
| 54 | |
| 55 | Note that an password that is an empty string or NULL will disable |
| 56 | ablockquote>Thus, to lock a user out of the s</blockquot system, |
| 57 | one has only to set their password to an empty string, using either |
| 58 | the web interface or direct SQL manipulation of the USER table. |
| 59 | Note also that the password field is |
| 60 | essentially ignored for the special users named "anonymous", "developer", |
| 61 | "reader", and "nobody". It is not possible to authenticate as users |
| 62 | "developer", "reader", or "nobody" and the authentication protocol |
| 63 | for "anonymous" uses one-time captchas not persistent passwords. |
| 64 | |
| 65 | <h2>Web spository. When two |
| 66 | res |