/*

Copyright 2008-2015 Clipperz Srl

This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.

* Clipperz is free software: you can redistribute it and/or modify it
  under the terms of the GNU Affero General Public License as published
  by the Free Software Foundation, either version 3 of the License, or 
  (at your option) any later version.

* Clipperz 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 Affero General Public License for more details.

* You should have received a copy of the GNU Affero General Public
  License along with Clipperz. If not, see http://www.gnu.org/licenses/.

*/

"use strict";

if (typeof(Clipperz) == 'undefined') { Clipperz = {}; }
if (typeof(Clipperz.PM) == 'undefined') { Clipperz.PM = {}; }

//-----------------------------------------------------------------------------
//
//		Abstract   C O N N E C T I O N   class
//
//-----------------------------------------------------------------------------

Clipperz.PM.Connection = function (args) {
	args = args || {};

	this._proxy = args.proxy || Clipperz.PM.Proxy.defaultProxy;
	this._getCredentialsFunction = args.getCredentialsFunction;

	this._clipperz_pm_crypto_version = null;
	this._connectionId = null;
	this._sharedSecret = null;
	this._serverLockValue = null;

	return this;
}

Clipperz.PM.Connection.prototype = MochiKit.Base.update(null, {

	'toString': function() {
		return "Connection [" + this.version() + "]";
	},
	
	//=========================================================================

	'version': function() {
		throw Clipperz.Base.exception.AbstractMethod;
	},
	
	'clipperz_pm_crypto_version': function() {
		if (this._clipperz_pm_crypto_version == null) {
			var connectionVersions;
			var	versions;
			var	version;
			var i, c;

			version = null;
			connectionVersions = Clipperz.PM.Connection.communicationProtocol.versions;
			versions = MochiKit.Base.keys(connectionVersions);
			c = versions.length;
			for (i=0; i<c; i++) {
				if (! (versions[i] == 'current')) {
					if (this instanceof connectionVersions[versions[i]]) {
						version = versions[i];
					};
				}
			}
			
			this._clipperz_pm_crypto_version = version;
		}
		
		return this._clipperz_pm_crypto_version;
	},

	//-------------------------------------------------------------------------

	'defaultErrorHandler': function(anErrorString, anException) {
//		Clipperz.logError("### Connection.defaultErrorHandler: " + anErrorString, anException);
		Clipperz.logError("### Connection.defaultErrorHandler: " + anErrorString + " (" + anException + ")");
	},
	
	//-------------------------------------------------------------------------

	'getCredentialsFunction': function () {
		return this._getCredentialsFunction;
	},

	'normalizedCredentials': function(someValues) {
		throw Clipperz.Base.exception.AbstractMethod;
	},
	
	//=========================================================================

	'proxy': function () {
		return this._proxy;
	},

	//=========================================================================

	'register': function () {
		throw Clipperz.Base.exception.AbstractMethod;
	},

	'login': function() {
		throw Clipperz.Base.exception.AbstractMethod;
	},

	//-------------------------------------------------------------------------

	'message': function(someArguments, aCallback) {
		throw Clipperz.Base.exception.AbstractMethod;
	},

	//-------------------------------------------------------------------------

	'serverSideUserCredentials': function() {
		throw Clipperz.Base.exception.AbstractMethod;
	},

	//-------------------------------------------------------------------------

	'uploadAttachment': function(someArguments, aProgressCallback) {
		throw Clipperz.Base.exception.AbstractMethod;
	},

	'downloadAttachment': function(someArguments, aProgressCallback) {
		throw Clipperz.Base.exception.AbstractMethod;
	},

	//=========================================================================

	'sharedSecret': function () {
		return this._sharedSecret;
	},
	
	'setSharedSecret': function (aValue) {
		this._sharedSecret = aValue;
	},

	//-------------------------------------------------------------------------

	'connectionId': function() {
		return this._connectionId;
	},
	
	'setConnectionId': function(aValue) {
		this._connectionId = aValue;
	},

	//-------------------------------------------------------------------------

	'serverLockValue': function () {
		return this._serverLockValue;
	},
	
	'setServerLockValue': function (aValue) {
		this._serverLockValue = aValue;
	},

	//=========================================================================
/*
//	TODO: ?????
	'oneTimePassword': function() {
		return this._oneTimePassword;
	},
	
	'setOneTimePassword': function(aValue) {
		this._oneTimePassword = aValue;
	},
*/
	//=========================================================================

	'reset': function() {
		this.setSharedSecret(null);
		this.setConnectionId(null);
	},
	
	//=========================================================================
	__syntaxFix__: "syntax fix"

}
);


if (typeof(Clipperz.PM.Connection.SRP) == 'undefined') { Clipperz.PM.Connection.SRP = {}; }
//-----------------------------------------------------------------------------
//
//		S R P [ 1 . 0 ]    C O N N E C T I O N   class
//
//-----------------------------------------------------------------------------

Clipperz.PM.Connection.SRP['1.0'] = function (args) {
	Clipperz.PM.Connection.call(this, args);
	
	return this;
}

Clipperz.PM.Connection.SRP['1.0'].prototype = MochiKit.Base.update(new Clipperz.PM.Connection(), {

	'version': function() {
		return '1.0';
	},

	//=========================================================================
	
	'register': function (someUserData) {
		var	deferredResult;
		var cryptoVersion;
		var srpConnection;

		cryptoVersion = this.clipperz_pm_crypto_version();

		deferredResult = new Clipperz.Async.Deferred("Connection.registerWithVersion", {trace:false});
		deferredResult.collectResults({
			'credentials': [
				this.getCredentialsFunction(),
				MochiKit.Base.method(this, 'normalizedCredentials'),
				MochiKit.Base.bind(function(someCredentials) {
					var srpConnection;
					var result;

					srpConnection = new Clipperz.Crypto.SRP.Connection({ C:someCredentials['username'], P:someCredentials['password'], hash:this.hash() });
					result = srpConnection.serverSideCredentials();
					result['version'] = Clipperz.PM.Connection.communicationProtocol.currentVersion;

					return result;
				}, this)
			],
			'user':		MochiKit.Base.partial(MochiKit.Async.succeed, someUserData),
			'version':	MochiKit.Base.partial(MochiKit.Async.succeed, Clipperz.PM.Connection.communicationProtocol.currentVersion),
			'message':	MochiKit.Base.partial(MochiKit.Async.succeed, 'completeRegistration')
		});
//		deferredResult.addCallbackPass(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'advanceProgress');
		deferredResult.addMethod(this.proxy(), 'registration');
//		deferredResult.addCallbackPass(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'advanceProgress');

		deferredResult.callback();
		
		return deferredResult;
	},

	//-------------------------------------------------------------------------

	'updateCredentials': function (someData) {
		var	deferredResult;

		deferredResult = new Clipperz.Async.Deferred("Connection.updateCredentials", {trace:false});
		deferredResult.collectResults({
			'credentials': [
				MochiKit.Base.method(this, 'normalizedCredentials', {username:someData['newUsername'], password:someData['newPassphrase']}),
				MochiKit.Base.bind(function(someCredentials) {
					var srpConnection;
					var result;

					srpConnection = new Clipperz.Crypto.SRP.Connection({ C:someCredentials['username'], P:someCredentials['password'], hash:this.hash() });
					result = srpConnection.serverSideCredentials();
					result['version'] = Clipperz.PM.Connection.communicationProtocol.currentVersion;

					return result;
				}, this)
			],
			'user':		MochiKit.Base.partial(MochiKit.Async.succeed, someData['user']),
			'oneTimePasswords': MochiKit.Base.partial(MochiKit.Async.succeed, someData['oneTimePasswords'])
		});
		deferredResult.addMethod(this, 'message', 'upgradeUserCredentials');
		deferredResult.callback();
		
		return deferredResult;
	},

	//=========================================================================

	'redeemOneTimePassword': function (someParameters) {
/*
	//=========================================================================
		//	LOGIN WITH PASSPHRASE, extracted from the TRUNK version (LoginPanel.js)
		deferredResult.addCallback(function(anUsername, aOneTimePassword) {
			var args;
			
			args = {
				'message': 'oneTimePassword',
				'version': Clipperz.PM.Crypto.communicationProtocol.currentVersion,
				'parameters': {
					'oneTimePasswordKey': Clipperz.PM.DataModel.OneTimePassword.computeKeyWithUsernameAndPassword(anUsername, aOneTimePassword),
					'oneTimePasswordKeyChecksum': Clipperz.PM.DataModel.OneTimePassword.computeKeyChecksumWithUsernameAndPassword(anUsername, aOneTimePassword)
				}
			}
			
			return args;
		}, anUsername, oneTimePassword);
		deferredResult.addCallback(Clipperz.NotificationCenter.deferredNotification, this, 'updatedProgressState', 'OTP_login_loadingOTP');
		deferredResult.addCallback(MochiKit.Base.method(Clipperz.PM.Proxy.defaultProxy, 'handshake'));
		deferredResult.addCallback(Clipperz.NotificationCenter.deferredNotification, this, 'updatedProgressState', 'OTP_login_extractingPassphrase');
		deferredResult.addCallback(function(aResult) {
			return Clipperz.PM.Crypto.deferredDecrypt(oneTimePassword, aResult['data'], aResult['version']);
		});
		deferredResult.addCallback(function(aResult) {
			return (new Clipperz.ByteArray().appendBase64String(aResult['passphrase'])).asString();
		});
		deferredResult.addMethod(this, 'doLoginWithUsernameAndPassphrase', anUsername),
*/
		var args;
		var normalizedOTP;

		normalizedOTP = Clipperz.PM.DataModel.OneTimePassword.normalizedOneTimePassword(someParameters['password']);

		args = {
			'message': 'oneTimePassword',
			'version': Clipperz.PM.Connection.communicationProtocol.currentVersion,
			'parameters': {
				'oneTimePasswordKey':			Clipperz.PM.DataModel.OneTimePassword.computeKeyWithPassword(normalizedOTP),
				'oneTimePasswordKeyChecksum':	Clipperz.PM.DataModel.OneTimePassword.computeKeyChecksumWithUsernameAndPassword(someParameters['username'], normalizedOTP)
			}
		}

		return Clipperz.Async.callbacks("Connction.redeemOneTimePassword", [
			MochiKit.Base.method(this.proxy(), 'handshake', args),
			function(aResult) {
				return Clipperz.PM.Crypto.deferredDecrypt({
					value:	aResult['data'],
					key:	normalizedOTP,
					version:aResult['version']
				});
			},
			function(aResult) {
				return (new Clipperz.ByteArray().appendBase64String(aResult['passphrase'])).asString();
			}			
		], {trace:false})
	},

	'login': function(isReconnecting) {
		var	deferredResult;
		var cryptoVersion;
		var srpConnection;

		cryptoVersion = this.clipperz_pm_crypto_version();
		deferredResult = new Clipperz.Async.Deferred("Connection.login", {trace:false});
		deferredResult.addCallback(this.getCredentialsFunction());
		deferredResult.addMethod(this, 'normalizedCredentials');
//		deferredResult.addCallbackPass(MochiKit.Signal.signal, this, 'updatedProgressState', 'connection_sendingCredentials');
//		deferredResult.addCallbackPass(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'advanceProgress');
		deferredResult.addCallback(MochiKit.Base.bind(function(someCredentials) {
			srpConnection = new Clipperz.Crypto.SRP.Connection({ C:someCredentials['username'], P:someCredentials['password'], hash:this.hash() });
		}, this));
		deferredResult.addCallback(function() {
			var result;

			result = {
				message: 'connect',
				version: cryptoVersion,
				parameters: {
					C: srpConnection.C(),
					A: srpConnection.A().asString(16)
//					reconnecting: this.connectionId()	
				}
			};

//	TODO: ?????			
//			if (isReconnecting == true) {
//				args.parameters['reconnecting'] = aConnection.connectionId();
//			}

			return result;
		});
		deferredResult.addMethod(this.proxy(), 'handshake');
		deferredResult.addCallback(function(someParameters) {
			var result;

			srpConnection.set_s(new Clipperz.Crypto.BigInt(someParameters['s'], 16));
			srpConnection.set_B(new Clipperz.Crypto.BigInt(someParameters['B'], 16));

//	TODO: ?????
//			if (typeof(someParameters['oneTimePassword']) != 'undefined') {
//				this.setOneTimePassword(someParameters['oneTimePassword']);
//			}
			
			result = {
				message: 'credentialCheck',
				version: cryptoVersion,
				parameters: {
					M1: srpConnection.M1()
				}
			};

			return result;
		});
		deferredResult.addMethod(this.proxy(), 'handshake');
		deferredResult.addCallback(function(someParameters) {
			var result;

			if (someParameters['M2'] == srpConnection.M2()) {
				result = MochiKit.Async.succeed(someParameters);
			} else {
				result = MochiKit.Async.fail(Clipperz.PM.Connection.exception.WrongChecksum);
			}

			return result;
		});
		deferredResult.addCallback(MochiKit.Base.bind(function(someParameters) {
			this.setConnectionId(someParameters['connectionId']);
			this.setSharedSecret(srpConnection.K());
//	TODO: ?????			
//			if (this.oneTimePassword() != null) {
///	??			result = this.user().oneTimePasswordManager().archiveOneTimePassword(this.oneTimePassword()));
//			}

			if ((isReconnecting == true) && (this.serverLockValue() != someParameters['lock'])) {
				throw Clipperz.PM.Connection.exception.StaleData;
			} else {
				this.setServerLockValue(someParameters['lock']);
			}

			return someParameters;
		}, this));
		deferredResult.callback();
		
		return deferredResult;
	},

	//=========================================================================

	'logout': function() {
		var	deferredResult;

		deferredResult = new Clipperz.Async.Deferred("Connection.login", {trace:false});
		deferredResult.addMethod(this, 'setSharedSecret');
		deferredResult.addMethod(this.proxy(), 'logout', {});
		deferredResult.addErrback(function (aResult) { Clipperz.log("Ignored error while logging out"); return {}; });
		deferredResult.callback();
		
		return deferredResult;
	},

	//=========================================================================

	'ping': function () {
		//	TODO: ping the server in order to have a valid session
	},

	//=========================================================================

	'message': function(aMessageName, someParameters, someOptionalParameters) {
		var args;
		var parameters;

		parameters = someParameters || {};
		if (typeof(parameters['user']) != 'undefined') {
			parameters['user']['lock'] = this.serverLockValue();
		}

		args = {
			message: aMessageName,
			srpSharedSecret: this.sharedSecret(),
//			parameters: (someParameters || {})
			parameters: parameters
		}

		return this.sendMessage(args, someOptionalParameters);
	},

	//-------------------------------------------------------------------------

	'sendMessage': function(someArguments, someOptionalParameters) {
		var	deferredResult;

		deferredResult = new Clipperz.Async.Deferred("Connection.sendMessage", {trace:false});
		deferredResult.addMethod(this.proxy(), 'message', someArguments, someOptionalParameters);
		deferredResult.addCallback(MochiKit.Base.bind(function(res) {
			if (typeof(res['lock']) != 'undefined') {
				this.setServerLockValue(res['lock']);
			}
			return res;
		}, this));

		deferredResult.addErrback(MochiKit.Base.method(this, 'messageExceptionHandler'), someArguments);
		deferredResult.callback();
		
		return deferredResult
	},
	
	//-------------------------------------------------------------------------

	'messageExceptionHandler': function(anOriginalMessageArguments, anError) {
		var result;

Clipperz.log(">>> Connection.messageExceptionHandler:  " + anError.message, anError);
		if (anError instanceof MochiKit.Async.CancelledError) {
			result = anError;
		} else if (typeof(anError.req) == 'undefined') {
			result = anError;
		} else {
			var	errorPayload;

			errorPayload = Clipperz.Base.evalJSON(anError.req.responseText);
			if ((errorPayload.message == 'Trying to communicate without an active connection')	||
				(errorPayload.message == 'No tollManager available for current session')		||
				(errorPayload.message == 'HashCash verification failed. The provided toll is not valid.')
			) {
				result = this.reestablishConnection(anOriginalMessageArguments);
			} else if (errorPayload.message == 'Session with stale data') {
				MochiKit.Signal.signal(this, 'EXCEPTION');
			} else {
				result = errorPayload;
			}
		}
Clipperz.log("<<< Connection.messageExceptionHandler")

		return result;;
	},
	
	//=========================================================================

	'uploadAttachment': function(someArguments, aProgressCallback) {
		return Clipperz.Async.callbacks("Connction.uploadAttachment", [
			MochiKit.Base.method(this, 'message', 'echo', {'echo':"echo"}),
			MochiKit.Base.bind(function(){ return this.sharedSecret()}, this),
			MochiKit.Base.method(this.proxy(), 'uploadAttachment', someArguments, aProgressCallback/*, this.sharedSecret()*/),
		], {trace:false});
	},

	'downloadAttachment': function(someArguments, aProgressCallback) {
		return Clipperz.Async.callbacks("Connction.uploadAttachment", [
			MochiKit.Base.method(this, 'message', 'echo', {'echo':"echo"}),
			MochiKit.Base.bind(function(){ return this.sharedSecret()}, this),
			MochiKit.Base.method(this.proxy(), 'downloadAttachment', someArguments, aProgressCallback/*, this.sharedSecret()*/),
		], {trace:false});
	},

	//=========================================================================

	'reestablishConnection': function(anOriginalMessageArguments) {
		var deferredResult;

		deferredResult = new Clipperz.Async.Deferred("Connection.reestablishConnection");
		deferredResult.addMethod(this, 'reset');
		deferredResult.addMethod(this, 'login', true);
		deferredResult.addCallback(MochiKit.Base.bind(function(aMessage) {
			aMessage['srpSharedSecret'] = this.sharedSecret();
			return aMessage;
		}, this), anOriginalMessageArguments);
		deferredResult.addMethod(this, 'sendMessage');
		deferredResult.addErrback(MochiKit.Signal.signal, this, 'EXCEPTION', null);
		deferredResult.callback();

		return deferredResult;
	},

	//=========================================================================

	'serverSideUserCredentials': function(aUsername, aPassword) {
		var	result;
		var	newSrpConnection;
		var normalizedAttributes;
		
		normalizedAttributes = this.normalizedCredentials({username:aUsername, password:aPassword});
		newSrpConnection = new Clipperz.Crypto.SRP.Connection({ C:normalizedAttributes['username'], P:normalizedAttributes['password'], hash:this.hash() });
		result = newSrpConnection.serverSideCredentials();
		result['version'] = this.clipperz_pm_crypto_version();

		return result;
	},

	//=========================================================================

	'normalizedCredentials': function(someValues) {
		var result;

		result = {}
		result['username'] = this.hash()(new Clipperz.ByteArray(someValues['username'])).toHexString().substring(2);
		result['password'] = this.hash()(new Clipperz.ByteArray(someValues['password'] + someValues['username'])).toHexString().substring(2);

		return result;
	},

	//-----------------------------------------------------------------------------

	'hash': function() {
		return Clipperz.PM.Crypto.encryptingFunctions.versions['0.1'].hash;
	},
	
	//-----------------------------------------------------------------------------
	__syntaxFix__: "syntax fix"

});



//-----------------------------------------------------------------------------
//
//		S R P [ 1 . 1 ]    C O N N E C T I O N   class
//
//-----------------------------------------------------------------------------

Clipperz.PM.Connection.SRP['1.1'] = function (args) {
	Clipperz.PM.Connection.SRP['1.0'].call(this, args);
	
	return this;
}

Clipperz.PM.Connection.SRP['1.1'].prototype = MochiKit.Base.update(new Clipperz.PM.Connection.SRP['1.0'](), {

	'version': function() {
		return '1.1';
	},

	//-----------------------------------------------------------------------------

	'normalizedCredentials': function(someValues) {
		var result;
		
		result = {}
		result['username'] = this.hash()(new Clipperz.ByteArray(someValues['username'] + someValues['password'])).toHexString().substring(2);
		result['password'] = this.hash()(new Clipperz.ByteArray(someValues['password'] + someValues['username'])).toHexString().substring(2);

		return result;
	},

	//-----------------------------------------------------------------------------

	'hash': function() {
		return Clipperz.PM.Crypto.encryptingFunctions.versions['0.2'].hash;
	},

	//-----------------------------------------------------------------------------
	__syntaxFix__: "syntax fix"

});

Clipperz.PM.Connection.exception = {
	WrongChecksum:		new MochiKit.Base.NamedError("Clipperz.ByteArray.exception.InvalidValue"),
	StaleData:			new MochiKit.Base.NamedError("Stale data"),
	UnexpectedRequest:	new MochiKit.Base.NamedError("Clipperz.ByteArray.exception.UnexpectedRequest")
};


Clipperz.PM.Connection.communicationProtocol = {
	'currentVersion': '0.2',
	'versions': {
		'0.1': Clipperz.PM.Connection.SRP['1.0'],	//Clipperz.Crypto.SRP.versions['1.0'].Connection,
		'0.2': Clipperz.PM.Connection.SRP['1.1']	//Clipperz.Crypto.SRP.versions['1.1'].Connection
	},
	'fallbackVersions': {
//		'current':	'0.1',
		'0.2':		'0.1',
		'0.1':		null
	}
};

MochiKit.Base.update(Clipperz.PM.Connection.communicationProtocol.versions, {
	'current': Clipperz.PM.Connection.communicationProtocol.versions[Clipperz.PM.Connection.communicationProtocol.currentVersion]
});

MochiKit.Base.update(Clipperz.PM.Connection.communicationProtocol.fallbackVersions, {
	'current': Clipperz.PM.Connection.communicationProtocol.fallbackVersions[Clipperz.PM.Connection.communicationProtocol.currentVersion]
});