blob: 0a7636d2b99ab083d7f1cf3d52332553632cc857 [file] [log] [blame]
// Copyright (C) 2010 David Sugar, Tycho Softworks.
//
// This file is part of GNU uCommon C++.
//
// GNU uCommon C++ is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU uCommon C++ 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. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with GNU uCommon C++. If not, see <http://www.gnu.org/licenses/>.
#include <ucommon/secure.h>
#include <sys/stat.h>
using namespace UCOMMON_NAMESPACE;
static shell::flagopt helpflag('h',"--help", _TEXT("display this list"));
static shell::flagopt althelp('?', NULL, NULL);
static shell::stringopt tag('t', "--tag", _TEXT("tag annotation"), "text", "");
static shell::stringopt algo('c', "--cipher", _TEXT("cipher method (aes256)"), "method", "aes256");
static shell::flagopt decode('d', "--decode", _TEXT("decode archive"));
static shell::stringopt hash('h', "--digest", _TEXT("digest method (sha)"), "method", "sha");
static shell::flagopt noheader('n', "--no-header", _TEXT("without wrapper"));
static shell::stringopt out('o', "--output", _TEXT("output file"), "filename", "-");
static shell::flagopt quiet('q', "--quiet", _TEXT("quiet operation"));
static shell::flagopt recursive('R', "--recursive", _TEXT("recursive directory scan"));
static shell::flagopt altrecursive('r', NULL, NULL);
static shell::flagopt hidden('s', "--hidden", _TEXT("include hidden files"));
static shell::flagopt yes('y', "--overwrite", _TEXT("overwrite existing files"));
static bool binary = false;
static int exit_code = 0;
static const char *argv0 = "car";
static unsigned char frame[48], cbuf[48];
static cipher_t cipher;
static FILE *output = stdout;
static enum {d_text, d_file, d_scan, d_init} decoder = d_init;
static unsigned frames;
static void report(const char *path, int code)
{
const char *err = _TEXT("i/o error");
switch(code) {
case EACCES:
case EPERM:
err = _TEXT("permission denied");
break;
case EROFS:
err = _TEXT("read-only file system");
break;
case ENODEV:
case ENOENT:
err = _TEXT("no such file or directory");
break;
case ENOTDIR:
err = _TEXT("not a directory");
break;
case ENOTEMPTY:
err = _TEXT("directory not empty");
break;
case ENOSPC:
err = _TEXT("no space left on device");
break;
case EBADF:
case ENAMETOOLONG:
err = _TEXT("bad file path");
break;
case EBUSY:
case EINPROGRESS:
err = _TEXT("file or directory busy");
break;
case EINTR:
err = _TEXT("operation interupted");
break;
case EISDIR:
err = _TEXT("is a directory");
break;
#ifdef ELOOP
case ELOOP:
err = _TEXT("too many sym links");
break;
#endif
}
if(path)
shell::printf("%s: %s: %s\n", argv0, path, err);
else
shell::errexit(1, "*** %s: %s\n", argv0, err);
exit_code = 1;
}
static bool encode(const char *path, FILE *fp, size_t offset = 0)
{
memset(frame, 0, sizeof(frame));
size_t count = fread(frame + offset, 1, sizeof(frame) - offset, fp);
char buffer[128];
if(ferror(fp)) {
report(path, errno);
return false;
}
if(count)
count += offset;
// add padd value for last frame...
if(count < sizeof(frame))
frame[sizeof(frame) - 1] = (char)(count - offset);
// cipher it...
size_t encoded = cipher.put(frame, sizeof(frame));
if(encoded != sizeof(frame)) {
report(path, EINTR);
return false;
}
if(binary)
fwrite(cbuf, sizeof(cbuf), 1, output);
else {
String::b64encode(buffer, cbuf, 48);
fprintf(output, "%s\n", buffer);
}
// return status
if(count == sizeof(frame))
return true;
return false;
}
static void encodestream(void)
{
size_t offset = 6;
if(fsys::is_tty(shell::input()))
fputs("car: type your message\n", stderr);
memset(frame, 0, sizeof(frame));
for(;;) {
if(!encode("-", stdin, offset))
break;
offset = 0;
}
if(!binary && !is(noheader))
fprintf(output, "-----END CAR STREAM-----\n");
}
static void header(void)
{
binary = true;
memset(frame, 0, sizeof(frame));
String::set((char *)frame, 6, ".car");
frame[5] = 1;
frame[4] = 0xff;
String::set((char *)frame + 6, sizeof(frame) - 6, *tag);
fwrite(frame, sizeof(frame), 1, output);
}
static void encodefile(const char *path, const char *name)
{
char buffer[128];
fsys::fileinfo_t ino;
fsys::info(path, &ino);
FILE *fp = fopen(path, "r");
if(!fp) {
report(name, errno);
return;
}
lsb_setlong(frame, ino.st_size);
frame[4] = 1;
frame[5] = 0;
String::set((char *)(frame + 6), sizeof(frame) - 6, name);
cipher.put(frame, sizeof(frame));
if(binary)
fwrite(cbuf, sizeof(cbuf), 1, output);
else {
String::b64encode(buffer, cbuf, 48);
fprintf(output, "%s\n", buffer);
}
for(;;) {
if(!encode(name, fp))
break;
}
}
static void final(void)
{
size_t size;
switch(decoder) {
case d_init:
return;
case d_scan:
cipher.put(frame, sizeof(frame));
if(cbuf[4] > 0)
return;
size = cbuf[sizeof(frame) - 1];
if(size)
fwrite(cbuf + 6, size, 1, output);
break;
default:
decoder = d_scan;
cipher.put(frame, sizeof(frame));
size = cbuf[sizeof(frame) - 1];
if(size)
fwrite(cbuf, size, 1, output);
}
}
static void process(void)
{
string_t path;
int key;
char *cp;
switch(decoder) {
case d_init:
decoder = d_scan;
return;
case d_scan:
cipher.put(frame, sizeof(frame));
if(cbuf[4] == 0xff) // header...
break;
if(!cbuf[4]) { // if message
fwrite(cbuf + 6, sizeof(frame) - 6, 1, output);
decoder = d_text;
break;
}
decoder = d_file;
frames = lsb_getlong(cbuf) / sizeof(frame);
path = str((char *)(cbuf + 6));
cp = strrchr((char *)(cbuf + 6), '/');
if(cp) {
*cp = 0;
dir::create((char *)(cbuf + 6), 0640);
}
if(fsys::is_dir(*path))
shell::errexit(8, "*** %s: %s: %s\n",
argv0, *path, _TEXT("output is directory"));
if(fsys::is_file(*path) && !is(yes)) {
string_t prompt = str("overwrite ") + path + " <y/n>? ";
if(is(quiet))
key = 0;
else
key = shell::inkey(prompt);
switch(key)
{
case 'y':
case 'Y':
printf("y\n");
break;
default:
printf("n\n");
case 0:
shell::errexit(8, "*** %s: %s: %s\n",
argv0, *path, _TEXT("will not overwrite"));
}
}
output = fopen(*path, "w");
if(!output)
shell::errexit(8, "*** %s: %s: %s\n",
argv0, *path, _TEXT("cannot create"));
if(!is(quiet))
printf("decoding %s...\n", *path);
break;
case d_file:
if(!frames) {
final();
break;
}
--frames;
default:
cipher.put(frame, sizeof(frame));
fwrite(cbuf, sizeof(frame), 1, output);
}
}
static void binarydecode(FILE *fp, const char *path)
{
char buffer[48];
memset(frame, 0, sizeof(frame));
memset(buffer, 0, sizeof(buffer));
if(fread(frame, sizeof(frame), 1, fp) < 1)
shell::errexit(6, "*** %s: %s: %s\n",
argv0, path, _TEXT("cannot read archive"));
if(!eq((char *)frame, ".car", 4) || (frame[4] != 0xff))
shell::errexit(6, "*** %s: %s: %s\n",
argv0, path, _TEXT("not a cryptographic archive"));
for(;;) {
if(feof(fp)) {
final();
return;
}
if(ferror(fp)) {
exit_code = errno;
report(path, exit_code);
return;
}
if(fread(buffer, sizeof(buffer), 1, fp) < 1)
continue;
process();
memcpy(frame, buffer, sizeof(frame));
}
}
static void streamdecode(FILE *fp, const char *path)
{
char buffer[128];
for(;;) {
if(NULL == fgets(buffer, sizeof(buffer), fp) || feof(fp)) {
shell::errexit(5, "*** %s: %s: %s\n",
argv0, path, _TEXT("no archive found"));
}
if(ferror(fp)) {
exit_code = errno;
report(path, exit_code);
return;
}
if(eq("-----BEGIN CAR STREAM-----\n", buffer))
break;
}
for(;;) {
if(feof(fp)) {
final();
return;
}
if(ferror(fp)) {
exit_code = errno;
report(path, exit_code);
return;
}
if(fgets(buffer, sizeof(buffer), fp) == NULL)
continue;
if(eq("-----END CAR STREAM-----\n", buffer)) {
final();
return;
}
// ignore extra headers...
if(strstr(buffer, ": "))
continue;
process();
unsigned len = String::b64decode(frame, buffer, 48);
if(len < 48) {
report(path, EINTR);
return;
}
}
}
static void scan(string_t path, string_t prefix)
{
char filename[128];
string_t filepath;
string_t name;
string_t subdir;
dir_t dir(path);
while(is(dir) && dir.read(filename, sizeof(filename))) {
if(*filename == '.' && (filename[1] == '.' || !filename[1]))
continue;
if(*filename == '.' && !is(hidden))
continue;
filepath = str(path) + str("/") + str(filename);
if(prefix[0])
name ^= prefix + str("/") + str(filename);
else
name ^= str(filename);
if(fsys::is_dir(filepath)) {
if(is(recursive) || is(altrecursive))
scan(filepath, name);
else
report(*filepath, EISDIR);
}
else
encodefile(*filepath, *name);
}
}
PROGRAM_MAIN(argc, argv)
{
shell::bind("car");
shell args(argc, argv);
argv0 = args.argv0();
unsigned count = 0;
char passphrase[256];
char confirm[256];
const char *ext;
argv0 = args.argv0();
if(is(helpflag) || is(althelp)) {
printf("%s\n", _TEXT("Usage: car [options] path..."));
printf("%s\n\n", _TEXT("Crytographic archiver"));
printf("%s\n", _TEXT("Options:"));
shell::help();
printf("\n%s\n", _TEXT("Report bugs to dyfet@gnu.org"));
PROGRAM_EXIT(0);
}
if(!secure::init())
shell::errexit(1, "*** %s: %s\n", argv0, _TEXT("not supported"));
if(!Digest::has(*hash))
shell::errexit(2, "*** %s: %s: %s\n",
argv0, *hash, _TEXT("unkown or unsupported digest method"));
if(!Cipher::has(*algo))
shell::errexit(2, "*** %s: %s: %s\n",
argv0, *algo, _TEXT("unknown or unsupported cipher method"));
shell::getpass("passphrase: ", passphrase, sizeof(passphrase));
shell::getpass("confirm: ", confirm, sizeof(confirm));
if(!eq(passphrase, confirm))
shell::errexit(3, "*** %s: %s\n",
argv0, _TEXT("passphrase does not match confirmation"));
// set key and cleanup buffers...
skey_t key(*algo, *hash, passphrase);
memset(passphrase, 0, sizeof(passphrase));
memset(confirm, 0, sizeof(confirm));
memset(cbuf, 0, sizeof(cbuf));
if(is(decode))
cipher.set(&key, Cipher::DECRYPT, cbuf, sizeof(cbuf));
else
cipher.set(&key, Cipher::ENCRYPT, cbuf, sizeof(cbuf));
if(is(decode) && !args()) {
streamdecode(stdin, "-");
goto end;
}
if(is(decode) && args() > 1)
shell::errexit(3, "*** %s: %s\n", argv0, _TEXT("specify only one file for decoder"));
if(is(decode)) {
FILE *fp = fopen(args[0], "r");
if(!fp)
shell::errexit(7, "*** %s: %s: %s\n",
argv0, args[0], _TEXT("cannot open or access"));
const char *ext = strrchr(args[0], '.');
if(eq_case(ext, ".car"))
binarydecode(fp, args[0]);
else
streamdecode(fp, args[0]);
goto end;
}
if(!eq(*out, "-")) {
output = fopen(*out, "w");
if(!output) {
shell::errexit(4, "*** %s: %s: %s\n",
argv0, *out, _TEXT("cannot create"));
}
}
// if we are outputting to a car file, do it in binary
ext = strrchr(*out, '.');
if(eq_case(ext, ".car"))
header();
if(!binary && !is(noheader)) {
fprintf(output, "-----BEGIN CAR STREAM-----\n");
if(tag)
fprintf(output, "Tag: %s\n", *tag);
}
if(!args()) {
encodestream();
goto end;
}
while(count < args()) {
if(fsys::is_dir(args[count]))
scan(str(args[count++]), "");
else {
const char *cp = args[count++];
const char *ep = strrchr(cp, '/');
if(!ep)
ep = strrchr(cp, '\\');
if(ep)
++ep;
else
ep = cp;
encodefile(cp, ep);
}
}
if(!binary && !is(noheader))
fprintf(output, "-----END CAR STREAM-----\n");
end:
PROGRAM_EXIT(exit_code);
}