/*
** 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 3 /* 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*6*strlen(zPw) + 7 );
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++] = 'X';
z[k++] = 'X';
}else{
z[k++] = ' ';
z[k++] = ' ';
}
}
z[k++] = ' ';
z[k++] = ' ';
}
z[k++] = '\n';
}
z[k] = 0;
return z;
}
#endif /* CAPTCHA==1 */
#if CAPTCHA==2
static const char *const azFont2[] = {
/* 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 = azFont2[4*v + i];
for(m=0; zChar[m]; m++){
z[k++] = zChar[m];
}
}
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( 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 = azFont3[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==3 */
/*
** 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;
}
/*
** Translate a captcha seed value into the captcha password string.
** The returned string is static and overwritten on each call to
** this function.
*/
const char *captcha_decode(unsigned int seed){
const char *zSecret;
const char *z;
Blob b;
static char zRes[20];
zSecret = db_get("captcha-secret", 0);
if( zSecret==0 ){
db_unprotect(PROTECT_CONFIG);
db_multi_exec(
"REPLACE INTO config(name,value)"
" VALUES('captcha-secret', lower(hex(randomblob(20))));"
);
db_protect_pop();
zSecret = db_get("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;
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;
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;
zDecode = captcha_decode((unsigned int)atoi(zSeed));
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;
}
if( strncmp(zDecode,z,8)!=0 ) return 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.
*/
void captcha_generate(int showButton){
unsigned int uSeed;
const char *zDecoded;
char *zCaptcha;
if( !captcha_needed() ) return;
uSeed = captcha_seed();
zDecoded = captcha_decode(uSeed);
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>
if( showButton ){
@ <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
** Test the captcha-generator by rendering the value of the name= query
** parameter using ascii-art. If name= is omitted, show a random 16-digit
** hexadecimal number.
*/
void captcha_test(void){
const char *zPw = P("name");
if( zPw==0 || zPw[0]==0 ){
u64 x;
sqlite3_randomness(sizeof(x), &x);
zPw = mprintf("%016llx", x);
}
style_set_current_feature("test");
style_header("Captcha Test");
@ <pre>
@ %s(captcha_render(zPw))
@ </pre>
style_finish_page();
}
/*
** Check to see if the current request is coming from an agent that might
** be a spider. If the agent is not a spider, then return 0 without doing
** anything. But if the user agent appears to be a spider, offer
** a captcha challenge to allow the user agent to prove that it is human
** and return non-zero.
*/
int exclude_spiders(void){
const char *zCookieValue;
char *zCookieName;
if( g.isHuman ) return 0;
#if 0
{
const char *zReferer = P("HTTP_REFERER");
if( zReferer && strncmp(g.zBaseURL, zReferer, strlen(g.zBaseURL))==0 ){
return 0;
}
}
#endif
zCookieName = mprintf("fossil-cc-%.10s", db_get("project-code","x"));
zCookieValue = P(zCookieName);
if( zCookieValue && atoi(zCookieValue)==1 ) return 0;
if( captcha_is_correct(0) ){
cgi_set_cookie(zCookieName, "1", login_cookie_path(), 8*3600);
return 0;
}
/* This appears to be a spider. Offer the captcha */
style_set_current_feature("captcha");
style_header("Verification");
@ <form method='POST' action='%s(g.zPath)'>
cgi_query_parameters_to_hidden();
@ <p>Please demonstrate that you are human, not a spider or robot</p>
captcha_generate(1);
@ </form>
style_finish_page();
return 1;
}
/*
** 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));
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);
}