Add command line interface (cli)

This commit is contained in:
Grégory Soutadé 2016-08-14 11:37:39 +02:00
parent 89ccd0575f
commit 0cc706d260
5 changed files with 883 additions and 0 deletions

12
cli/Makefile Normal file
View File

@ -0,0 +1,12 @@
CC=gcc
CFLAGS=-Wall -O2
LDFLAGS= -lcrypto -lcurl
TARGET=gpass_cli
SRCS=main.c ini.c
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
clean:
rm -f $(TARGET) *.o *~

6
cli/gpass.ini.sample Normal file
View File

@ -0,0 +1,6 @@
[params]
# ca_path=./ca_path/ca_authority.pem
# server=https://demo-gpass.soutade.fr/demo
# pkdbf2_level=1000
# server_port=443
# verify_ssl_peer=1

194
cli/ini.c Normal file
View File

@ -0,0 +1,194 @@
/* inih -- simple .INI file parser
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include "ini.h"
#if !INI_USE_STACK
#include <stdlib.h>
#endif
#define MAX_SECTION 50
#define MAX_NAME 50
/* Strip whitespace chars off end of given string, in place. Return s. */
static char* rstrip(char* s)
{
char* p = s + strlen(s);
while (p > s && isspace((unsigned char)(*--p)))
*p = '\0';
return s;
}
/* Return pointer to first non-whitespace char in given string. */
static char* lskip(const char* s)
{
while (*s && isspace((unsigned char)(*s)))
s++;
return (char*)s;
}
/* Return pointer to first char (of chars) or inline comment in given string,
or pointer to null at end of string if neither found. Inline comment must
be prefixed by a whitespace character to register as a comment. */
static char* find_chars_or_comment(const char* s, const char* chars)
{
#if INI_ALLOW_INLINE_COMMENTS
int was_space = 0;
while (*s && (!chars || !strchr(chars, *s)) &&
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
was_space = isspace((unsigned char)(*s));
s++;
}
#else
while (*s && (!chars || !strchr(chars, *s))) {
s++;
}
#endif
return (char*)s;
}
/* Version of strncpy that ensures dest (size bytes) is null-terminated. */
static char* strncpy0(char* dest, const char* src, size_t size)
{
strncpy(dest, src, size);
dest[size - 1] = '\0';
return dest;
}
/* See documentation in header file. */
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user)
{
/* Uses a fair bit of stack (use heap instead if you need to) */
#if INI_USE_STACK
char line[INI_MAX_LINE];
#else
char* line;
#endif
char section[MAX_SECTION] = "";
char prev_name[MAX_NAME] = "";
char* start;
char* end;
char* name;
char* value;
int lineno = 0;
int error = 0;
#if !INI_USE_STACK
line = (char*)malloc(INI_MAX_LINE);
if (!line) {
return -2;
}
#endif
/* Scan through stream line by line */
while (reader(line, INI_MAX_LINE, stream) != NULL) {
lineno++;
start = line;
#if INI_ALLOW_BOM
if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
(unsigned char)start[1] == 0xBB &&
(unsigned char)start[2] == 0xBF) {
start += 3;
}
#endif
start = lskip(rstrip(start));
if (*start == ';' || *start == '#') {
/* Per Python configparser, allow both ; and # comments at the
start of a line */
}
#if INI_ALLOW_MULTILINE
else if (*prev_name && *start && start > line) {
/* Non-blank line with leading whitespace, treat as continuation
of previous name's value (as per Python configparser). */
if (!handler(user, section, prev_name, start) && !error)
error = lineno;
}
#endif
else if (*start == '[') {
/* A "[section]" line */
end = find_chars_or_comment(start + 1, "]");
if (*end == ']') {
*end = '\0';
strncpy0(section, start + 1, sizeof(section));
*prev_name = '\0';
}
else if (!error) {
/* No ']' found on section line */
error = lineno;
}
}
else if (*start) {
/* Not a comment, must be a name[=:]value pair */
end = find_chars_or_comment(start, "=:");
if (*end == '=' || *end == ':') {
*end = '\0';
name = rstrip(start);
value = lskip(end + 1);
#if INI_ALLOW_INLINE_COMMENTS
end = find_chars_or_comment(value, NULL);
if (*end)
*end = '\0';
#endif
rstrip(value);
/* Valid name[=:]value pair found, call handler */
strncpy0(prev_name, name, sizeof(prev_name));
if (!handler(user, section, name, value) && !error)
error = lineno;
}
else if (!error) {
/* No '=' or ':' found on name[=:]value line */
error = lineno;
}
}
#if INI_STOP_ON_FIRST_ERROR
if (error)
break;
#endif
}
#if !INI_USE_STACK
free(line);
#endif
return error;
}
/* See documentation in header file. */
int ini_parse_file(FILE* file, ini_handler handler, void* user)
{
return ini_parse_stream((ini_reader)fgets, file, handler, user);
}
/* See documentation in header file. */
int ini_parse(const char* filename, ini_handler handler, void* user)
{
FILE* file;
int error;
file = fopen(filename, "r");
if (!file)
return -1;
error = ini_parse_file(file, handler, user);
fclose(file);
return error;
}

93
cli/ini.h Normal file
View File

@ -0,0 +1,93 @@
/* inih -- simple .INI file parser
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#ifndef __INI_H__
#define __INI_H__
/* Make this header file easier to include in C++ code */
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
/* Typedef for prototype of handler function. */
typedef int (*ini_handler)(void* user, const char* section,
const char* name, const char* value);
/* Typedef for prototype of fgets-style reader function. */
typedef char* (*ini_reader)(char* str, int num, void* stream);
/* Parse given INI-style file. May have [section]s, name=value pairs
(whitespace stripped), and comments starting with ';' (semicolon). Section
is "" if name=value pair parsed before any section heading. name:value
pairs are also supported as a concession to Python's configparser.
For each name=value pair parsed, call handler function with given user
pointer as well as section, name, and value (data only valid for duration
of handler call). Handler should return nonzero on success, zero on error.
Returns 0 on success, line number of first error on parse error (doesn't
stop on first error), -1 on file open error, or -2 on memory allocation
error (only when INI_USE_STACK is zero).
*/
int ini_parse(const char* filename, ini_handler handler, void* user);
/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
close the file when it's finished -- the caller must do that. */
int ini_parse_file(FILE* file, ini_handler handler, void* user);
/* Same as ini_parse(), but takes an ini_reader function pointer instead of
filename. Used for implementing custom or string-based I/O. */
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user);
/* Nonzero to allow multi-line value parsing, in the style of Python's
configparser. If allowed, ini_parse() will call the handler with the same
name for each subsequent line parsed. */
#ifndef INI_ALLOW_MULTILINE
#define INI_ALLOW_MULTILINE 1
#endif
/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
the file. See http://code.google.com/p/inih/issues/detail?id=21 */
#ifndef INI_ALLOW_BOM
#define INI_ALLOW_BOM 1
#endif
/* Nonzero to allow inline comments (with valid inline comment characters
specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
Python 3.2+ configparser behaviour. */
#ifndef INI_ALLOW_INLINE_COMMENTS
#define INI_ALLOW_INLINE_COMMENTS 1
#endif
#ifndef INI_INLINE_COMMENT_PREFIXES
#define INI_INLINE_COMMENT_PREFIXES ";"
#endif
/* Nonzero to use stack, zero to use heap (malloc/free). */
#ifndef INI_USE_STACK
#define INI_USE_STACK 1
#endif
/* Stop parsing on first error (default is to keep parsing). */
#ifndef INI_STOP_ON_FIRST_ERROR
#define INI_STOP_ON_FIRST_ERROR 0
#endif
/* Maximum line length for any line in INI file. */
#ifndef INI_MAX_LINE
#define INI_MAX_LINE 200
#endif
#ifdef __cplusplus
}
#endif
#endif /* __INI_H__ */

578
cli/main.c Normal file
View File

@ -0,0 +1,578 @@
/*
Copyright (C) 2013-2016 Grégory Soutadé
This file is part of gPass.
gPass is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gPass 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with gPass. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include <openssl/evp.h>
#include "ini.h"
#define STRNCMP(a, b) strncmp(a, b, sizeof(b)-1)
#define DEFAULT_CONFIG_FILE ".local/share/gpass/gpass.ini"
#define DEFAULT_PKDBF2_LEVEL 1000
#define MASTER_KEY_LENGTH (256/8)
#define BLOCK_SIZE (128/8)
#define DEFAULT_SERVER_PORT 443
#define SERVER_PROTOCOL 3
#define RESPONSE_SIZE 2048
#define MAX_SUBDOMAINS 10
struct gpass_parameters {
unsigned pkdbf2_level;
char *server;
char *salt;
char *domain;
char *username;
char *orig_master_key;
unsigned char *derived_master_key;
unsigned server_port;
unsigned verbose;
char *ca_path;
unsigned verify_ssl_peer;
unsigned port_set;
} ;
static void derive_master_key(struct gpass_parameters* params)
{
if (!params->derived_master_key)
params->derived_master_key = malloc(MASTER_KEY_LENGTH);
PKCS5_PBKDF2_HMAC(params->orig_master_key, strlen(params->orig_master_key),
(unsigned char*)params->salt, strlen(params->salt),
params->pkdbf2_level, EVP_sha256(),
MASTER_KEY_LENGTH, params->derived_master_key);
}
static void bin_to_hex(unsigned char* bin, unsigned char* hex, unsigned bin_size)
{
unsigned char tmp;
for (; bin_size--; bin++)
{
tmp = (*bin >> 4) & 0xf;
if (tmp <= 9)
*hex++ = '0' + tmp;
else
*hex++ = 'a' + (tmp-10);
tmp = *bin & 0xf;
if (tmp <= 9)
*hex++ = '0' + tmp;
else
*hex++ = 'a' + (tmp-10);
}
}
static void hex_to_bin(unsigned char* bin, unsigned char* hex, long hex_size)
{
unsigned char tmp;
// Round to 2
hex_size &= ~1;
for (; hex_size; hex_size-=2, bin++)
{
tmp = *hex++;
if (tmp >= '0' && tmp <= '9')
*bin = (tmp - '0') << 4;
else if (tmp >= 'a' && tmp <= 'f')
*bin = ((tmp - 'a')+10) << 4;
else
*bin = ((tmp - 'A')+10) << 4;
tmp = *hex++;
if (tmp >= '0' && tmp <= '9')
*bin |= (tmp - '0');
else if (tmp >= 'a' && tmp <= 'f')
*bin |= ((tmp - 'a')+10);
else
*bin |= ((tmp - 'A')+10);
}
}
static void encrypt_domain(struct gpass_parameters* params, char* domain,
unsigned char** res, unsigned* out_size)
{
EVP_CIPHER_CTX* evp_ctx;
unsigned size = 2+strlen(domain)+1+strlen(params->username)+1;
unsigned char* buffer, *tmp;
if (params->verbose)
printf("%s: %s\n", __func__, domain);
if ((size % BLOCK_SIZE))
size = ((size/BLOCK_SIZE)+1)*BLOCK_SIZE;
buffer = malloc(size);
memset(buffer, 0, size);
snprintf((char*)buffer, size, "@@%s;%s", domain, params->username);
tmp = malloc(size);
*res = malloc(size*2);
evp_ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit(evp_ctx, EVP_aes_256_ecb(), params->derived_master_key, NULL);
EVP_CipherUpdate(evp_ctx, tmp, (int*)out_size, buffer, size);
EVP_CIPHER_CTX_free(evp_ctx);
bin_to_hex(tmp, *res, size);
*out_size *= 2;
free(buffer);
free(tmp);
}
static void append_to_request(char** request, char* new_req)
{
static int cur_req_idx = 0;
int size_added;
if (!cur_req_idx)
{
*request = malloc(3+strlen(new_req)+1);
sprintf(*request, "k0=%s", new_req);
}
else
{
size_added = 4+strlen(new_req);
if (cur_req_idx >= 10)
size_added++;
*request = realloc(*request, strlen(*request)+1+size_added);
sprintf(&((*request)[strlen(*request)]), "&k%d=%s", cur_req_idx, new_req);
}
cur_req_idx++;
}
static char* wildcard_domain(char* domain)
{
int cur_level = 1;
char* level_ptr[MAX_SUBDOMAINS], *tmp, *res = NULL;
int level_length[MAX_SUBDOMAINS];
memset(level_ptr, 0, sizeof(level_ptr));
memset(level_length, 0, sizeof(level_length));
level_ptr[0] = domain;
for (tmp=domain; *tmp && cur_level < MAX_SUBDOMAINS; tmp++)
{
if (*tmp == '.')
{
level_ptr[cur_level] = tmp+1;
level_length[cur_level-1] = tmp - level_ptr[cur_level-1];
cur_level++;
}
}
// Too much levels
if (cur_level == MAX_SUBDOMAINS)
{
fprintf(stderr, "Error: Too much levels for domain %s\n", domain);
return NULL;
}
// Final level
level_length[cur_level] = tmp - level_ptr[cur_level-1];
tmp = NULL;
if (cur_level >= 3)
{
// Seems to be a two level root domain (ie zzz.xxx.co.uk ...)
if (level_length[cur_level-2] <= 3)
{
if (cur_level > 3)
tmp = level_ptr[cur_level-3];
}
else
// Standard root domain (zzz.xxx.com)
tmp = level_ptr[cur_level-2];
}
// Simple xxx.com
else if (cur_level == 2)
tmp = level_ptr[0];
if (tmp)
{
res = malloc(2+strlen(tmp)+1);
sprintf(res, "*.%s", tmp);
}
return res;
}
static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata)
{
if ((size*nmemb) > RESPONSE_SIZE)
{
fprintf(stderr, "Error curl response is too big (%d bytes, max %d bytes)\n",
(int)(size*nmemb), RESPONSE_SIZE);
}
else
memcpy(userdata, ptr, size*nmemb);
return size*nmemb;
}
static int ask_server(struct gpass_parameters* params)
{
char* wc_domain, *saveptr, *token, *cur_ptr;
unsigned char* enc_domain;
unsigned enc_size;
char* request = NULL;
int ret = -1, res;
CURL *curl;
EVP_CIPHER_CTX* evp_ctx;
char response[RESPONSE_SIZE];
unsigned char password[256];
encrypt_domain(params, params->domain, &enc_domain, &enc_size);
append_to_request(&request, (char*)enc_domain);
free(enc_domain);
wc_domain = wildcard_domain(params->domain);
if (wc_domain)
{
encrypt_domain(params, wc_domain, &enc_domain, &enc_size);
append_to_request(&request, (char*)enc_domain);
free(enc_domain);
}
if (params->verbose)
printf("Request: %s\n", request);
curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, params->server);
curl_easy_setopt(curl, CURLOPT_PORT, params->server_port);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, params->verify_ssl_peer);
if (params->ca_path)
curl_easy_setopt(curl, CURLOPT_CAINFO, params->ca_path);
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request);
if (params->verbose)
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)response);
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK)
{
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
goto end;
}
token = strtok_r(response, "\n", &saveptr);
while (token)
{
if (params->verbose)
printf("Parse %s\n", token);
cur_ptr = token;
if (!strcmp(token, "<end>"))
break;
else if (!STRNCMP(token, "protocol"))
{
cur_ptr += sizeof("protocol"); // includes "="
if (STRNCMP(cur_ptr, "gpass-"))
{
fprintf(stderr, "Error: Unknown server protocol %s\n", token);
break;
}
else
{
cur_ptr += sizeof("gpass-")-1;
if (atoi(cur_ptr) > SERVER_PROTOCOL)
{
fprintf(stderr, "Error: Cannot handle server protocol %s\n", token);
break;
}
}
}
else if (!STRNCMP(token, "pass"))
{
cur_ptr += sizeof("pass"); // includes "="
if ((strlen(cur_ptr)/2) > sizeof(password))
{
fprintf(stderr, "Error: retrieved password is too big !\n");
goto end;
}
hex_to_bin(password, (unsigned char*)cur_ptr, strlen(cur_ptr));
evp_ctx = EVP_CIPHER_CTX_new();
EVP_DecryptInit(evp_ctx, EVP_aes_256_ecb(), params->derived_master_key, NULL);
EVP_CipherUpdate(evp_ctx, password, &res, password, strlen(cur_ptr)/2);
EVP_CIPHER_CTX_free(evp_ctx);
// Remove salt
password[strlen((char*)password)-3] = 0;
printf("Password found: %s\n", password);
ret = 0;
goto end;
}
else if (!STRNCMP(token, "pkdbf2_level"))
{
cur_ptr += sizeof("pkdbf2_level"); // includes "="
if (atoi(cur_ptr) != params->pkdbf2_level)
{
params->pkdbf2_level = atoi(cur_ptr);
ret = 1;
break;
}
}
else
{
fprintf(stderr, "Error: Unknown server response %s\n", token);
break;
}
token = strtok_r(NULL, "\n", &saveptr);
}
if (ret)
printf("Password not found\n");
end:
free(request);
return ret;
}
static void init_parameters(struct gpass_parameters* params)
{
memset (params, 0, sizeof(*params));
params->pkdbf2_level = DEFAULT_PKDBF2_LEVEL;
params->server_port = DEFAULT_SERVER_PORT;
params->verify_ssl_peer = 1;
}
static void release_parameters(struct gpass_parameters* params)
{
if (params->server) free(params->server);
if (params->salt) free(params->salt);
if (params->domain) free(params->domain);
if (params->username) free(params->username);
if (params->orig_master_key) free(params->orig_master_key);
if (params->derived_master_key) free(params->derived_master_key);
if( params->ca_path) free(params->ca_path);
}
static int check_parameters(struct gpass_parameters* params)
{
if (!params->server)
{
fprintf(stderr, "Error: server not set\n");
return 1;
}
if (!params->domain)
{
fprintf(stderr, "Error: gpass domain not set\n");
return 1;
}
if (!params->username)
{
fprintf(stderr, "Error: username not set\n");
return 1;
}
return 0;
}
static int gpass_ini_handler(void* user, const char* section,
const char* name, const char* value)
{
struct gpass_parameters* params = (struct gpass_parameters*) user;
if (!STRNCMP(name, "ca_path"))
{
if (params->ca_path) free(params->ca_path);
params->ca_path = strdup(value);
}
else if (!STRNCMP(name, "pkdbf2_level"))
params->pkdbf2_level = atoi(value);
else if (!STRNCMP(name, "verify_ssl_peer"))
params->verify_ssl_peer = atoi(value);
else if (!STRNCMP(name, "server_port"))
{
params->server_port = atoi(value);
params->port_set = 1;
}
else if (!STRNCMP(name, "server"))
{
if (params->server) free(params->server);
params->server = strdup(value);
}
else
fprintf(stderr, "Error: Unknown key '%s' in config file\n", name);
return 1;
}
static void usage(char* program_name)
{
fprintf(stderr, "Usage: %s [-f config_file] [-p server_port] [-c CA_certificate_path] [-l PKDBF2_level] [-s gpass_server] [-v] -d domain -u username\n",
program_name);
exit(EXIT_FAILURE);
}
int main(int argc, char** argv)
{
struct gpass_parameters params;
int opt, ret = 0;
char* tmp;
char* config_file, *home;
if (argc == 1)
usage(argv[0]);
init_parameters(&params);
home = getenv("HOME");
if (home)
{
config_file = malloc(strlen(home)+1+sizeof(DEFAULT_CONFIG_FILE));
sprintf(config_file, "%s/" DEFAULT_CONFIG_FILE, home);
ini_parse(config_file, gpass_ini_handler, &params);
free(config_file);
}
while ((opt = getopt(argc, argv, "c:d:f:l:np:s:u:vh")) != -1) {
switch (opt) {
case 'c':
if (params.ca_path) free(params.ca_path);
params.ca_path = strdup(optarg);
break;
case 'd':
if (params.domain) free(params.domain);
params.domain = strdup(optarg);
break;
case 'f':
ini_parse(optarg, gpass_ini_handler, &params);
break;
case 'l':
params.pkdbf2_level = atoi(optarg);
break;
case 'n':
params.verify_ssl_peer = 0;
break;
case 'p':
params.server_port = atoi(optarg);
params.port_set = 1;
break;
case 's':
if (params.server) free(params.server);
params.server = strdup(optarg);
break;
case 'u':
if (params.username) free(params.username);
params.username = strdup(optarg);
break;
case 'v':
params.verbose++;
break;
case 'h':
case '?':
default: /* '?' */
usage(argv[0]);
}
}
ret = check_parameters(&params);
if (ret)
goto end;
// Manage server, server_port and salt
if (!STRNCMP(params.server, "http://"))
{
if (!params.port_set)
params.server_port = 80;
params.salt = strdup(&params.server[7]);
}
else if (!STRNCMP(params.server, "https://"))
{
if (!params.port_set)
params.server_port = 443;
params.salt = strdup(&params.server[8]);
}
// Manage domain
if (!STRNCMP(params.domain, "http://"))
{
tmp = strdup(&params.domain[7]);
free(params.domain);
params.domain = tmp;
}
else if (!STRNCMP(params.domain, "https://"))
{
tmp = strdup(&params.domain[8]);
free(params.domain);
params.domain = tmp;
}
// Remove query part of domain (a.com[/XXXX])
for (tmp=params.domain; *tmp; tmp++)
{
if (*tmp == '/')
{
*tmp = 0;
break;
}
}
// Let's go
tmp = getpass("Enter master key: ");
if (!tmp)
goto end;
params.orig_master_key = strdup(tmp);
derive_master_key(&params);
ret = ask_server(&params);
// try again with new parameters
if (ret > 0)
{
derive_master_key(&params);
ask_server(&params);
}
end:
release_parameters(&params);
return ret;
}