Merge pull request #79 from shekenahglory/develop

Ripple Vault Client Integration
This commit is contained in:
wltsmrz
2014-05-21 00:43:45 -07:00
8 changed files with 1714 additions and 1 deletions

View File

@@ -18,7 +18,8 @@
"async": "~0.8.0",
"ws": "~0.4.31",
"extend": "~1.2.1",
"lru-cache": "~2.5.0"
"lru-cache": "~2.5.0",
"superagent": "^0.18.0"
},
"devDependencies": {
"mocha": "~1.14.0",

36
src/js/ripple/authinfo.js Normal file
View File

@@ -0,0 +1,36 @@
var RippleTxt = require('./rippletxt');
var request = require('superagent');
function AuthInfo () {
this.rippleTxt = new RippleTxt;
}
/**
* Get auth info for a given username
* @param {string} domain - Domain which hosts the user's info
* @param {string} username - Username who's info we are retreiving
* @param {function} fn - Callback function
*/
AuthInfo.prototype.get = function (domain, username, fn) {
var self = this;
self.rippleTxt.get(domain, function(err, txt){
if (err) return fn(err);
processTxt(txt)
});
function processTxt(txt) {
if (!txt.authinfo_url) return fn(new Error("Authentication is not supported on "+domain));
var url = Array.isArray(txt.authinfo_url) ? txt.authinfo_url[0] : txt.authinfo_url;
url += "?domain="+domain+"&username="+username;
request.get(url, function(err, resp){
if (err || resp.error) return fn(new Error("Authentication info server unreachable"));
fn(null, resp.body);
});
}
}
module.exports = AuthInfo;

597
src/js/ripple/blob.js Normal file
View File

@@ -0,0 +1,597 @@
var crypt = require('./crypt'),
request = require('superagent'),
extend = require("extend");
//Blob object class
var BlobObj = function (url, id, key) {
this.url = url;
this.id = id;
this.key = key;
this.data = {};
};
// Blob operations
// Do NOT change the mapping of existing ops
BlobObj.ops = {
// Special
"noop" : 0,
// Simple ops
"set" : 16,
"unset" : 17,
"extend" : 18,
// Meta ops
"push" : 32,
"pop" : 33,
"shift" : 34,
"unshift" : 35,
"filter" : 36
};
BlobObj.opsReverseMap = [];
for (var name in BlobObj.ops) {
BlobObj.opsReverseMap[BlobObj.ops[name]] = name;
}
/*
* Initialize a new blob object
* @param {function} fn - Callback function
*/
BlobObj.prototype.init = function (fn) {
var self = this, url;
if (self.url.indexOf("://") === -1) self.url = "http://" + url;
url = self.url + '/v1/blob/' + self.id;
request.get(url, function(err, resp){
if (err || !resp.body || resp.body.result !== 'success')
return fn(new Error("Could not retrieve blob"));
self.revision = resp.body.revision;
self.encrypted_secret = resp.body.encrypted_secret;
if (!self.decrypt(resp.body.blob)) {
return fn(new Error("Error while decrypting blob"));
}
//Apply patches
if (resp.body.patches && resp.body.patches.length) {
var successful = true;
resp.body.patches.forEach(function (patch) {
successful = successful && self.applyEncryptedPatch(patch);
});
if (successful) self.consolidate();
}
fn(null, self);//return with newly decrypted blob
}).timeout(8000);
}
/*
* Consolidate -
* Consolidate patches as a new revision
* @param {function} fn - Callback function
*/
BlobObj.prototype.consolidate = function (fn) {
// Callback is optional
if ("function" !== typeof fn) fn = function(){};
console.log("client: blob: consolidation at revision", this.revision);
var encrypted = this.encrypt();
var config = {
method : 'POST',
url : this.url + '/v1/blob/consolidate',
dataType : 'json',
data : {
blob_id : this.id,
data : encrypted,
revision : this.revision
},
};
var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id);
request.post(signed.url)
.send(signed.data)
.end(function(err, resp) {
// XXX Add better error information to exception
if (err) return fn(new Error("Failed to consolidate blob - XHR error"));
else if (resp.body && resp.body.result === 'success') return fn(null, resp.body);
else return fn(new Error("Failed to consolidate blob"));
});
};
/*
* ApplyEncryptedPatch -
* save changes from a downloaded patch to the blob
* @param {string} patch - encrypted patch string
*/
BlobObj.prototype.applyEncryptedPatch = function (patch)
{
try {
var params = JSON.parse(crypt.decrypt(this.key, patch));
var op = params.shift();
var path = params.shift();
this.applyUpdate(op, path, params);
this.revision++;
return true;
} catch (err) {
console.log("client: blob: failed to apply patch:", err.toString());
console.log(err.stack);
return false;
}
}
/**
* Encrypt secret with unlock key
* @param {string} secretUnlockkey
*/
BlobObj.prototype.encryptSecret = function (secretUnlockKey, secret) {
return crypt.encrypt(secretUnlockKey, secret);
};
/**
* Decrypt secret with unlock key
* @param {string} secretUnlockkey
*/
BlobObj.prototype.decryptSecret = function (secretUnlockKey) {
return crypt.decrypt(secretUnlockKey, this.encrypted_secret);
};
/**
* Decrypt blob with crypt key
* @param {string} data - encrypted blob data
*/
BlobObj.prototype.decrypt = function (data) {
try {
this.data = JSON.parse(crypt.decrypt(this.key, data));
return this;
} catch (e) {
console.log("client: blob: decryption failed", e.toString());
console.log(e.stack);
return false;
}
};
/**
* Encrypt blob with crypt key
*/
BlobObj.prototype.encrypt = function()
{
// Filter Angular metadata before encryption
// if ('object' === typeof this.data &&
// 'object' === typeof this.data.contacts)
// this.data.contacts = angular.fromJson(angular.toJson(this.data.contacts));
return crypt.encrypt(this.key, JSON.stringify(this.data));
};
/**
* Encrypt recovery key
* @param {string} secret
* @param {string} blobDecryptKey
*/
BlobObj.prototype.encryptBlobCrypt = function (secret, blobDecryptKey) {
var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret);
return crypt.encrypt(recoveryEncryptionKey, blobDecryptKey);
};
/**
* Decrypt recovery key
* @param {string} secret
*/
BlobObj.prototype.decryptBlobCrypt = function (secret) {
var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret);
return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key);
};
/**** Blob updating functions ****/
/**
* Set blob element
*/
BlobObj.prototype.set = function (pointer, value, fn) {
this.applyUpdate('set', pointer, [value]);
this.postUpdate('set', pointer, [value], fn);
};
/**
* Remove blob element
*/
BlobObj.prototype.unset = function (pointer, fn) {
this.applyUpdate('unset', pointer, []);
this.postUpdate('unset', pointer, [], fn);
};
/**
* Extend blob object
*/
BlobObj.prototype.extend = function (pointer, value, fn) {
this.applyUpdate('extend', pointer, [value]);
this.postUpdate('extend', pointer, [value], fn);
};
/**
* Prepend blob array
*/
BlobObj.prototype.unshift = function (pointer, value, fn) {
this.applyUpdate('unshift', pointer, [value]);
this.postUpdate('unshift', pointer, [value], fn);
};
/**
* Filter the row(s) from an array.
*
* This method will find any entries from the array stored under `pointer` and
* apply the `subcommands` to each of them.
*
* The subcommands can be any commands with the pointer parameter left out.
*/
BlobObj.prototype.filter = function (pointer, field, value, subcommands, callback) {
var params = Array.prototype.slice.apply(arguments);
if ("function" === typeof params[params.length-1]) {
callback = params.pop();
}
params.shift();
// Normalize subcommands to minimize the patch size
params = params.slice(0, 2).concat(normalizeSubcommands(params.slice(2), true));
this.applyUpdate('filter', pointer, params);
this.postUpdate('filter', pointer, params, callback);
};
/**
* Apply udpdate to the blob
*/
BlobObj.prototype.applyUpdate = function (op, path, params) {
// Exchange from numeric op code to string
if ("number" === typeof op) {
op = BlobObj.opsReverseMap[op];
}
if ("string" !== typeof op) {
throw new Error("Blob update op code must be a number or a valid op id string");
}
// Separate each step in the "pointer"
var pointer = path.split("/");
var first = pointer.shift();
if (first !== "") {
throw new Error("Invalid JSON pointer: "+path);
}
this._traverse(this.data, pointer, path, op, params);
};
//for applyUpdate function
BlobObj.prototype._traverse = function (context, pointer,
originalPointer, op, params) {
var _this = this;
var part = _this.unescapeToken(pointer.shift());
if (Array.isArray(context)) {
if (part === '-') {
part = context.length;
} else if (part % 1 !== 0 && part >= 0) {
throw new Error("Invalid pointer, array element segments must be " +
"a positive integer, zero or '-'");
}
} else if ("object" !== typeof context) {
return null;
} else if (!context.hasOwnProperty(part)) {
// Some opcodes create the path as they're going along
if (op === "set") {
context[part] = {};
} else if (op === "unshift") {
context[part] = [];
} else {
return null;
}
}
if (pointer.length !== 0) {
return this._traverse(context[part], pointer,
originalPointer, op, params);
}
switch (op) {
case "set":
context[part] = params[0];
break;
case "unset":
if (Array.isArray(context)) {
context.splice(part, 1);
} else {
delete context[part];
}
break;
case "extend":
if ("object" !== typeof context[part]) {
throw new Error("Tried to extend a non-object");
}
extend(true, context[part], params[0]);
break;
case "unshift":
if ("undefined" === typeof context[part]) {
context[part] = [];
} else if (!Array.isArray(context[part])) {
throw new Error("Operator 'unshift' must be applied to an array.");
}
context[part].unshift(params[0]);
break;
case "filter":
if (Array.isArray(context[part])) {
context[part].forEach(function (element, i) {
if ("object" === typeof element &&
element.hasOwnProperty(params[0]) &&
element[params[0]] === params[1]) {
var subpointer = originalPointer+"/"+i;
var subcommands = normalizeSubcommands(params.slice(2));
subcommands.forEach(function (subcommand) {
var op = subcommand[0];
var pointer = subpointer+subcommand[1];
_this.applyUpdate(op, pointer, subcommand.slice(2));
});
}
});
}
break;
default:
throw new Error("Unsupported op "+op);
}
};
BlobObj.prototype.escapeToken = function (token) {
return token.replace(/[~\/]/g, function (key) { return key === "~" ? "~0" : "~1"; });
};
BlobObj.prototype.unescapeToken = function(str) {
return str.replace(/~./g, function(m) {
switch (m) {
case "~0":
return "~";
case "~1":
return "/";
}
throw("Invalid tilde escape: " + m);
});
};
/**
* Sumbit update to blob vault
*/
BlobObj.prototype.postUpdate = function (op, pointer, params, fn) {
// Callback is optional
if ("function" !== typeof fn) fn = function(){};
if ("string" === typeof op) {
op = BlobObj.ops[op];
}
if ("number" !== typeof op) {
throw new Error("Blob update op code must be a number or a valid op id string");
}
if (op < 0 || op > 255) {
throw new Error("Blob update op code out of bounds");
}
console.log("client: blob: submitting update", BlobObj.opsReverseMap[op], pointer, params);
params.unshift(pointer);
params.unshift(op);
var config = {
method : 'POST',
url : this.url + '/v1/blob/patch',
dataType : 'json',
data : {
blob_id : this.id,
patch : crypt.encrypt(this.key, JSON.stringify(params))
}
};
var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id);
request.post(signed.url)
.send(signed.data)
.end(function(err, resp) {
if (err)
return fn(new Error("Patch could not be saved - XHR error"));
else if (!resp.body || resp.body.result !== 'success')
return fn(new Error("Patch could not be saved - bad result"));
return fn(null, resp.body);
});
};
/***** helper functions *****/
function normalizeSubcommands(subcommands, compress) {
// Normalize parameter structure
if ("number" === typeof subcommands[0] ||
"string" === typeof subcommands[0]) {
// Case 1: Single subcommand inline
subcommands = [subcommands];
} else if (subcommands.length === 1 &&
Array.isArray(subcommands[0]) &&
("number" === typeof subcommands[0][0] ||
"string" === typeof subcommands[0][0])) {
// Case 2: Single subcommand as array
// (nothing to do)
} else if (Array.isArray(subcommands[0])) {
// Case 3: Multiple subcommands as array of arrays
subcommands = subcommands[0];
}
// Normalize op name and convert strings to numeric codes
subcommands = subcommands.map(function (subcommand) {
if ("string" === typeof subcommand[0]) {
subcommand[0] = BlobObj.ops[subcommand[0]];
}
if ("number" !== typeof subcommand[0]) {
throw new Error("Invalid op in subcommand");
}
if ("string" !== typeof subcommand[1]) {
throw new Error("Invalid path in subcommand");
}
return subcommand;
});
if (compress) {
// Convert to the minimal possible format
if (subcommands.length === 1) {
return subcommands[0];
} else {
return [subcommands];
}
} else {
return subcommands;
}
}
/***** blob client methods ****/
/**
* Blob object class
*/
module.exports.Blob = BlobObj
/**
* Get ripple name for a given address
*/
module.exports.getRippleName = function (url, address, fn) {
if (!crypt.isValidAddress(address)) return fn (new Error("Invalid ripple address"));
request.get(url + '/v1/user/' + address, function(err, resp){
if (err) return fn(new Error("Unable to access vault sever"));
else if (resp.body && resp.body.username) return fn(null, resp.body.username);
else if (resp.body && resp.body.exists === false) return fn (new Error("No ripple name for this address"));
else return fn(new Error("Unable to determine if ripple name exists"));
});
}
/*
* Retrive a blob with url, id and key
*/
module.exports.get = function (url, id, crypt, fn) {
var blob = new BlobObj(url, id, crypt);
blob.init(fn);
}
/*
* Verify email address
*/
module.exports.verify = function (url, username, token, fn) {
url += '/v1/user/' + username + '/verify/' + token;
request.get(url, function(err, resp){
if (err) return fn(err);
else if (resp.body && resp.body.result === 'success') return fn(null, data);
else return fn(new Error("Failed to verify the account"));
});
}
/**
* Create a blob object
*
* @param {object} options
* @param {string} options.url
* @param {string} options.id
* @param {string} options.crypt
* @param {string} options.unlock
* @param {string} options.username
* @param {string} options.masterkey
* @param {object} options.oldUserBlob
* @param {function} fn
*/
module.exports.create = function (options, fn)
{
var blob = new BlobObj(options.url, options.id, options.crypt);
blob.revision = 0;
blob.data = {
auth_secret : crypt.createSecret(8),
account_id : crypt.getAddress(options.masterkey),
email : options.email,
contacts : [],
created : (new Date()).toJSON()
};
blob.encrypted_secret = blob.encryptSecret(options.unlock, options.masterkey);
// Migration
if (options.oldUserBlob) {
blob.data.contacts = options.oldUserBlob.data.contacts;
}
//post to the blob vault to create
var config = {
method : "POST",
url : options.url + '/v1/user',
data : {
blob_id : options.id,
username : options.username,
address : blob.data.account_id,
auth_secret : blob.data.auth_secret,
data : blob.encrypt(),
email : options.email,
hostlink : options.activateLink,
encrypted_blobdecrypt_key : blob.encryptBlobCrypt(options.masterkey, options.crypt),
encrypted_secret : blob.encrypted_secret
}
};
var signed = crypt.signRequestAsymmetric(config, options.masterkey, blob.data.account_id, options.id);
request.post(signed)
.send(signed.data)
.end(function(err, resp) {
if (err) return fn(err);
else if (resp.body && resp.body.result === 'success') return fn(null, blob,resp.body);
else return fn(new Error("Could not create blob"));
});
}

480
src/js/ripple/crypt.js Normal file
View File

@@ -0,0 +1,480 @@
var sjcl = require('./utils').sjcl,
base = require('./base').Base,
UInt160 = require('./uint160').UInt160,
message = require('./message'),
request = require('superagent'),
extend = require("extend"),
parser = require("url");
var cryptConfig = {
cipher : "aes",
mode : "ccm",
ts : 64, // tag length
ks : 256, // key size
iter : 1000 // iterations (key derivation)
};
var Crypt = {};
/**
* Full domain hash based on SHA512
*/
function fdh(data, bytelen)
{
var bitlen = bytelen << 3;
if (typeof data === "string") {
data = sjcl.codec.utf8String.toBits(data);
}
// Add hashing rounds until we exceed desired length in bits
var counter = 0, output = [];
while (sjcl.bitArray.bitLength(output) < bitlen) {
var hash = sjcl.hash.sha512.hash(sjcl.bitArray.concat([counter], data));
output = sjcl.bitArray.concat(output, hash);
counter++;
}
// Truncate to desired length
output = sjcl.bitArray.clamp(output, bitlen);
return output;
}
/**
* This is a function to derive different hashes from the same key.
* Each hash is derived as HMAC-SHA512HALF(key, token).
* @param {string} key
* @param {string} hash
*/
function keyHash(key, token) {
var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha512);
return sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(hmac.encrypt(token), 0, 256));
}
/****** exposed functions ******/
/**
* KEY DERIVATION FUNCTION
*
* This service takes care of the key derivation, i.e. converting low-entropy
* secret into higher entropy secret via either computationally expensive
* processes or peer-assisted key derivation (PAKDF).
* @param {object} opts
* @param {string} purpose - Key type/purpose
* @param {string} username
* @param {string} secret - Also known as passphrase/password
* @param {function} fn
*/
Crypt.derive = function (opts, purpose, username, secret, fn) {
var tokens;
if (purpose=='login') tokens = ['id', 'crypt'];
else tokens = ['unlock'];
var iExponent = new sjcl.bn(String(opts.exponent)),
iModulus = new sjcl.bn(String(opts.modulus)),
iAlpha = new sjcl.bn(String(opts.alpha));
var publicInfo = "PAKDF_1_0_0:"+opts.host.length+":"+opts.host+
":"+username.length+":"+username+
":"+purpose.length+":"+purpose+
":",
publicSize = Math.ceil(Math.min((7+iModulus.bitLength()) >>> 3, 256)/8),
publicHash = fdh(publicInfo, publicSize),
publicHex = sjcl.codec.hex.fromBits(publicHash),
iPublic = new sjcl.bn(String(publicHex)).setBitM(0),
secretInfo = publicInfo+":"+secret.length+":"+secret+":",
secretSize = (7+iModulus.bitLength()) >>> 3,
secretHash = fdh(secretInfo, secretSize),
secretHex = sjcl.codec.hex.fromBits(secretHash),
iSecret = new sjcl.bn(String(secretHex)).mod(iModulus);
if (iSecret.jacobi(iModulus) !== 1) {
iSecret = iSecret.mul(iAlpha).mod(iModulus);
}
var iRandom;
for (;;) {
iRandom = sjcl.bn.random(iModulus, 0);
if (iRandom.jacobi(iModulus) === 1)
break;
}
var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus),
iSignreq = iSecret.mulmod(iBlind, iModulus),
signreq = sjcl.codec.hex.fromBits(iSignreq.toBits());
request.post(opts.url)
.send({
info : publicInfo,
signreq : signreq
}).end(function(err, resp) {
if (err || !resp) return fn(new Error("Could not query PAKDF server "+opts.host));
var data = resp.body || resp.text ? JSON.parse(resp.text) : {};
if (!data.result=='success') return fn(new Error("Could not query PAKDF server "+opts.host));
var iSignres = new sjcl.bn(String(data.signres));
iRandomInv = iRandom.inverseMod(iModulus),
iSigned = iSignres.mulmod(iRandomInv, iModulus),
key = iSigned.toBits(),
result = {};
tokens.forEach(function (token) {
result[token] = keyHash(key, token);
});
fn (null, result);
});
}
/**
* Imported from ripple-client
*
*/
Crypt.RippleAddress = (function () {
function append_int(a, i) {
return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff)
}
function firstHalfOfSHA512(bytes) {
return sjcl.bitArray.bitSlice(
sjcl.hash.sha512.hash(sjcl.codec.bytes.toBits(bytes)),
0, 256
);
}
function SHA256_RIPEMD160(bits) {
return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits));
}
return function (seed) {
this.seed = base.decode_check(33, seed);
if (!this.seed) {
throw "Invalid seed."
}
this.getAddress = function (seq) {
seq = seq || 0;
var private_gen, public_gen, i = 0;
do {
private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.seed, i)));
i++;
} while (!sjcl.ecc.curves.c256.r.greaterEquals(private_gen));
public_gen = sjcl.ecc.curves.c256.G.mult(private_gen);
var sec;
i = 0;
do {
sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i)));
i++;
} while (!sjcl.ecc.curves.c256.r.greaterEquals(sec));
var pubKey = sjcl.ecc.curves.c256.G.mult(sec).toJac().add(public_gen).toAffine();
return base.encode_check(0, sjcl.codec.bytes.fromBits(SHA256_RIPEMD160(sjcl.codec.bytes.toBits(pubKey.toBytesCompressed()))));
};
};
})();
/**
* Encrypt data
* @params {string} key
* @params {string} data
*/
Crypt.encrypt = function(key, data)
{
key = sjcl.codec.hex.toBits(key);
var opts = extend(true, {}, cryptConfig);
var encryptedObj = JSON.parse(sjcl.encrypt(key, data, opts));
var version = [sjcl.bitArray.partial(8, 0)];
var initVector = sjcl.codec.base64.toBits(encryptedObj.iv);
var ciphertext = sjcl.codec.base64.toBits(encryptedObj.ct);
var encryptedBits = sjcl.bitArray.concat(version, initVector);
encryptedBits = sjcl.bitArray.concat(encryptedBits, ciphertext);
return sjcl.codec.base64.fromBits(encryptedBits);
}
/**
* Decrypt data
* @params {string} key
* @params {string} data
*/
Crypt.decrypt = function(key, data)
{
key = sjcl.codec.hex.toBits(key);
var encryptedBits = sjcl.codec.base64.toBits(data);
var version = sjcl.bitArray.extract(encryptedBits, 0, 8);
if (version !== 0) {
throw new Error("Unsupported encryption version: "+version);
}
var encrypted = extend(true, {}, cryptConfig, {
iv: sjcl.codec.base64.fromBits(sjcl.bitArray.bitSlice(encryptedBits, 8, 8+128)),
ct: sjcl.codec.base64.fromBits(sjcl.bitArray.bitSlice(encryptedBits, 8+128))
});
return sjcl.decrypt(key, JSON.stringify(encrypted));
}
/**
* Validate a ripple address
* @param {string} address
*/
Crypt.isValidAddress = function (address) {
return UInt160.is_valid(address);
}
/**
* Validate a ripple address
* @param {integer} nWords - number of words
*/
Crypt.createSecret = function (nWords) {
return sjcl.codec.hex.fromBits(sjcl.random.randomWords(nWords));
}
/**
* Create a new master key
*/
Crypt.createMaster = function () {
return base.encode_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4)));
}
/**
* Create a ripple address from a master key
* @param {string} masterkey
*/
Crypt.getAddress = function (masterkey) {
return new Crypt.RippleAddress(masterkey).getAddress();
}
/**
* Hash data
* @param {string} data
*/
Crypt.hashSha512 = function (data) {
return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data));
}
/**
* Sign a data string with a secret key
* @param {string} secret
* @param {string} data
*/
Crypt.signString = function (secret, data) {
var hmac = new sjcl.misc.hmac(sjcl.codec.hex.toBits(secret), sjcl.hash.sha512);
return sjcl.codec.hex.fromBits(hmac.mac(data));
}
/**
* Create an an accout recovery key
* @param {string} secret
*/
Crypt.deriveRecoveryEncryptionKeyFromSecret = function(secret) {
var seed = ripple.Seed.from_json(secret).to_bits();
var hmac = new sjcl.misc.hmac(seed, sjcl.hash.sha512);
var key = hmac.mac("ripple/hmac/recovery_encryption_key/v1");
key = sjcl.bitArray.bitSlice(key, 0, 256);
return sjcl.codec.hex.fromBits(key);
}
/**
* Convert base64 encoded data into base64url encoded data.
* @param {String} base64 Data
*/
Crypt.base64ToBase64Url = function (encodedData) {
return encodedData.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+$/, '');
};
/**
* Convert base64url encoded data into base64 encoded data.
* @param {String} base64 Data
*/
Crypt.base64UrlToBase64 = function (encodedData) {
encodedData = encodedData.replace(/-/g, '+').replace(/_/g, '/');
while (encodedData.length % 4) {
encodedData += '=';
}
return encodedData;
};
/**
* Create a string from request parameters that
* will be used to sign a request
* @param {Object} config - request params
* @param {Object} parsed - parsed url
* @param {Object} date
* @param {Object} mechanism - type of signing
*/
Crypt.getStringToSign = function (config, parsed, date, mechanism) {
// XXX This method doesn't handle signing GET requests correctly. The data
// field will be merged into the search string, not the request body.
// Sort the properties of the JSON object into canonical form
var canonicalData = JSON.stringify(copyObjectWithSortedKeys(config.data));
// Canonical request using Amazon's v4 signature format
// See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
var canonicalRequest = [
config.method || 'GET',
parsed.pathname || '',
parsed.search || '',
// XXX Headers signing not supported
'',
'',
Crypt.hashSha512(canonicalData).toLowerCase()
].join('\n');
// String to sign inspired by Amazon's v4 signature format
// See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
//
// We don't have a credential scope, so we skip it.
//
// But that modifies the format, so the format ID is RIPPLE1, instead of AWS4.
return stringToSign = [
mechanism,
date,
Crypt.hashSha512(canonicalRequest).toLowerCase()
].join('\n');
}
/**
* HMAC signed request
* @param {Object} config
* @param {Object} auth_secret
* @param {Object} blob_id
*/
Crypt.signRequestHmac = function (config, auth_secret, blob_id) {
config = extend(true, {}, config);
// Parse URL
var parsed = parser.parse(config.url);
var date = dateAsIso8601();
var signatureType = 'RIPPLE1-HMAC-SHA512';
var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType);
var signature = Crypt.signString(auth_secret, stringToSign);
config.url += (parsed.search ? "&" : "?") +
'signature='+Crypt.base64ToBase64Url(signature)+
'&signature_date='+date+
'&signature_blob_id='+blob_id+
'&signature_type='+signatureType
return config;
};
/**
* Asymmetric signed request
* @param {Object} config
* @param {Object} secretKey
* @param {Object} account
* @param {Object} blob_id
*/
Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) {
config = extend(true, {}, config);
// Parse URL
var parsed = parser.parse(config.url);
var date = dateAsIso8601();
var signatureType = 'RIPPLE1-ECDSA-SHA512';
var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType);
var signature = message.signMessage(stringToSign, secretKey);
config.url += (parsed.search ? "&" : "?") +
'signature='+Crypt.base64ToBase64Url(signature)+
'&signature_date='+date+
'&signature_blob_id='+blob_id+
'&signature_account='+account+
'&signature_type='+signatureType;
return config;
};
//prepare for signing
function copyObjectWithSortedKeys(object) {
if (isPlainObject(object)) {
var newObj = {};
var keysSorted = Object.keys(object).sort();
var key;
for (var i in keysSorted) {
key = keysSorted[i];
if (Object.prototype.hasOwnProperty.call(object, key)) {
newObj[key] = copyObjectWithSortedKeys(object[key]);
}
}
return newObj;
} else if (Array.isArray(object)) {
return object.map(copyObjectWithSortedKeys);
} else {
return object;
}
}
//from npm extend
function isPlainObject(obj) {
var hasOwn = Object.prototype.hasOwnProperty;
var toString = Object.prototype.toString;
if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval)
return false;
var has_own_constructor = hasOwn.call(obj, 'constructor');
var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
// Not own constructor property must be Object
if (obj.constructor && !has_own_constructor && !has_is_property_of_method)
return false;
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var key;
for ( key in obj ) {}
return key === undefined || hasOwn.call( obj, key );
};
var dateAsIso8601 = (function () {
function pad(n) {
return (n < 0 || n > 9 ? "" : "0") + n;
}
return function dateAsIso8601() {
var date = new Date();
return date.getUTCFullYear() + "-"
+ pad(date.getUTCMonth() + 1) + "-"
+ pad(date.getUTCDate()) + "T"
+ pad(date.getUTCHours()) + ":"
+ pad(date.getUTCMinutes()) + ":"
+ pad(date.getUTCSeconds()) + ".000Z";
};
})();
module.exports = Crypt;

View File

@@ -12,6 +12,7 @@ exports.Meta = require('./meta').Meta;
exports.SerializedObject = require('./serializedobject').SerializedObject;
exports.RippleError = require('./rippleerror').RippleError;
exports.Message = require('./message');
exports.VaultClient = require('./vaultclient');
exports.binformat = require('./binformat');
exports.utils = require('./utils');
exports.Server = require('./server').Server;

View File

@@ -0,0 +1,73 @@
var request = require('superagent');
function RippleTxt() {
this.txts = {};
};
/**
* Gets the ripple.txt file for the given domain
* @param {string} domain - Domain to retrieve file from
* @param {function} fn - Callback function
*/
RippleTxt.prototype.get = function (domain, fn) {
var self = this;
if (self.txts[domain]) return fn(null, self.txts[domain]);
var urls = [
'https://ripple.'+domain+'/ripple.txt',
'https://www.'+domain+'/ripple.txt',
'https://'+domain+'/ripple.txt',
'http://ripple.'+domain+'/ripple.txt',
'http://www.'+domain+'/ripple.txt',
'http://'+domain+'/ripple.txt'
].reverse();
next();
function next () {
if (!urls.length) return fn(new Error("No ripple.txt found"));
var url = urls.pop();
request.get(url, function(err, resp) {
if (err || !resp.text) return next();
var sections = self.parse(resp.text);
self.txts[domain] = sections;
fn(null, sections);
});
}
}
/**
* Parse a ripple.txt file
* @param {string} txt - Unparsed ripple.txt data
*/
RippleTxt.prototype.parse = function (txt) {
txt = txt.replace('\r\n', '\n');
txt = txt.replace('\r', '\n');
txt = txt.split('\n');
var currentSection = "", sections = {};
for (var i = 0, l = txt.length; i < l; i++) {
var line = txt[i];
if (!line.length || line[0] === '#') {
continue;
} else if (line[0] === '[' && line[line.length-1] === ']') {
currentSection = line.slice(1, line.length-1);
sections[currentSection] = [];
} else {
line = line.replace(/^\s+|\s+$/g, '');
if (sections[currentSection]) {
sections[currentSection].push(line);
}
}
}
return sections;
}
module.exports = RippleTxt;

View File

@@ -0,0 +1,285 @@
var AuthInfo = require('./authinfo');
var blobClient = require('./blob');
var crypt = require('./crypt');
function VaultClient(opts) {
if (!opts) opts = {};
else if (typeof opts === "string") opts = {domain:opts};
this.domain = opts.domain || 'ripple.com';
this.authInfo = new AuthInfo;
this.infos = {};
};
/**
* Reduce username to standardized form.
* Strips whitespace at beginning and end.
* @param {string} username - Username to normalize
*/
VaultClient.prototype.normalizeUsername = function (username) {
username = ""+username;
username = username.trim();
return username;
};
/**
* Reduce password to standardized form.
* Strips whitespace at beginning and end.
* @param {string} password - password to normalize
*/
VaultClient.prototype.normalizePassword = function (password) {
password = ""+password;
password = password.trim();
return password;
};
/**
* Get a ripple name from a given account address, if it has one
* @param {string} address - Account address to query
* @param {string} url - Url of blob vault
*/
VaultClient.prototype.getRippleName = function(address, url, fn) {
//use the url from previously retrieved authInfo, if necessary
if (!url) return fn(new Error("Blob vault URL is required"));
blobClient.getRippleName(url, address, fn);
};
/**
* Authenticate and retrieve a decrypted blob using a ripple name and password
* @param {string} username
* @param {string} password
* @param {function} fn - Callback function
*/
VaultClient.prototype.login = function(username, password, fn) {
var self = this;
self.authInfo.get(self.domain, username, function(err, authInfo){
if (err) return fn(err);
if (authInfo.version !== 3) {
return fn(new Error("This wallet is incompatible with this version of the vault-client."));
}
if (!authInfo.pakdf) {
return fn(new Error("No settings for PAKDF in auth packet."));
}
if (!authInfo.exists) {
return fn(new Error("User does not exist."));
}
if ("string" !== typeof authInfo.blobvault) {
return fn(new Error("No blobvault specified in the authinfo."));
}
//derive login keys
crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys){
if (err) return fn(err);
blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function (err, blob) {
if (err) return fn(err);
self.infos[keys.id] = authInfo; //save for relogin
fn (null, {
blob : blob,
username : authInfo.username,
verified : authInfo.emailVerified
});
});
});
});
};
/**
* Retreive and decrypt blob using a blob url, id and crypt derived previously.
* @param {string} url - Blob vault url
* @param {string} id - Blob id from previously retreived blob
* @param {string} key - Blob decryption key
* @param {function} fn - Callback function
*/
VaultClient.prototype.relogin = function(url, id, key, fn) {
//use the url from previously retrieved authInfo, if necessary
if (!url && this.infos[id]) url = this.infos[id].blobvault;
if (!url) return fn(new Error("Blob vault URL is required"));
blobClient.get(url, id, key, function (err, blob) {
if (err) return fn(err);
fn (null, {
blob : blob,
});
});
};
/**
* Decrypt the secret key using a username and password
* @param {string} username
* @param {string} password
* @param {string} encryptSecret
* @param {function} fn - Callback function
*/
VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) {
var self = this;
self.authInfo.get(self.domain, username, function(err, authInfo){
if (err) return fn(err);
//derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){
if (err) return fn(err);
fn(null, {
keys : keys,
secret : crypt.decrypt(keys.unlock, encryptSecret)
});
});
});
};
/**
* Retrieve the decrypted blob and secret key in one step using
* the username and password
* @param {string} username
* @param {string} password
* @param {function} fn - Callback function
*/
VaultClient.prototype.loginAndUnlock = function(username, password, fn) {
var self = this;
this.login(username, password, function(err, resp){
if (err) return fn(err);
if (!resp.blob || !resp.blob.encrypted_secret)
return fn(new Error("Unable to retrieve blob and secret."));
if (!resp.blob.id || !resp.blob.key)
return fn(new Error("Unable to retrieve keys."));
//get authInfo via id - would have been saved from login
var authInfo = self.infos[resp.blob.id];
if (!authInfo) return fn(new Error("Unable to find authInfo"));
//derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){
if (err) return fn(err);
fn(null, {
blob : resp.blob,
unlock : keys.unlock,
secret : crypt.decrypt(keys.unlock, resp.blob.encrypted_secret),
username : authInfo.username,
verified : authInfo.emailVerified
});
});
});
};
/**
* Check blobvault for existance of username
* @param {string} username
* @param {function} fn - Callback function
*/
VaultClient.prototype.exists = function (username, fn) {
this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo){
if (err) return fn(err);
return fn(null, !!authInfo.exists);
});
}
/*
* Verify an email address for an existing user
* @param {string} username
* @param {string} token - Verification token
* @param {function} fn - Callback function
*/
VaultClient.prototype.verify = function (username, token, fn) {
this.authInfo.get(this.domain, username.toLowerCase(), function (err, authInfo) {
if (err) return fn(err);
if ("string" !== typeof authInfo.blobvault) {
return fn(new Error("No blobvault specified in the authinfo."));
}
blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, fn);
});
}
/*
* Register a new user and save to the blob vault
*
* @param {object} options
* @param {string} options.username
* @param {string} options.password
* @param {string} options.masterkey //optional, will create if absent
* @param {string} options.email
* @param {string} options.activateLink
* @param {object} options.oldUserBlob //optional
* @param {function} fn
*/
VaultClient.prototype.register = function (options, fn) {
var self = this,
username = this.normalizeUsername(options.username),
password = this.normalizePassword(options.password);
self.authInfo.get(self.domain, username, function(err, authInfo){
if (err) return fn(err);
if ("string" !== typeof authInfo.blobvault) {
return fn(new Error("No blobvault specified in the authinfo."));
}
if (!authInfo.pakdf) {
return fn(new Error("No settings for PAKDF in auth packet."));
}
//derive login keys
crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, loginKeys){
if (err) return fn(err);
//derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, unlockKeys){
if (err) return fn(err);
var params = {
'url' : authInfo.blobvault,
'id' : loginKeys.id,
'crypt' : loginKeys.crypt,
'unlock' : unlockKeys.unlock,
'username' : username,
'email' : options.email,
'masterkey' : options.masterkey || crypt.createMaster(),
'activateLink' : options.activateLink,
'oldUserBlob' : options.oldUserBlob
}
blobClient.create(params, function(err, blob){
if (err) return fn(err);
fn(null, blob, loginKeys, authInfo.username);
});
});
});
});
};
module.exports = VaultClient;

240
test/vault-test.js Normal file
View File

@@ -0,0 +1,240 @@
var assert = require('assert'),
RippleTxt = require('../src/js/ripple/rippletxt'),
AuthInfo = require('../src/js/ripple/authinfo'),
VaultClient = require('../src/js/ripple/vaultclient'),
Blob = require('../src/js/ripple/blob').Blob,
UInt256 = require('../src/js/ripple/uint256').UInt256;
var exampleData = {
id : "ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f",
crypt : "f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82",
unlock : "3e15fe3218a9c664835a6f585582e14480112110ddbe50e5028d05fc5bd9b5f4",
blobURL : "https://id.staging.ripple.com",
username : "exampleUser",
password : "pass word",
domain : "staging.ripple.com",
encrypted_secret : "APYqtqvjJk/J324rx2BGGzUiQ3mtmMMhMsbrUmgxb00W2aFVQzCC2mqd58Z17gzeUUcjtjAm"
};
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //must be set for self signed certs
describe('Ripple Txt', function() {
it('should get the context of a ripple.txt file from a given domain', function(done){
var rt = new RippleTxt();
rt.get(exampleData.domain, function(err, resp){
assert.ifError(err);
assert.equal(typeof resp, 'object');
done();
});
});
});
describe('AuthInfo', function() {
var auth = new AuthInfo();
it ('should', function(done){
auth.get(exampleData.domain, exampleData.user, function(err, resp){
assert.ifError(err);
assert.equal(typeof resp, 'object');
done();
});
});
});
describe('VaultClient', function() {
var client = new VaultClient(exampleData.domain);
describe('initialization', function() {
it('should be initialized with a domain', function() {
var client = new VaultClient({ domain: exampleData.domain });
assert.strictEqual(client.domain, exampleData.domain);
});
it('should default to ripple.com without a domain', function() {
var client = new VaultClient();
assert.strictEqual(client.domain, 'ripple.com');
});
});
describe('#exists', function() {
it('should determine if a username exists on the domain', function(done) {
this.timeout(10000);
client.exists(exampleData.username, function(err, resp) {
assert.ifError(err);
assert.equal(typeof resp, 'boolean');
done();
});
});
});
describe('#login', function() {
it('with username and password should retrive the blob, crypt key, and id', function(done) {
this.timeout(10000);
client.login(exampleData.username, exampleData.password, function(err, resp) {
assert.ifError(err);
assert.equal(typeof resp, 'object');
assert(resp.blob instanceof Blob);
assert.equal(typeof resp.blob.id, 'string');
assert(UInt256.from_json(resp.blob.id).is_valid(), true);
assert.equal(typeof resp.blob.key, 'string');
assert(UInt256.from_json(resp.blob.key).is_valid(), true);
assert.equal(typeof resp.username, 'string');
assert.equal(typeof resp.verified, 'boolean');
done();
});
});
});
describe('#relogin', function() {
it('should retrieve the decrypted blob with blob vault url, id, and crypt key', function(done) {
this.timeout(10000);
client.relogin(exampleData.blobURL, exampleData.id, exampleData.crypt, function(err, resp) {
assert.ifError(err);
assert.equal(typeof resp, 'object');
assert(resp.blob instanceof Blob);
done();
});
});
});
describe('#unlock', function() {
it('should access the wallet secret using encryption secret, username and password', function(done) {
this.timeout(10000);
client.unlock(exampleData.username, exampleData.password, exampleData.encrypted_secret, function(err, resp) {
assert.ifError(err);
assert.equal(typeof resp, 'object');
assert.equal(typeof resp.keys, 'object');
assert.equal(typeof resp.keys.unlock, 'string');
assert(UInt256.from_json(resp.keys.unlock).is_valid(), true);
done();
});
});
});
describe('#loginAndUnlock', function() {
it('should get the decrypted blob and decrypted secret given name and password', function(done) {
this.timeout(10000);
client.loginAndUnlock(exampleData.username, exampleData.password, function(err, resp) {
assert.ifError(err);
assert.equal(typeof resp, 'object');
assert(resp.blob instanceof Blob);
assert.equal(typeof resp.blob.id, 'string');
assert(UInt256.from_json(resp.blob.id).is_valid(), true);
assert.equal(typeof resp.blob.key, 'string');
assert(UInt256.from_json(resp.blob.key).is_valid(), true);
assert.equal(typeof resp.unlock, 'string');
assert(UInt256.from_json(resp.unlock).is_valid(), true);
assert.equal(typeof resp.secret, 'string');
assert.equal(typeof resp.username, 'string');
assert.equal(typeof resp.verified, 'boolean');
done();
});
});
});
});
describe('Blob', function() {
var vaultClient;
vaultClient = new VaultClient({ domain: exampleData.domain });
vaultClient.login(exampleData.username, exampleData.password, function(err,resp){
assert.ifError(err);
var blob = resp.blob;
describe('#set', function() {
it('should set a new property in the blob', function(done) {
this.timeout(10000)
blob.extend("/testObject", {
foo : [],
}, function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
done();
});
});
});
describe('#extend', function() {
it('should extend an object in the blob', function(done) {
this.timeout(10000)
blob.extend("/testObject", {
foobar : "baz",
}, function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
done();
});
});
});
describe('#unset', function() {
it('should remove a property from the blob', function(done) {
this.timeout(10000)
blob.unset("/testObject", function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
done();
});
});
});
describe('#unshift', function() {
it('should prepend an item to an array in the blob', function(done) {
this.timeout(10000)
blob.unshift("/testArray", {
name : "bob",
address : "1234"
}, function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
done();
});
});
});
describe('#filter', function() {
it('should find a specific entity in an array and apply subcommands to it', function(done) {
this.timeout(10000)
blob.filter('/testArray', 'name', 'bob', 'extend', '', {description:"Alice"}, function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
done();
});
});
});
describe('#consolidate', function() {
it('should consolidate and save changes to the blob', function(done) {
this.timeout(10000)
blob.unset('/testArray', function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
blob.consolidate(function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
done();
});
});
});
});
});
});