stellar/stellar-wallet

View on GitHub
lib/models/wallet-v2.js

Summary

Maintainability
D
1 day
Test Coverage
var Stex    = require("stex");
var errors  = Stex.errors;
var Promise = Stex.Promise;
var _       = Stex._;

var walletV2 = module.exports;

var email          = require("../util/email");
var hash           = require("../util/hash");
var validate       = require("../util/validate");
var usernameProofs = require("../util/username-proofs");
var totp           = require("../util/totp");
var scmp           = require('scmp');
var Duration       = require("duration-js");

walletV2.errors                 = {};
walletV2.errors.Base            = Error.subclass("walletV2.Base");
walletV2.errors.InvalidTotpKey  = walletV2.errors.Base.subclass("walletV2.InvalidTotpKey");
walletV2.errors.InvalidTotpCode = walletV2.errors.Base.subclass("walletV2.InvalidTotpCode");

walletV2.hashWalletId = function(base64EncodedWalletId) {
  if(typeof base64EncodedWalletId !== 'string') {
    return null;
  }

  var rawWalletId = new Buffer(base64EncodedWalletId, "base64");
  return hash.sha2(rawWalletId);
};

walletV2.get = function(username) {
  return db("wallets_v2")
    .where("username", username)
    .whereNull("deletedAt")
    .select()
    .then(function(results) {
      var result = _.first(results);
      if (!result) {
        throw new errors.RecordNotFound();
      }
      return result;
    });
};

walletV2.getLoginParams = function(username) {
  return walletV2.get(username)
    .then(function (wallet) {
      if(!wallet){ throw new errors.RecordNotFound(); }

      return _(wallet)
        .pick("username", "salt", "kdfParams")
        .extend({totpRequired: walletV2.isTotpEnabled(wallet)})
        .value();
    });
};

/**
 * Retrieves a wallet and checks authorization to view the wallet using
 * walletId and totpCode.
 * 
 * @param  {string} username
 * @param  {string} walletId base64-encoded key used to authorize access.
 * @param  {string} [totpCode] optional totp-code
 * @return {Promise.<object>} A promise that resolved to the found and authorized wallet object
 */
walletV2.getWithAuthorization = function(username, walletId, totpCode) {
  return walletV2.get(username)
    .then(function(wallet) {
      if(!walletV2.isTotpEnabled(wallet)) {
        return wallet;
      }
      var isValid = totp.verify(totpCode, wallet.totpKey);
      
      return isValid ? wallet : Promise.reject(new errors.Forbidden());
    })
    .then(function(wallet) {
      var hashed  = walletV2.hashWalletId(walletId);
      var isValid = scmp(hashed, wallet.walletId);
      
      return isValid ? wallet : Promise.reject(new errors.Forbidden());
    })
    .tap(function(wallet) {
      return walletV2.clearTotpRemovalRequestIfPossible(wallet);
    });
};

walletV2.getByWalletId = function(username, walletId) {

  return db("wallets_v2")
    .where("username", username)
    .where("walletId", walletV2.hashWalletId(walletId))
    .whereNull("deletedAt")
    .select()
    .then(function(results) {
      var result = _.first(results);
      if (!result) {
        throw new errors.RecordNotFound();
      }
      return result;
    });
};


walletV2.getPublicKey = function(username, walletId) {
  return walletV2.getByWalletId(username, walletId).get('publicKey');
};

walletV2.create = function(attrs) {

  return Promise.resolve(attrs)
    .then(validate.present("username"))
    .then(validate.present("walletId"))
    .then(validate.present("salt"))
    .then(validate.present("kdfParams"))
    .then(validate.present("publicKey"))
    .then(validate.present("mainData"))
    .then(validate.present("keychainData"))
    .then(validate.hash("mainData"))
    .then(validate.hash("keychainData"))
    .then(validate.json("kdfParams"))
    .then(validate.username("username"))
    .then(validate.byteLength("salt", 16))
    .then(validate.byteLength("walletId", 32))
    .then(validate.byteLength("publicKey", 32))
    .tap(function(attrs) {
      if(conf.get("requireUsernameProofs") === true) {
        return usernameProofs.validate(attrs.publicKey, attrs.usernameProof);
      }
      return true;
    })
    .then(function(attrs) {
      var insertAttrs         = _.omit(attrs, ['mainDataHash', 'keychainDataHash', 'usernameProof']);
      insertAttrs.walletId    = hash.sha2(insertAttrs.walletId);
      insertAttrs.createdAt   = new Date();
      insertAttrs.updatedAt   = insertAttrs.createdAt;
      insertAttrs.lockVersion = 0;

      return db("wallets_v2").insert(insertAttrs)
        .then(function(wallet) {
          attrs.wallet = wallet;
          return Promise.resolve(attrs);
        })
        .catch(function(e) {
          if(e.errno === 1062) {
            e = new errors.DuplicateRecord(e.message);
          }

          return Promise.reject(e);
        });
    }).tap(function(attrs) {
      if (!(attrs.usernameProof && attrs.usernameProof.migrated)) {
        return;
      }

      var usernameWithoutDomain = stex.fbgive.usernameWithoutDomain(attrs.username);
      email.sendEmail(usernameWithoutDomain, 'wallet_upgrade');
    });
};

walletV2.update = function(id, lockVersion, attrs) {
  //TODO: we should validate the updates, including checking hashes etc.

  return Promise.resolve(attrs)
    .then(validate.hash('mainData',     {allowBlank: true}))
    .then(validate.hash('keychainData', {allowBlank: true}))
    .then(function(attrs) {
      var updateAttrs         =  _.omit(attrs, ['mainDataHash', 'keychainDataHash']);
      updateAttrs.updatedAt   = new Date();
      updateAttrs.lockVersion = lockVersion + 1;

      return db("wallets_v2")
        .where({id:id, lockVersion:lockVersion})
        .whereNull("deletedAt")
        .update(updateAttrs)
        .then(function (changedRows) {
          if (changedRows === 0) {
            return Promise.reject(new errors.RecordNotFound());
          } else {
            return {newLockVersion: updateAttrs.lockVersion};
          }
        });
    });
};

walletV2.delete = function(id, lockVersion) {
  return db("wallets_v2")
    .where({id:id, lockVersion:lockVersion})
    .update('deletedAt', new Date())
    .then(function (changedRows) {
      if (changedRows === 0) {
        return Promise.reject(new errors.RecordNotFound());
      }
    });
};

walletV2.enableRecovery = function(id, lockVersion, attrs) {
  return Promise.resolve(attrs)
    .then(validate.present('recoveryId'))
    .then(validate.present('recoveryData'))
    .then(function(attrs) {
      attrs.lockVersion = lockVersion + 1;

      return db("wallets_v2")
        .where({id:id, lockVersion:lockVersion})
        .whereNull("deletedAt")
        .update(attrs)
        .then(function (changedRows) {
          if (changedRows === 0) {
            return Promise.reject(new errors.RecordNotFound());
          } else {
            return {newLockVersion: attrs.lockVersion};
          }
        });
    });
};

walletV2.enableTotp = function(id, lockVersion, totpKey, totpCode) {
  if(_.isEmpty(totpKey)) {
    return Promise.reject(new walletV2.errors.InvalidTotpKey());
  }


  //Possible TODO: verify the length of the buffer is of a certain size

  var isValidKey = totp.verify(totpCode, totpKey);

  if(!isValidKey) {
    return Promise.reject(new walletV2.errors.InvalidTotpCode());
  }

  return walletV2.update(id, lockVersion, {totpKey:totpKey, totpDisabledAt:null});
};

walletV2.disableTotp = function(id, lockVersion, totpKey, totpCode) {
  if(_.isEmpty(totpCode)) {
    return Promise.reject(new walletV2.errors.InvalidTotpCode());
  }

  var isValidKey = totp.verify(totpCode, totpKey);

  if(!isValidKey) {
    return Promise.reject(new walletV2.errors.InvalidTotpCode());
  }

  return walletV2.update(id, lockVersion, {totpKey: null});
};

walletV2.initiateTotpGracePeriod = function(id) {
  var now         = (new Date()).getTime();
  var gracePeriod = new Duration(conf.get("totpDisableGracePeriod"));
  var disableAt   = new Date(now + gracePeriod);

  return db("wallets_v2")
    .where({id: id})
    .whereNull("deletedAt")
    .update({totpDisabledAt: disableAt})
    .then(function () {
      return Promise.resolve();
    });
};

walletV2.clearTotpRemovalRequestIfPossible = function(wallet) {
  if(!wallet.totpDisabledAt) {
    return Promise.resolve();
  } else {
    var now        = Math.floor((new Date()).getTime() / 1000);
    var disabledAt = Math.floor(wallet.totpDisabledAt.getTime() / 1000);
    var updateData;
    if (disabledAt > now) {
      // User logged in with a TOTP code => set the totpDisabledAt to null
      updateData = {
        totpDisabledAt: null
      };
    } else {
      // User logged in after the TOTP grace period (without a TOTP code)=> clear totpKey too
      updateData = {
        totpDisabledAt: null,
        totpKey: null
      };
    }

    return db("wallets_v2")
      .where({id:wallet.id})
      .whereNull("deletedAt")
      .update(updateData);
  }
};

walletV2.isTotpEnabled = function(wallet) {
  if (_.isEmpty(wallet.totpKey)){ 
    return false;
  }

  if (wallet.totpDisabledAt instanceof Date) {
    var now        = Math.floor((new Date()).getTime() / 1000);
    var disabledAt = Math.floor(wallet.totpDisabledAt.getTime() / 1000);
    return disabledAt > now;
  } else {
    return true;
  }
};