Commit 0407fc5d authored by Joel Copi's avatar Joel Copi
Browse files

Focused on PC application first

parent e9a2c7ca
......@@ -62,3 +62,7 @@ typings/
*/build/*
.vscode/*
# No need to include libsodium in this repo
libsodium-stable/
a.out
\ No newline at end of file
This diff is collapsed.
# passmngr
Data will be organized into seperate files:
- A `keys` file will hold encryption keys for decrypting other files. The keys file will have a salt attached somewhere in the meta data that will be used to create the master encryption key from the user password. This will be verified as associated data in the AEAD used to secure the keys file.
- An encrypted `accounts` file will hold information on user accounts. This will include domains/app identifiers, usernames, passwords and other meta data to facilitate searching.
- An encrypted `settings` file will hold user preferences. password timeout settings, master password keygen parameters, etc.
The initial MVP will not use libsodium allocations, this will bring the risk of sensitive data being paged to RAM, testing will be done to determine the best way to utilize the limited amount of non-paged ram available.
\ No newline at end of file
#include "passmngr.h"
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <sys/resource.h>
#include <sys/time.h>
#define PASSWORD_CHUNK_SIZE (32)
#define DEFAULT_CHUNK_SIZE (8192)
#if defined(__WIN_32) || defined(__WIN_64)
#include <conio.h>
#else
#include <termios.h>
#include <unistd.h>
int getch() {
struct termios o_term, n_term;
int ch;
if (tcgetattr(STDIN_FILENO, &o_term) != 0) {
return -1;
}
n_term = o_term;
n_term.c_lflag &= ~(ICANON | ECHO);
if (tcsetattr(STDIN_FILENO, TCSANOW, &n_term) != 0) {
return -1;
}
ch = getchar();
if (tcsetattr(STDIN_FILENO, TCSANOW, &o_term) != 0) {
return -1;
}
return ch;
}
#endif
#include <sodium.h>
int main (int argc, char** argv) {
/* The sequence for initializing the password manager is:
- Init Sodium
- Allocate a locked buffer for the password and master encryption key
- read the password into guarded allocation and g
- all data will be encrypted (chunked) with crypto_secretstream with a configurable rekey period
- The data will be stored in one large file
*/
// These items are sensitive and need to be put in guarded allocations
unsigned char* password_buffer = NULL;
unsigned char* master_key = NULL;
unsigned char** file_keys = NULL;
// This is not a sensitive allocation, it can be allocated with calloc, etc.
char** file_names = NULL;
unsigned char salt[crypto_pwhash_SALTBYTES];
size_t password_buffer_size = PASSWORD_CHUNK_SIZE;
size_t password_length = 0;
size_t master_key_size = crypto_secretstream_xchacha20poly1305_KEYBYTES;
int errcode = 0;
//struct rlimit limits;
// Initialize libsodium, failure here is an unrecoverable error
if (sodium_init() != 0) {
printf("An error occured starting libsodium, exiting...\n");
return 1;
}
password_buffer = sodium_allocarray(password_buffer_size, sizeof (unsigned char));
master_key = sodium_allocarray(master_key_size, sizeof (unsigned char));
if (password_buffer == NULL || master_key == NULL) {
fprintf(stderr, "An error occured allocating locked memory, exiting...\n");
errcode = 1;
goto cleanup;
}
fprintf(stdout, "Password: ");
for (;;) {
int ch = getch();
if (ch < 0) {
fprintf(stderr, "An error occured reading user password, exiting...\n");
errcode = 1;
goto cleanup;
} else if (ch == '\n') {
break;
} else {
if (password_length >= password_buffer_size) {
size_t new_password_buffer_size = password_buffer_size + PASSWORD_CHUNK_SIZE;
char* tmp = sodium_allocarray(new_password_buffer_size, sizeof (unsigned char));
if (tmp == NULL) {
fprintf(stderr, "An error occured allocating locked memory, exiting...\n");
errcode = 1;
goto cleanup;
}
memcpy(tmp, password_buffer, password_buffer_size);
sodium_free(password_buffer);
password_buffer = tmp;
password_buffer_size = new_password_buffer_size;
}
password_buffer[password_length] = (unsigned char)ch;
password_length += 1;
}
}
randombytes_buf(salt, sizeof salt);
// The password needs to be turned into the master key
if (crypto_pwhash(master_key, master_key_size, password_buffer, password_length, salt,
crypto_pwhash_OPSLIMIT_SENSITIVE, crypto_pwhash_MEMLIMIT_SENSITIVE, crypto_pwhash_ALG_ARGON2ID13) != 0) {
fprintf(stderr, "An error occured generating the master key, exiting...\n");
errcode = 1;
goto cleanup;
}
// The password is no longer needed it can be removed from memory
sodium_free(password_buffer);
password_buffer = NULL;
password_length = 0;
password_buffer_size = 0;
printf("Generated Key: ");
for (int i = 0; i < master_key_size; i++) {
printf("%x ", master_key[i]);
}
printf("\n");
cleanup:
sodium_free(password_buffer);
sodium_free(master_key);
sodium_free(file_keys);
free(file_names);
return errcode;
}
#ifndef PASSMNGR_H
#define PASSMNGR_H
// Functions for setting up a terminal for password input
// As a password manager it is important to get password input correct
//int setup_password_input(int stream);
//int teardown_password_input(int stream);
//int read_password(int stream, size_t max, char* pwd_buffer);
enum state {
IDLE,
VAULT_OPEN,
VAULT_UNLOCKED
}
struct passmngr {
enum state state;
char* vault_name;
}
#endif
\ No newline at end of file
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<style>
</style>
</head>
<body>
<script src="build/utils.js" type="module"></script>
<script src="test.js" type="module"></script>
</body>
</html>
\ No newline at end of file
import {passmngr} from "./build/utils.js"
async function symmetricTest (keys) {
let toEncrypt = "The quick brown fox jumps over the lazy dog.";
try {
let encrypted = await passmngr.crypto.symmetric.encrypt(keys, utils.stringEncoding.toUTF8(toEncrypt));
let decrypted = await passmngr.crypto.symmetric.decrypt(keys, encrypted);
if (toEncrypt !== passmngr.stringEncoding.fromUTF8(decrypted)) {
return false;
}
} catch (ex) {
return false;
}
try {
let encrypted = await passmngr.crypto.symmetric.encrypt(keys, passmngr.stringEncoding.toUTF8(toEncrypt));
// Modifying the ciphertext should result in a null return on decryption
let encArr = new Uint8Array(encrypted);
encArr[Math.floor(Math.random() * encArr.length)] ^= 0b00010000;
encrypted = encArr.buffer;
let decrypted = await passmngr.crypto.symmetric.decrypt(keys, encrypted);
if (decrypted !== null) {
return false;
}
} catch (ex) {
return false;
}
return true;
}
async function asymSigningTest (keys) {
let toSign = passmngr.stringEncoding.toUTF8("The quick brown fox jumps over the lazy dog.");
try {
let signature = await passmngr.crypto.asymmetric.sign(keys, toSign);
let verified = await passmngr.crypto.asymmetric.verify(keys, signature, toSign);
if (!verified) {
return false;
}
} catch (ex) {
return false;
}
try {
let signature = await passmngr.crypto.asymmetric.sign(keys, toSign);
let modifiedToSign = passmngr.stringEncoding.toUTF8("The quick brovn fox jumps over the lazy dog.");
let verified = await passmngr.crypto.asymmetric.verify(keys, signature, modifiedToSign);
if (verified) {
return false;
}
} catch (ex) {
return false;
}
return true;
}
async function asymKeyExchangeTest (aliceKeys, bobKeys) {
let aComputedKeys, bComputedKeys;
try {
let aPublicMessage = await passmngr.crypto.asymmetric.keyExchangeMarshallPublic(aliceKeys);
let bPublicMessage = await passmngr.crypto.asymmetric.keyExchangeMarshallPublic(bobKeys);
let aKnowledgeOfB = await passmngr.crypto.asymmetric.keyExchangeUnmarshallPublic(aliceKeys.algo, bPublicMessage);
let bKnowledgeOfA = await passmngr.crypto.asymmetric.keyExchangeUnmarshallPublic(bobKeys.algo, aPublicMessage);
aComputedKeys = await passmngr.crypto.asymmetric.keyExchangeDeriveSymmetricKeys(passmngr.cryptoAlgos.AES_CBC_256_HMAC_SHA384, aliceKeys, aKnowledgeOfB);
bComputedKeys = await passmngr.crypto.asymmetric.keyExchangeDeriveSymmetricKeys(passmngr.cryptoAlgos.AES_CBC_256_HMAC_SHA384, bobKeys, bKnowledgeOfA);
} catch (ex) {
return false;
}
try {
let toEncrypt = "The quick brown fox jumped over the lazy dog.";
let encrypted = await passmngr.crypto.symmetric.encrypt(aComputedKeys, utils.stringEncoding.toUTF8(toEncrypt));
let decrypted = await passmngr.crypto.symmetric.decrypt(bComputedKeys, encrypted);
if (toEncrypt !== passmngr.stringEncoding.fromUTF8(decrypted)) {
return false;
}
} catch (ex) {
return false;
}
return true;
}
function entropyEncodeTest (obj) {
let plainString = JSON.stringify(obj);
let plainBuffer = passmngr.stringEncoding.toUTF8(plainString);
let encoded = passmngr.compress.entropyEncode(passmngr.compress.jsonEntropyModel, plainBuffer);
let decoded = passmngr.compress.entropyDecode(passmngr.compress.jsonEntropyModel, encoded);
let decodedString = passmngr.stringEncoding.fromUTF8(decoded);
if (decodedString === plainString) {
console.log("ENTROPY ENCODING: PASS");
console.log(" : " + (100 - (100 * (encoded.byteLength / plainBuffer.byteLength))).toFixed(2) + "%");
}
}
async function test () {
console.log("Testing crypto library");
for (let k in passmngr.cryptoAlgos) {
let a = passmngr.cryptoAlgos[k];
if (!Number.isInteger(a)) continue;
if (a & passmngr.calg.AES) {
// Perform symmetric encryption tests
// Keygen from password -> encrypt -> decrypt -> encrypt & modify -> fail to decrypt
// Keygen from random -> encrypt -> decrypt -> encrypt & modify -> fail to decrypt
let salt = passmngr.crypto.primitives.randomBits(passmngr.crypto.keygen.defaultAESLength);
let keys = await passmngr.crypto.keygen.fromPassword("ThisIsAPasswordToTest", a, salt, 10);
let fromPwd = await symmetricTest(keys);
keys = await passmngr.crypto.keygen.fromRandom(a);
let fromRnd = await symmetricTest(keys);
console.log(passmngr.cryptoAlgos[a] + " [Password] : " + (fromPwd ? "PASS" : "FAIL"));
console.log(" " + " [Random] : " + (fromRnd ? "PASS" : "FAIL"));
} else if (a & passmngr.calg.ECDSA) {
// Perform asymmetric signing tests
// Keygen from random -> sign -> verify -> sign & modify -> fail to verify
let keys = await passmngr.crypto.keygen.fromRandom(a);
let fromRnd = await asymSigningTest(keys);
console.log(passmngr.cryptoAlgos[a] + " [Random] : " + (fromRnd ? "PASS" : "FAIL"));
} else if (a & passmngr.calg.ECDHE) {
// Perform key exchange tests
// Keygen from random -> verify 2 local entites derive the same symmetric keys
let aliceKeys = await passmngr.crypto.keygen.fromRandom(a);
let bobKeys = await passmngr.crypto.keygen.fromRandom(a);
let fromRnd = await asymKeyExchangeTest(aliceKeys, bobKeys);
console.log(passmngr.cryptoAlgos[a] + " [Random] : " + (fromRnd ? "PASS" : "FAIL"));
}
}
}
let x = {
"compilerOptions":{
"target" : "es6",
"lib" : ["dom", "es6"],
"declaration": true,
"outDir" : "./build"
},
"files": [
"./utils.ts"
]
};
let y = {
"options": {"failByDrop": false},
"outdir": "./reports/clients",
"servers": [
{"agent": "ReadAllWriteMessage", "url": "ws://localhost:9000/m", "options": {"version": 18}},
{"agent": "ReadAllWritePreparedMessage", "url": "ws://localhost:9000/p", "options": {"version": 18}},
{"agent": "ReadAllWrite", "url": "ws://localhost:9000/r", "options": {"version": 18}},
{"agent": "CopyFull", "url": "ws://localhost:9000/f", "options": {"version": 18}},
{"agent": "CopyWriterOnly", "url": "ws://localhost:9000/c", "options": {"version": 18}}
],
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
};
entropyEncodeTest(x);
entropyEncodeTest(y);
// init();
test();
window.utils = passmngr;
\ No newline at end of file
{
"compilerOptions":{
"target" : "es6",
"lib" : ["dom", "es6"],
"declaration": true,
"outDir" : "./build"
},
"files": [
"./utils.ts"
]
}
\ No newline at end of file
This diff is collapsed.
if (!browser) var browser = chrome;
var application = {
state: null,
keysets: {}
}
browser.runtime.onMessage.addListener(function (message, sender, sendMessage) {
});
\ No newline at end of file
class strings {
static toUTF8 (string) {
// Input validation
if (typeof string !== "string") {
throw "Invalid input. 1st argument of strings.toUTF8 must be a string";
}
let tmp = [];
for (let i = 0, j = 0; i < string.length; ++i) {
let cp = string.codePointAt(i);
if (cp <= 0x0000007F) {
tmp.push(cp);
} else if (cp <= 0x000007FF) {
tmp.push(0xC0 | ((cp & 0x7C0) >> 6));
tmp.push(0x80 | (cp & 0x3F))
} else if (cp <= 0x0000FFFF) {
tmp.push(0xE0 | ((cp & 0xF000) >> 12));
tmp.push(0x80 | ((cp & 0xFC0) >> 6));
tmp.push(0x80 | (cp & 0x3F));
} else if (cp <= 0x001FFFFF) {
tmp.push(0xF0 | ((cp & 0x1C0000) >> 18));
tmp.push(0x80 | ((cp & 0x3F000) >> 12));
tmp.push(0x80 | ((cp & 0xFC0) >> 6));
tmp.push(0x80 | (cp & 0x3F));
}
}
let bytes = new Uint8Array(tmp);
return bytes.buffer;
};
static fromUTF8 (bytes) {
// Input validation
if (!(bytes instanceof ArrayBuffer || ArrayBuffer.isView(bytes))) {
throw "Invalid input. 1st argument of string.fromUTF8 must be an ArrayBuffer";
}
let result = "";
let u8 = new Uint8Array(buffer);
for (let i = 0; i < u8.length; ++i) {
if (u8[i] <= 0x7F) {
result += String.fromCodePoint(u8[i]);
} else if ((u8[i] >> 5) == 0x6) {
// The next bytes should be part of this Unicode Code Point
if (u8.length < (i + 2))
break;
if ((u8[i+1] >> 6) == 0x2) {
let cp = ((u8[i] & 0x1F) << 6) | (u8[i+1] & 0x3F);
result += String.fromCodePoint(cp);
++i;
}
} else if ((u8[i] >> 4) == 0xE) {
// The next bytes should be part of this Unicode Code Point
if (u8.length < (i + 3))
break;
if (((u8[i+1] >> 6) == 0x2) && ((u8[i+2] >> 6) == 0x2)) {
let cp = ((u8[i] & 0xF) << 12) | ((u8[i+1] & 0x3F) << 6) | (u8[i+2] & 0x3F);
result += String.fromCodePoint(cp);
i += 2;
}
} else if ((u8[i] >> 3) == 0xF) {
// The next bytes should be part of this Unicode Code Point
if (u8.length < (i + 4))
break;
if (((u8[i+1] >> 6) == 0x2) && ((u8[i+2] >> 6) == 0x2) && ((u8[i+3] >> 6) == 0x2)) {
let cp = ((u8[i] & 0x7) << 18) | ((u8[i+1] & 0x3F) << 12) | ((u8[i+2] & 0x3F) << 6) | (u8[i+3] & 0x3F);
result += String.fromCodePoint(cp);
i += 3;
}
}
}
return result;
};
};
class bytes {
static toBase64 (bytes) {
// Input Validation
if (!(bytes instanceof ArrayBuffer || ArrayBuffer.isView(bytes))) {
throw "Invalid input. 1st argument of bytes.toBase64 must be an ArrayBuffer";
}
let result = "";
let encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let padding = "=";
let u8 = new Uint8Array(buffer);
let remainder = u8.length % 3;
let mainlen = u8.length - remainder;
for (let i = 0; i < mainlen; i += 3) {
let word = (u8[i] << 16) | (u8[i+1] << 8) | u8[i+2];
result += encoding[(word & 0b111111000000000000000000) >> 18];
result += encoding[(word & 0b000000111111000000000000) >> 12];
result += encoding[(word & 0b000000000000111111000000) >> 6];
result += encoding[(word & 0b000000000000000000111111)];
}
if (remainder == 2) {
let word = (u8[mainlen] << 8) | u8[mainlen+1];
result += encoding[(word & 0b1111110000000000) >> 10];
result += encoding[(word & 0b0000001111110000) >> 4];
result += encoding[(word & 0b0000000000001111) << 2];
result += padding;
} else if (remainder == 1) {
let word = u8[mainlen];
result += encoding[(word & 0b11111100) >> 2];
result += encoding[(word & 0b00000011) << 4];
result += padding;
result += padding;
}
return result;
};
static fromBase64 (string) {
// Input validation
if (typeof string !== "string") {
throw "Invalid input. 1st argument of bytes.fromBase64 must be a string";
}
let encodings = {"0":52,"1":53,"2":54,"3":55,"4":56,"5":57,"6":58,"7":59,"8":60,"9":61,"A":0,"B":1,"C":2,"D":3,"E":4,"F":5,
"G":6,"H":7,"I":8,"J":9,"K":10,"L":11,"M":12,"N":13,"O":14,"P":15,"Q":16,"R":17,"S":18,"T":19,"U":20,"V":21,"W":22,"X":23,"Y":24,"Z":25,"a":26,"b":27,"c":28,"d":29,"e":30,"f":31,"g":32,"h":33,"i":34,"j":35,"k":36,"l":37,"m":38,"n":39,"o":40,
"p":41,"q":42,"r":43,"s":44,"t":45,"u":46,"v":47,"w":48,"x":49,"y":50,"z":51,"+":62,"/":63};
let remainder = string.length % 4;
if (remainder > 0)
throw "Invalid padding on base64 string";
let lastChunkBCnt = (string.endsWith("=") ? (string.endsWith("==") ? 1 : 2) : 0);
let mainlen = string.length - (lastChunkBCnt === 0 ? 0 : 4);
let nbytes = ((mainlen / 4) * 3) + lastChunkBCnt;
let bytes = new Uint8Array(nbytes);
let j = 0;
for (let i = 0; i < mainlen; i += 4) {
let word = (encodings[string[i]] << 18) | (encodings[string[i+1]] << 12) | (encodings[string[i+2]] << 6) | encodings[string[i+3]];
bytes[j++] = (word & 0b111111110000000000000000) >> 16;
bytes[j++] = (word & 0b000000001111111100000000) >> 8;
bytes[j++] = (word & 0b000000000000000011111111);
}
if (lastChunkBCnt === 1) {
let word = (encodings[string[mainlen]] << 2) | (encodings[string[mainlen+1]] >> 4);
bytes[j++] = word;
} else if (lastChunkBCnt === 2) {
let word = (encodings[string[mainlen]] << 10) | (encodings[string[mainlen+1]] << 4) | (encodings[string[mainlen+2]] >>2);
bytes[j++] = (word & 0b1111111100000000) >> 8;
bytes[j++] = (word & 0b0000000011111111);
}
return bytes.buffer;
};
};
class encryption {
static isKeypair(keys) {
return "aes" in keys && "hmac" in keys && keys.aes instanceof CryptoKey && keys.hmac instanceof CryptoKey;
}
static async sign (keys, data) {
// Validate the type of the inputs
if (!encryption.isKeypair(keys)) {
return "Invalid input. 1st argument of encryption.sign must be a keypair.";
} else if (!(data instanceof ArrayBuffer || ArrayBuffer.isView(data))) {
return "Inalid input. 2nd argument of encryption.sign must be an ArrayBuffer."
}
let signature = null;
try {
signature = await window.crypto.subtle.sign("HMAC", keys.hmac, data);
} catch (ex) {
return null;
}
return signature;
};
static async verify (keys, signature, data) {
// Validate the type of the inputs
if (!encryption.isKeypair(keys)) {
return "Invalid input. 1st argument of encryption.sign must be a keypair.";
} else if (!(signature instanceof ArrayBuffer || ArrayBuffer.isView(signature))) {
return "Inalid input. 2nd argument of encryption.sign must be an ArrayBuffer."
} else if (!(data instanceof ArrayBuffer || ArrayBuffer.isView(data))) {
return "Inalid input. 3rd argument of encryption.sign must be an ArrayBuffer."
}
let verified = false;
try {
verified = await window.crypto.subtle.verify("HMAC", keys.hmac, signature, data);
} catch (ex) {