/* ** Copyright (c) 2009 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code to a simple text-based CAPTCHA. Though easily ** defeated by a sophisticated attacker, this CAPTCHA does at least make ** scripting attacks more difficult. */ #include "config.h" #include <assert.h> #include "captcha.h" #if INTERFACE #define CAPTCHA 2 /* Which captcha rendering to use */ #endif /* ** Convert a hex digit into a value between 0 and 15 */ int hex_digit_value(char c){ if( c>='0' && c<='9' ){ return c - '0'; }else if( c>='a' && c<='f' ){ return c - 'a' + 10; }else if( c>='A' && c<='F' ){ return c - 'A' + 10; }else{ return 0; } } #if CAPTCHA==1 /* ** A 4x6 pixel bitmap font for hexadecimal digits */ static const unsigned int aFont1[] = { 0x699996, 0x262227, 0x69124f, 0xf16196, 0x26af22, 0xf8e196, 0x68e996, 0xf12244, 0x696996, 0x699716, 0x699f99, 0xe9e99e, 0x698896, 0xe9999e, 0xf8e88f, 0xf8e888, }; /* ** Render an 8-character hexadecimal string as ascii art. ** Space to hold the result is obtained from malloc() and should be freed ** by the caller. */ char *captcha_render(const char *zPw){ char *z = fossil_malloc( 9*12*3*strlen(zPw) + 8 ); int i, j, k, m; k = 0; for(i=0; i<6; i++){ for(j=0; zPw[j]; j++){ unsigned char v = hex_digit_value(zPw[j]); v = (aFont1[v] >> ((5-i)*4)) & 0xf; for(m=8; m>=1; m = m>>1){ if( v & m ){ z[k++] = 0xe2; z[k++] = 0x96; z[k++] = 0x88; z[k++] = 0xe2; z[k++] = 0x96; z[k++] = 0x88; }else{ z[k++] = ' '; z[k++] = ' '; } } z[k++] = ' '; z[k++] = ' '; } z[k++] = '\n'; } z[k] = 0; return z; } #endif /* CAPTCHA==1 */ #if CAPTCHA==2 /* ** A 5x7 pixel bitmap font for hexadecimal digits */ static const unsigned char aFont2[] = { /* 0 */ 0x0e, 0x13, 0x15, 0x19, 0x11, 0x11, 0x0e, /* 1 */ 0x02, 0x06, 0x0A, 0x02, 0x02, 0x02, 0x02, /* 2 */ 0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f, /* 3 */ 0x0e, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0e, /* 4 */ 0x02, 0x06, 0x0A, 0x12, 0x1f, 0x02, 0x02, /* 5 */ 0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e, /* 6 */ 0x0e, 0x11, 0x10, 0x1e, 0x11, 0x11, 0x0e, /* 7 */ 0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08, /* 8 */ 0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e, /* 9 */ 0x0e, 0x11, 0x11, 0x0f, 0x01, 0x11, 0x0e, /* A */ 0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, /* B */ 0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e, /* C */ 0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e, /* D */ 0x1c, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1c, /* E */ 0x1f, 0x10, 0x10, 0x1c, 0x10, 0x10, 0x1f, /* F */ 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10, }; /* ** Render an 8-character hexadecimal string as ascii art. ** Space to hold the result is obtained from malloc() and should be freed ** by the caller. */ char *captcha_render(const char *zPw){ char *z = fossil_malloc( 160*strlen(zPw) + 9 ); int i, j, k, m; k = 0; for(i=0; i<7; i++){ for(j=0; zPw[j]; j++){ unsigned char v = hex_digit_value(zPw[j]); v = aFont2[v*7+i]; for(m=16; m>=1; m = m>>1){ if( v & m ){ z[k++] = 0xe2; z[k++] = 0x96; z[k++] = 0x88; z[k++] = 0xe2; z[k++] = 0x96; z[k++] = 0x88; }else{ z[k++] = ' '; z[k++] = ' '; } } z[k++] = ' '; z[k++] = ' '; } z[k++] = '\n'; } z[k] = 0; return z; } #endif /* CAPTCHA==2 */ #if CAPTCHA==3 static const char *const azFont3[] = { /* 0 */ " __ ", " / \\ ", "| () |", " \\__/ ", /* 1 */ " _ ", "/ |", "| |", "|_|", /* 2 */ " ___ ", "|_ )", " / / ", "/___|", /* 3 */ " ____", "|__ /", " |_ \\", "|___/", /* 4 */ " _ _ ", "| | | ", "|_ _|", " |_| ", /* 5 */ " ___ ", "| __|", "|__ \\", "|___/", /* 6 */ " __ ", " / / ", "/ _ \\", "\\___/", /* 7 */ " ____ ", "|__ |", " / / ", " /_/ ", /* 8 */ " ___ ", "( _ )", "/ _ \\", "\\___/", /* 9 */ " ___ ", "/ _ \\", "\\_, /", " /_/ ", /* A */ " ", " /\\ ", " / \\ ", "/_/\\_\\", /* B */ " ___ ", "| _ )", "| _ \\", "|___/", /* C */ " ___ ", " / __|", "| (__ ", " \\___|", /* D */ " ___ ", "| \\ ", "| |) |", "|___/ ", /* E */ " ___ ", "| __|", "| _| ", "|___|", /* F */ " ___ ", "| __|", "| _| ", "|_| ", }; /* ** Render an 8-digit hexadecimal string as ascii arg. ** Space to hold the result is obtained from malloc() and should be freed ** by the caller. */ char *captcha_render(const char *zPw){ char *z = fossil_malloc( 7*4*strlen(zPw) + 5 ); int i, j, k, m; const char *zChar; k = 0; for(i=0; i<4; i++){ for(j=0; zPw[j]; j++){ unsigned char v = hex_digit_value(zPw[j]); zChar = azFont3[4*v + i]; for(m=0; zChar[m]; m++){ z[k++] = zChar[m]; } } z[k++] = '\n'; } z[k] = 0; return z; } #endif /* CAPTCHA==3 */ #if CAPTCHA==4 static const char *const azFont4[] = { /* 0 */ " ___ ", " / _ \\ ", "| | | |", "| | | |", "| |_| |", " \\___/ ", /* 1 */ " __ ", "/_ |", " | |", " | |", " | |", " |_|", /* 2 */ " ___ ", "|__ \\ ", " ) |", " / / ", " / /_ ", "|____|", /* 3 */ " ____ ", "|___ \\ ", " __) |", " |__ < ", " ___) |", "|____/ ", /* 4 */ " _ _ ", "| || | ", "| || |_ ", "|__ _|", " | | ", " |_| ", /* 5 */ " _____ ", "| ____|", "| |__ ", "|___ \\ ", " ___) |", "|____/ ", /* 6 */ " __ ", " / / ", " / /_ ", "| '_ \\ ", "| (_) |", " \\___/ ", /* 7 */ " ______ ", "|____ |", " / / ", " / / ", " / / ", " /_/ ", /* 8 */ " ___ ", " / _ \\ ", "| (_) |", " > _ < ", "| (_) |", " \\___/ ", /* 9 */ " ___ ", " / _ \\ ", "| (_) |", " \\__, |", " / / ", " /_/ ", /* A */ " ", " /\\ ", " / \\ ", " / /\\ \\ ", " / ____ \\ ", "/_/ \\_\\", /* B */ " ____ ", "| _ \\ ", "| |_) |", "| _ < ", "| |_) |", "|____/ ", /* C */ " _____ ", " / ____|", "| | ", "| | ", "| |____ ", " \\_____|", /* D */ " _____ ", "| __ \\ ", "| | | |", "| | | |", "| |__| |", "|_____/ ", /* E */ " ______ ", "| ____|", "| |__ ", "| __| ", "| |____ ", "|______|", /* F */ " ______ ", "| ____|", "| |__ ", "| __| ", "| | ", "|_| ", }; /* ** Render an 8-digit hexadecimal string as ascii arg. ** Space to hold the result is obtained from malloc() and should be freed ** by the caller. */ char *captcha_render(const char *zPw){ char *z = fossil_malloc( 10*6*strlen(zPw) + 7 ); int i, j, k, m; const char *zChar; unsigned char x; int y; k = 0; for(i=0; i<6; i++){ x = 0; for(j=0; zPw[j]; j++){ unsigned char v = hex_digit_value(zPw[j]); x = (x<<4) + v; switch( x ){ case 0x7a: case 0xfa: y = 3; break; case 0x47: y = 2; break; case 0xf6: case 0xa9: case 0xa4: case 0xa1: case 0x9a: case 0x76: case 0x61: case 0x67: case 0x69: case 0x41: case 0x42: case 0x43: case 0x4a: y = 1; break; default: y = 0; break; } zChar = azFont4[6*v + i]; while( y && zChar[0]==' ' ){ y--; zChar++; } while( y && z[k-1]==' ' ){ y--; k--; } for(m=0; zChar[m]; m++){ z[k++] = zChar[m]; } } z[k++] = '\n'; } z[k] = 0; return z; } #endif /* CAPTCHA==4 */ /* ** COMMAND: test-captcha ** ** Render an ASCII-art captcha for numbers given on the command line. */ void test_captcha(void){ int i; unsigned int v; char *z; for(i=2; i<g.argc; i++){ char zHex[30]; v = (unsigned int)atoi(g.argv[i]); sqlite3_snprintf(sizeof(zHex), zHex, "%x", v); z = captcha_render(zHex); fossil_print("%s:\n%s", zHex, z); free(z); } } /* ** Compute a seed value for a captcha. The seed is public and is sent ** as a hidden parameter with the page that contains the captcha. Knowledge ** of the seed is insufficient for determining the captcha without additional ** information held only on the server and never revealed. */ unsigned int captcha_seed(void){ unsigned int x; sqlite3_randomness(sizeof(x), &x); x &= 0x7fffffff; return x; } /* The SQL that will rotate the the captcha-secret. */ static const char captchaSecretRotationSql[] = @ SAVEPOINT rotate; @ DELETE FROM config @ WHERE name GLOB 'captcha-secret-*' @ AND mtime<unixepoch('now','-6 hours'); @ UPDATE config @ SET name=format('captcha-secret-%%d',substr(name,16)+1) @ WHERE name GLOB 'captcha-secret-*'; @ UPDATE config @ SET name='captcha-secret-1', mtime=unixepoch() @ WHERE name='captcha-secret'; @ REPLACE INTO config(name,value,mtime) @ VALUES('captcha-secret',%Q,unixepoch()); @ RELEASE rotate; ; /* ** Create a new random captcha-secret. Rotate the old one into ** the captcha-secret-N backups. Purge captch-secret-N backups ** older than 6 hours. ** ** Do this on the current database and in all other databases of ** the same login group. */ void captcha_secret_rotate(void){ char *zNew = db_text(0, "SELECT lower(hex(randomblob(20)))"); char *zSql = mprintf(captchaSecretRotationSql/*works-like:"%Q"*/, zNew); char *zErrs = 0; fossil_free(zNew); db_unprotect(PROTECT_CONFIG); db_begin_transaction(); sqlite3_exec(g.db, zSql, 0, 0, &zErrs); db_protect_pop(); if( zErrs && zErrs[0] ){ db_rollback_transaction(); fossil_fatal("Unable to rotate captcha-secret\n%s\nERROR: %s\n", zSql, zErrs); } db_end_transaction(0); login_group_sql(zSql, "", "", &zErrs); if( zErrs ){ sqlite3_free(zErrs); /* Silently ignore errors on other repos */ } fossil_free(zSql); } /* ** Return the value of the N-th more recent captcha-secret. The ** most recent captch-secret is 0. Others are prior captcha-secrets ** that have expired, but are retained for a limited period of time ** so that pending anonymous login cookies and/or captcha dialogs ** don't malfunction when the captcha-secret changes. ** ** Clients should start by using the 0-th captcha-secret. Only if ** that one does not work should they advance to 1 and 2 and so forth, ** until this routine returns a NULL pointer. ** ** The value returned is a string obtained from fossil_malloc() and ** should be freed by the caller. ** ** The 0-th captcha secret is the value of Config.Name='captcha-secret'. ** For N>0, the value is in Config.Name='captcha-secret-$N'. */ char *captcha_secret(int N){ if( N==0 ){ return db_text(0, "SELECT value FROM config WHERE name='captcha-secret'"); }else{ return db_text(0, "SELECT value FROM config" " WHERE name='captcha-secret-%d'" " AND mtime>unixepoch('now','-6 hours')", N); } } /* ** Translate a captcha seed value into the captcha password string. ** The returned string is static and overwritten on each call to ** this function. ** ** Use the N-th captcha secret to compute the password. When N==0, ** a valid password is always returned. A new captcha-secret will ** be created if necessary. But for N>0, the return value might ** be NULL to indicate that there is no N-th captcha-secret. */ const char *captcha_decode(unsigned int seed, int N){ char *zSecret; const char *z; Blob b; static char zRes[20]; zSecret = captcha_secret(N); if( zSecret==0 ){ if( N>0 ) return 0; db_unprotect(PROTECT_CONFIG); db_multi_exec( "REPLACE INTO config(name,value)" " VALUES('captcha-secret', lower(hex(randomblob(20))));" ); db_protect_pop(); zSecret = captcha_secret(0); assert( zSecret!=0 ); } blob_init(&b, 0, 0); blob_appendf(&b, "%s-%x", zSecret, seed); sha1sum_blob(&b, &b); z = blob_buffer(&b); memcpy(zRes, z, 8); zRes[8] = 0; fossil_free(zSecret); return zRes; } /* ** Return true if a CAPTCHA is required for editing wiki or tickets or for ** adding attachments. ** ** A CAPTCHA is required in those cases if the user is not logged in (if they ** are user "nobody") and if the "require-captcha" setting is true. The ** "require-captcha" setting is controlled on the Admin/Access page. It ** defaults to true. */ int captcha_needed(void){ return login_is_nobody() && db_get_boolean("require-captcha", 1); } /* ** If a captcha is required but the correct captcha code is not supplied ** in the query parameters, then return false (0). ** ** If no captcha is required or if the correct captcha is supplied, return ** true (non-zero). ** ** The query parameters examined are "captchaseed" for the seed value and ** "captcha" for text that the user types in response to the captcha prompt. */ int captcha_is_correct(int bAlwaysNeeded){ const char *zSeed; const char *zEntered; const char *zDecode; char z[30]; int i; int n = 0; if( !bAlwaysNeeded && !captcha_needed() ){ return 1; /* No captcha needed */ } zSeed = P("captchaseed"); if( zSeed==0 ) return 0; zEntered = P("captcha"); if( zEntered==0 || strlen(zEntered)!=8 ) return 0; do{ zDecode = captcha_decode((unsigned int)atoi(zSeed), n++); if( zDecode==0 ) return 0; assert( strlen(zDecode)==8 ); for(i=0; i<8; i++){ char c = zEntered[i]; if( c>='A' && c<='F' ) c += 'a' - 'A'; if( c=='O' ) c = '0'; z[i] = c; } }while( strncmp(zDecode,z,8)!=0 ); return 1; } /* ** Generate a captcha display together with the necessary hidden parameter ** for the seed and the entry box into which the user will type the text of ** the captcha. This is typically done at the very bottom of a form. ** ** This routine is a no-op if no captcha is required. ** ** Flag values: ** ** 0x01 Show the "Submit" button in the form. ** 0x02 Always generate the captcha, even if not required */ void captcha_generate(int mFlags){ unsigned int uSeed; const char *zDecoded; char *zCaptcha; if( !captcha_needed() && (mFlags & 0x02)==0 ) return; uSeed = captcha_seed(); zDecoded = captcha_decode(uSeed, 0); zCaptcha = captcha_render(zDecoded); @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> @ %h(zCaptcha) @ </pre> @ Enter security code shown above: @ <input type="hidden" name="captchaseed" value="%u(uSeed)"> @ <input type="text" name="captcha" size="8" autofocus> if( mFlags & 0x01 ){ @ <input type="submit" value="Submit"> } @ <br/>\ captcha_speakit_button(uSeed, 0); @ </td></tr></table></div> } /* ** Add a "Speak the captcha" button. */ void captcha_speakit_button(unsigned int uSeed, const char *zMsg){ if( zMsg==0 ) zMsg = "Speak the text"; @ <input aria-label="%h(zMsg)" type="button" value="%h(zMsg)" \ @ id="speakthetext"> @ <script nonce="%h(style_nonce())">/* captcha_speakit_button() */ @ document.getElementById("speakthetext").onclick = function(){ @ var audio = window.fossilAudioCaptcha \ @ || new Audio("%R/captcha-audio/%u(uSeed)"); @ window.fossilAudioCaptcha = audio; @ audio.currentTime = 0; @ audio.play(); @ } @ </script> } /* ** WEBPAGE: test-captcha ** ** If the name query parameter is provided, then render the hex value of ** the name using the captcha font. ** ** Otherwise render the captcha screen. The "show-button" parameter causes ** the submit button to be rendered. */ void captcha_test(void){ const char *zPw = P("name"); if( zPw==0 || zPw[0]==0 ){ (void)exclude_spiders(1); @ <hr><p>The captcha is shown above. Add a name=HEX query parameter @ to see how HEX would be rendered in the current captcha font. @ <h2>Debug/Testing Values:</h2> @ <ul> @ <li> g.isHuman = %d(g.isHuman) @ <li> g.zLogin = %h(g.zLogin) @ <li> login_cookie_welformed() = %d(login_cookie_wellformed()) @ <li> captcha_is_correct(1) = %d(captcha_is_correct(1)). @ </ul> style_finish_page(); }else{ style_set_current_feature("test"); style_header("Captcha Test"); @ <pre class="captcha"> @ %s(captcha_render(zPw)) @ </pre> style_finish_page(); } } /* ** Check to see if the current request is coming from an agent that ** self-identifies as a spider. ** ** If the agent does not claim to be a spider or if the user has logged ** in (even as anonymous), then return 0 without doing anything. ** ** But if the user agent does self-identify as a spider and there is ** no login, offer a captcha challenge to allow the user agent to prove ** that he is human and return non-zero. ** ** If the bTest argument is non-zero, then show the captcha regardless of ** how the agent identifies. This is used for testing only. */ int exclude_spiders(int bTest){ if( !bTest ){ if( g.isHuman ) return 0; /* This user has already proven human */ if( g.zLogin!=0 ) return 0; /* Logged in. Consider them human */ if( login_cookie_wellformed() ){ /* Logged into another member of the login group */ return 0; } } /* This appears to be a spider. Offer the captcha */ style_set_current_feature("captcha"); style_header("I think you are a robot"); style_submenu_enable(0); @ <form method='POST' action='%R/ityaar'> @ <p>You seem like a robot. @ @ <p>If you are human, you can prove that by solving the captcha below, @ after which you will be allowed to proceed. if( bTest ){ @ <input type="hidden" name="istest" value="1"> } captcha_generate(3); @ </form> if( !bTest ){ if( P("fossil-goto")==0 ){ cgi_set_cookie("fossil-goto", cgi_reconstruct_original_url(), 0, 600); } cgi_append_header("X-Robot: 1\r\n"); style_finish_page(); } return 1; } /* ** WEBPAGE: ityaar ** ** This is the action for the form that is the captcha. Not intended ** for external use. "ityaar" is an acronym "I Think You Are A Robot". ** ** If the captcha is correctly solved, then an anonymous login cookie ** is set. Regardless of whether or not the captcha was solved, this ** page always redirects to the fossil-goto cookie. */ void captcha_callback(void){ int bTest = atoi(PD("istest","0")); if( captcha_is_correct(1) ){ if( bTest==0 ){ if( !login_cookie_wellformed() ){ /* ^^^^--- Don't overwrite a valid login on another repo! */ login_set_anon_cookie(0, 0); } cgi_append_header("X-Robot: 0\r\n"); } login_redirect_to_g(); }else{ g.isHuman = 0; (void)exclude_spiders(bTest); if( bTest ){ @ <hr><p>Wrong code. Try again style_finish_page(); } } } /* ** Generate a WAV file that reads aloud the hex digits given by ** zHex. */ static void captcha_wav(const char *zHex, Blob *pOut){ int i; const int szWavHdr = 44; blob_init(pOut, 0, 0); blob_resize(pOut, szWavHdr); /* Space for the WAV header */ pOut->nUsed = szWavHdr; memset(pOut->aData, 0, szWavHdr); if( zHex==0 || zHex[0]==0 ) zHex = "0"; for(i=0; zHex[i]; i++){ int v = hex_digit_value(zHex[i]); int sz; int nData; const unsigned char *pData; char zSoundName[50]; sqlite3_snprintf(sizeof(zSoundName),zSoundName,"sounds/%c.wav", "0123456789abcdef"[v]); /* Extra silence in between letters */ if( i>0 ){ int nQuiet = 3000; blob_resize(pOut, pOut->nUsed+nQuiet); memset(pOut->aData+pOut->nUsed-nQuiet, 0x80, nQuiet); } pData = builtin_file(zSoundName, &sz); nData = sz - szWavHdr; blob_resize(pOut, pOut->nUsed+nData); memcpy(pOut->aData+pOut->nUsed-nData, pData+szWavHdr, nData); if( zHex[i+1]==0 ){ int len = pOut->nUsed + 36; memcpy(pOut->aData, pData, szWavHdr); pOut->aData[4] = (char)(len&0xff); pOut->aData[5] = (char)((len>>8)&0xff); pOut->aData[6] = (char)((len>>16)&0xff); pOut->aData[7] = (char)((len>>24)&0xff); len = pOut->nUsed; pOut->aData[40] = (char)(len&0xff); pOut->aData[41] = (char)((len>>8)&0xff); pOut->aData[42] = (char)((len>>16)&0xff); pOut->aData[43] = (char)((len>>24)&0xff); } } } /* ** WEBPAGE: /captcha-audio ** ** Return a WAV file that pronounces the digits of the captcha that ** is determined by the seed given in the name= query parameter. */ void captcha_wav_page(void){ const char *zSeed = PD("name","0"); const char *zDecode = captcha_decode((unsigned int)atoi(zSeed), 0); Blob audio; captcha_wav(zDecode, &audio); cgi_set_content_type("audio/wav"); cgi_set_content(&audio); } /* ** WEBPAGE: /test-captcha-audio ** ** Return a WAV file that pronounces the hex digits of the name= ** query parameter. */ void captcha_test_wav_page(void){ const char *zSeed = P("name"); Blob audio; captcha_wav(zSeed, &audio); cgi_set_content_type("audio/wav"); cgi_set_content(&audio); }