cuEnc64.c   [plain text]


/*
 * Copyright (c) 1998-2003,2010 Apple Inc. All Rights Reserved.
 * 
 * The contents of this file constitute Original Code as defined in and are
 * subject to the Apple Public Source License Version 1.2 (the 'License').
 * You may not use this file except in compliance with the License. Please 
 * obtain a copy of the License at http://www.apple.com/publicsource and 
 * read it before using this file.
 * 
 * This Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 
 * Please see the License for the specific language governing rights and 
 * limitations under the License.
 *
 * cuEnc64.c - encode/decode in 64-char IA5 format, per RFC 1421
 */

#include "cuEnc64.h"
#include <stdlib.h>
#include <string.h>

#ifndef	NULL
#define NULL ((void *)0)
#endif	/* NULL */

/*
 * map a 6-bit binary value to a printable character.
 */
static const
unsigned char bintoasc[] =
	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/*
 * Map an 7-bit printable character to its corresponding binary value.
 * Any illegal characters return high bit set.
 */
static const
unsigned char asctobin[] =
{
    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
    0x80, 0x80, 0x80, 0x3e, 0x80, 0x80, 0x80, 0x3f,
    0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
    0x3c, 0x3d, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
    0x80, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
    0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
    0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
    0x17, 0x18, 0x19, 0x80, 0x80, 0x80, 0x80, 0x80,
    0x80, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
    0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
    0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
    0x31, 0x32, 0x33, 0x80, 0x80, 0x80, 0x80, 0x80
};

/*
 * map 6 bits to a printing char
 */
#define ENC(c) (bintoasc[((c) & 0x3f)])

#define PAD		'='

/*
 * map one group of up to 3 bytes at inp to 4 bytes at outp.
 * Count is number of valid bytes in *inp; if less than 3, the
 * 1 or two extras must be zeros.
 */
static void encChunk(const unsigned char *inp,
	unsigned char *outp,
	int count)
{
	unsigned char c1, c2, c3, c4;

	c1 = *inp >> 2;
	c2 = ((inp[0] << 4) & 0x30) | ((inp[1] >> 4) & 0xf);
	c3 = ((inp[1] << 2) & 0x3c) | ((inp[2] >> 6) & 0x3);
	c4 = inp[2] & 0x3f;
	*outp++ = ENC(c1);
	*outp++ = ENC(c2);
	if (count == 1) {
	    *outp++ = PAD;
	    *outp   = PAD;
	} else {
	    *outp++ = ENC(c3);
	    if (count == 2) {
		*outp = PAD;
	    }
	    else {
		*outp = ENC(c4);
	    }
	}
}

/*
 * Given input buffer inbuf, length inlen, encode to 64-char IA5 format.
 * Result is fmalloc'd and returned; it is terminated by Microsoft-style
 * newline and NULL. Its length (including the trailing newline and NULL)
 * is returned in *outlen.
 */

unsigned char *cuEnc64(const unsigned char *inbuf,
	unsigned inlen,
	unsigned *outlen)		// RETURNED
{
	return cuEnc64WithLines(inbuf, inlen, 0, outlen);
}

unsigned char *cuEnc64WithLines(const unsigned char *inbuf,
	unsigned inlen,
	unsigned linelen,
	unsigned *outlen)
{
	unsigned		outTextLen;
	unsigned 		len;			// to malloc, liberal
	unsigned		olen = 0;		// actual output size
	unsigned char 	*outbuf;
	unsigned char 	endbuf[3];
	unsigned		i;
	unsigned char 	*outp;
	unsigned		numLines;
	unsigned		thisLine;

	outTextLen = ((inlen + 2) / 3) * 4;
	if(linelen) {
	    /*
	     * linelen must be 0 mod 4 for this to work; round up...
	     */
	    if((linelen & 0x03) != 0) {
	        linelen = (linelen + 3) & 0xfffffffc;
	    }
	    numLines = (outTextLen + linelen - 1)/ linelen;
	}
	else {
	    numLines = 1;
	}

	/*
	 * Total output size = encoded text size plus one newline per
	 * line of output, plus trailing NULL. We always generate newlines 
	 * as \n; when decoding, we tolerate \r\n (Microsoft) or \n.
	 */
	len = outTextLen + (2 * numLines) + 1;
	outbuf = (unsigned char*)malloc(len);
	outp = outbuf;
	thisLine = 0;

	while(inlen) {
	    if(inlen < 3) {
			for(i=0; i<3; i++) {
				if(i < inlen) {
					endbuf[i] = inbuf[i];
				}
				else {
					endbuf[i] = 0;
				}
			}
			encChunk(endbuf, outp, inlen);
			inlen = 0;
	    }
	    else {
			encChunk(inbuf, outp, 3);
			inlen -= 3;
			inbuf += 3;
	    }
	    outp += 4;
	    thisLine += 4;
	    olen += 4;
	    if((linelen != 0) && (thisLine >= linelen) && inlen) {
	        /*
			 * last trailing newline added below
			 * Note we don't split 4-byte output chunks over newlines
			 */
	    	*outp++ = '\n';
			olen++;
			thisLine = 0;
	    }
	}
	*outp++ = '\n';
	*outp = '\0';
	olen += 2;
	*outlen = olen;
	return outbuf;
}

static inline int isWhite(unsigned char c)
{
	switch(c) {
	    case '\n':
	    case '\r':
	    case ' ':
	    case '\t':
	    case '\0':
			return 1;
	    default:
			return 0;
	}
}

/*
 * Strip off all whitespace from a (supposedly) enc64-format string.
 * Returns a malloc'd string.
 */
static unsigned char *stringCleanse(const unsigned char *inbuf,
	unsigned inlen,
	unsigned *outlen)
{
	unsigned char	*news;			// cleansed inbuf
	unsigned		newsDex;		// index into news
	unsigned		i;

	news = (unsigned char*)malloc(inlen);
	newsDex = 0;
	for(i=0; i<inlen; i++) {
	    if(!isWhite(inbuf[i])) {
	        news[newsDex++] = inbuf[i];
	    }
	}
	*outlen = newsDex;
	return news;
}

/*
 * Given input buffer inbuf, length inlen, decode from 64-char IA5 format to
 * binary. Result is malloced and returned; its length is returned in *outlen.
 * NULL return indicates corrupted input.
 *
 * All whitespace in input is ignored.
 */
unsigned char *cuDec64(const unsigned char *inbuf,
	unsigned inlen,
	unsigned *outlen)
{
	unsigned char 		*outbuf;
	unsigned char 		*outp;			// malloc'd outbuf size
	unsigned 			obuflen;
	const unsigned char	*bp;
	unsigned 			olen = 0;		// actual output size
	unsigned char 		c1, c2, c3, c4;
	unsigned char 		j;
	unsigned			thisOlen;
	unsigned char		*news;			// cleansed inbuf
	unsigned			newsLen;

	/*
	 * Strip out all whitespace; remainder must be multiple of four
	 * characters
	 */
	news = stringCleanse(inbuf, inlen, &newsLen);
	if((newsLen & 0x03) != 0) {
	    free(news);
	    return (unsigned char*) NULL;
	}
	inlen = newsLen;
	bp = news;

	obuflen = (inlen / 4) * 3;
	outbuf = (unsigned char*)malloc(obuflen);
	outp = outbuf;

	while (inlen) {
	    /*
	     * Note inlen is always a multiple of four here
	     */
	    if (*bp & 0x80 || (c1 = asctobin[*bp]) & 0x80) {
	        goto errorOut;
	    }
	    inlen--;
	    bp++;
	    if (*bp & 0x80 || (c2 = asctobin[*bp]) & 0x80){
	        goto errorOut;
	    }
	    inlen--;
	    bp++;
	    if (*bp == PAD) {
			/*
			 * two input bytes, one output byte
			 */
			c3 = c4 = 0;
			thisOlen = 1;
			if (c2 & 0xf) {
				goto errorOut;
			}
			bp++;
			inlen--;
			if (*bp == PAD) {
				bp++;
				inlen--;
				if(inlen > 0) {
					goto errorOut;
				}
			}
			else {
				goto errorOut;
			}
	    } else if (*bp & 0x80 || (c3 = asctobin[*bp]) & 0x80) {
	    	goto errorOut;
	    } else {
	        bp++;
		inlen--;
		if (*bp == PAD) {
		    /*
		     * Three input bytes, two output
		     */
		    c4 = 0;
		    thisOlen = 2;
		    if (c3 & 3) {
				goto errorOut;
		    }
		} else if (*bp & 0x80 || (c4 = asctobin[*bp]) & 0x80) {
		    goto errorOut;
		} else {
		    /*
		     * Normal non-pad case
		     */
		    thisOlen = 3;
		}
		bp++;
		inlen--;
	    }
	    j = (c1 << 2) | (c2 >> 4);
	    *outp++ = j;
	    if(thisOlen > 1) {
			j = (c2 << 4) | (c3 >> 2);
			*outp++ = j;
			if(thisOlen == 3) {
				j = (c3 << 6) | c4;
				*outp++ = j;
			}
	    }
	    olen += thisOlen;
	}
	free(news);
	*outlen = olen;
	return outbuf;			/* normal return */

errorOut:
	free(news);
	free(outbuf);
	return (unsigned char*) NULL;
}

/*
 * Determine if specified input data is valid enc64 format. Returns 1
 * if valid, 0 if not.
 * This doesn't do a full enc64 parse job; it scans for legal characters
 * and proper sync when a possible pad is found.
 */
int cuIsValidEnc64(const unsigned char *inbuf,
	unsigned inlen)
{
	int padChars = 0;	// running count of PAD chars
	int validEncChars = 0;
	unsigned char c;

	/*
	 *   -- scan inbuf
	 *   -- skip whitespace
	 *   -- count valid chars
	 *   -- ensure not more than 2 PAD chars, only at end
	 *   -- ensure valid chars mod 4 == 0
	 */

	while(inlen) {
	    c = *inbuf++;
	    inlen--;
	    if(isWhite(c)) {
	        continue;
	    }
	    if(c == PAD) {
			if(++padChars > 2) {
				return 0;		// max of 2 PAD chars at end
			}
	    }
	    else if(padChars > 0) {
			return 0;		// no normal chars after seeing PAD
	    }
	    else if((c & 0x80) || ((asctobin[c]) & 0x80)) {
			return 0;		// invalid encoded char
	    }
	    validEncChars++;
	}
	if((validEncChars & 0x03) != 0) {
	    return 0;
	}
	else {
	    return 1;
	}
}

/* 
 * Text parsing routines. 
 *
 * Search incoming text for specified string. Does not assume inText is
 * NULL terminated. Returns pointer to start of found string in inText.
 */
static const char *findStr(
	const char *inText,
	unsigned inTextLen,
	const char *str)				// NULL terminated - search for this
{
	/* probably not the hottest string search algorithm... */
	const char *cp;
	unsigned srchStrLen = strlen(str);
	char c = str[0];
	
	/* last char * we can search in inText for start of str */
	const char *endCp = inText + inTextLen - srchStrLen;
	
	for(cp=inText; cp<=endCp; cp++) {
		if(*cp == c) {
			if(!memcmp(cp, str, srchStrLen)) {
				return cp;
			}
		}
	}
	return NULL;
}

/*
 * Obtain one line from current text. Returns a mallocd, NULL-terminated string
 * which caller must free(). Also returns the number of chars consumed including
 * the returned chars PLUS EOL terminators (\n and/or \r).
 *
 * ALWAYS returns a mallocd string if there is ANY data remaining per the 
 * incoming inTextLen. Returns NULL if inTextLen is zero.
 */
static const char *getLine(
	const char *inText,
	unsigned inTextLen,			// RETURNED
	unsigned *consumed)			// RETURNED
	
{
	*consumed = 0;
	const char *cp = inText;
	const char *newline = NULL;		// if we found a newline, this points to the first one
	
	while(inTextLen) {
		char c = *cp;
		if((c == '\r') || (c == '\n')) {
			if(newline == NULL) {
				/* first newline */
				newline = cp;
			}
		}
		else if(newline != NULL) {
			/* non newline after newline, done */
			break;
		}
		(*consumed)++;
		inTextLen--;
		cp++;
	}
	unsigned linelen;
	if(newline) {
		linelen = newline - inText;
	}
	else {
		linelen = *consumed;
	}
	char *rtn = (char *)malloc(linelen + 1);
	memmove(rtn, inText, linelen);
	rtn[linelen] = 0;
	return rtn;
}

#define UNSUPPORTED_FORMAT_ERR	-25256

/*
 * Given input buffer containing a PEM-encoded certificate, convert to DER
 * and return in outbuf. Result is malloced and must be freed by caller;
 * its length is returned in *outlen. Returns 0 on success.
 */
int cuConvertPem(
	const unsigned char *inbuf,
	unsigned inlen,
	unsigned char **outbuf,	// RETURNED (caller must free)
	unsigned *outlen)			// RETURNED
{
	unsigned lenToGo =  (inlen) ? inlen : 0;
	const char	 *currCp = (inbuf) ? (const char *)inbuf : NULL;
	const char *currLine = NULL;		// mallocd by getLine()
	unsigned consumed;
	int ortn = 0;
	const char *start64;
	unsigned base64Len;	
	const char *end64;
	unsigned char *decData;
	unsigned decDataLen;
	
	/* search to START line, parse it to get type/format/alg */
	const char *startLine = findStr(currCp, lenToGo, "-----BEGIN");
	if(startLine != NULL) {
		/* possibly skip over leading garbage */
		consumed = startLine - currCp;
		lenToGo -= consumed;
		currCp = startLine;
		
		/* get C string of START line */
		currLine = getLine(startLine, lenToGo, &consumed);
		if(currLine == NULL) {
			/* somehow got here with no data */
			// assert(lenToGo == 0);
			ortn = UNSUPPORTED_FORMAT_ERR;
			goto errOut;
		}
		// assert(consumed <= lenToGo);
		currCp  += consumed;
		lenToGo -= consumed;
		
		free((void *)currLine);
	}

	/* Skip empty lines.	 */
	for( ; ; ) {
		currLine = getLine(currCp, lenToGo, &consumed);
		if(currLine == NULL) {
			/* out of data */
			ortn = UNSUPPORTED_FORMAT_ERR;
			goto errOut;
		}
		int skipThis = 0;
		unsigned lineLen = strlen(currLine);
		if(lineLen == 0) {
			/* empty line */
			skipThis = 1;
		}
		free((void *)currLine);

		if(!skipThis) {
			/* looks like good stuff; process */
			break;
		}
		/* skip this line */
		// assert(consumed <= lenToGo);
		currCp  += consumed;
		lenToGo -= consumed;
	}
	if(lenToGo == 0) {
		/* no valid base64 data */
		ortn = UNSUPPORTED_FORMAT_ERR;
		goto errOut;
	}

	/* 
	 * currCP points to start of base64 data - mark it and search for end line.
	 * We skip everything after the end line.
	 */
	start64 = currCp;
	base64Len = lenToGo;			// if no END
	end64 = findStr(currCp, lenToGo, "-----END");
	if(end64 != NULL) {
		if(end64 == start64) {
			/* Empty, nothing between START and END */
			ortn = UNSUPPORTED_FORMAT_ERR;
			goto errOut;
		}
		base64Len = end64 - start64;
	}
	/* else no END, no reason to complain about that as long as base64 decode works OK */
	
	/* Base 64 decode */
	decData = cuDec64((const unsigned char *)start64, base64Len, &decDataLen);
	if(decData == NULL) {
		/* bad base64 data */
		ortn = UNSUPPORTED_FORMAT_ERR;
		goto errOut;
	}
	
	if(outlen) {
		*outlen = decDataLen;
	}
	if(outbuf) {
		*outbuf = decData;
	}
	else {
		free((void *)decData);
	}
	
errOut:
	return ortn;
}