Skip to content

Commit

Permalink
Create the auth module
Browse files Browse the repository at this point in the history
  • Loading branch information
enricostara committed Nov 28, 2014
1 parent bab1af0 commit 1c91cdb
Show file tree
Hide file tree
Showing 5 changed files with 390 additions and 305 deletions.
8 changes: 8 additions & 0 deletions lib/auth/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// telegram.link
// Copyright 2014 Enrico Stara '[email protected]'
// Released under the MIT License
// https://github.com/enricostara/telegram-mt-node

exports.RequestPQ = require('./request-pq');
exports.RequestDHParams = require('./request-dh-params');
exports.SetClientDHParams = require('./set-client-dh-params');
166 changes: 166 additions & 0 deletions lib/auth/request-dh-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// telegram.link
// Copyright 2014 Enrico Stara '[email protected]'
// Released under the MIT License
// https://github.com/enricostara/telegram-mt-node

// Export the class
module.exports = exports = RequestDHParams;

// Import dependencies
var flow = require('get-flow');
var logger = require('get-log')('auth.RequestDHParams');
var TypeObject = require('telegram-tl-node').TypeObject;
var mtproto = require('telegram-mt-node');
var utility = mtproto.utility;
var security = mtproto.security;

// Requires a callback function and the connection
function RequestDHParams(callback, context) {
flow.runSeries([
createPQInnerData,
encryptPQInnerDataWithRSA,
requestDHParams,
decryptDHParams,
deserializeDHInnerData,
], callback, context);
};


// Create the pq_inner_data buffer
function createPQInnerData(context) {
var resPQ = context.resPQ;
var newNonce = utility.createNonce(32);
var pqInnerData = new mtproto.type.P_q_inner_data({props: {
pq: resPQ.pq,
p: context.pBuffer,
q: context.qBuffer,
nonce: resPQ.nonce,
server_nonce: resPQ.server_nonce,
new_nonce: newNonce
}}).serialize();
context.pqInnerData = pqInnerData;
context.newNonce = newNonce;
return context;
}

// Encrypt the pq_inner_data with RSA
function encryptPQInnerDataWithRSA(context) {
// Create the data with hash to be encrypt
var hash = utility.createSHA1Hash(context.pqInnerData);
var dataWithHash = Buffer.concat([hash, context.pqInnerData]);
if (logger.isDebugEnabled()) {
logger.debug('Data to be encrypted contains: hash(%s), pqInnerData(%s), total length %s',
hash.length, context.pqInnerData.length, dataWithHash.length);
}
// Encrypt data with RSA
context.encryptedData = security.cipher.rsaEncrypt(dataWithHash, context.publicKey);
return context;
}

// Request server DH parameters
function requestDHParams(callback, context) {
var resPQ = context.resPQ;
mtproto.service.req_DH_params({
props: {
nonce: resPQ.nonce,
server_nonce: resPQ.server_nonce,
p: context.pBuffer,
q: context.qBuffer,
public_key_fingerprint: context.fingerprint,
encrypted_data: context.encryptedData
},
conn: context.connection,
callback: function (ex, serverDHParams, duration) {
if (ex) {
logger.error(ex);
if (callback) {
callback(ex);
}
} else {
if (serverDHParams.typeName === 'mtproto.type.Server_DH_params_ok') {
if (logger.isDebugEnabled()) {
logger.debug('\'Server_DH_params_ok\' received from Telegram.');
}
context.serverDHParams = serverDHParams;
context.reqDHDuration = duration;
callback(null, context);
} else if (serverDHParams.typeName === 'mtproto.type.Server_DH_params_ko') {
logger.warn('\'Server_DH_params_ko\' received from Telegram!');
callback(createError(JSON.stringify(serverDHParams), 'EDHPARAMKO'));
} else {
var msg = 'Unknown error received from Telegram!';
logger.error(msg);
callback(createError(msg, 'EUNKNOWN'));
}
}
}
});
}

// Decrypt DH parameters and synch the local time with the server time
function decryptDHParams(context) {
var newNonce = TypeObject.stringValue2Buffer(context.newNonce, 32);
var serverNonce = TypeObject.stringValue2Buffer(context.resPQ.server_nonce, 16);
if (logger.isDebugEnabled()) {
logger.debug('newNonce = %s, serverNonce = %s', newNonce.toString('hex'), serverNonce.toString('hex'));
}
var hashNS = utility.createSHA1Hash([newNonce, serverNonce]);
var hashSN = utility.createSHA1Hash([serverNonce, newNonce]);
var hashNN = utility.createSHA1Hash([newNonce, newNonce]);
if (logger.isDebugEnabled()) {
logger.debug('hashNS = %s, hashSN = %s, hashNN = %s',
hashNS.toString('hex'), hashSN.toString('hex'), hashNN.toString('hex'));
}
// Create the AES key
var aesKey = Buffer.concat([hashNS, hashSN.slice(0, 12)]);
var aesIv = Buffer.concat([Buffer.concat([hashSN.slice(12), hashNN]), newNonce.slice(0, 4)]);
if (logger.isDebugEnabled()) {
logger.debug('aesKey = %s, aesIv = %s', aesKey.toString('hex'), aesIv.toString('hex'));
}
// Decrypt the message
var answerWithHash = security.cipher.aesDecrypt(
context.serverDHParams.encrypted_answer,
aesKey,
aesIv
);
context.decryptedDHParams = answerWithHash;
// Save AES key
context.aes = {key: aesKey, iv: aesIv};
return context;
}

// De-serialize the server DH inner data
function deserializeDHInnerData(context) {
var decryptedDHParamsWithHash = context.decryptedDHParams;
if (logger.isDebugEnabled()) {
logger.debug('decryptedDHParamsWithHash(%s) = %s', decryptedDHParamsWithHash.length, decryptedDHParamsWithHash.toString('hex'));
}
var decryptedDHParams = decryptedDHParamsWithHash.slice(20, 564 + 20);
if (logger.isDebugEnabled()) {
logger.debug('decryptedDHParams(%s) = %s', decryptedDHParams.length, decryptedDHParams.toString('hex'));
}
var serverDHInnerData = new mtproto.type.Server_DH_inner_data({
buffer: decryptedDHParams
}).deserialize();
if (logger.isDebugEnabled()) {
logger.debug('serverDHInnerData = %s obtained in %sms', JSON.stringify(serverDHInnerData), context.reqDHDuration);
}
// Check if the nonces are consistent
if (serverDHInnerData.nonce !== context.serverDHParams.nonce) {
throw createError('Nonce mismatch %s != %s', context.serverDHParams.nonce, serverDHInnerData.nonce);
}
if (serverDHInnerData.server_nonce !== context.serverDHParams.server_nonce) {
throw createError('ServerNonce mismatch %s != %s', context.serverDHParams.server_nonce, serverDHInnerData.server_nonce);
}
// Synch the local time with the server time
mtproto.time.timeSynchronization(serverDHInnerData.server_time * 1000, context.reqDHDuration);
context.serverDHInnerData = serverDHInnerData;
return context;
}


function createError(msg, code) {
var error = new Error(msg);
error.code = code;
return error;
}
92 changes: 92 additions & 0 deletions lib/auth/request-pq.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// telegram.link
// Copyright 2014 Enrico Stara '[email protected]'
// Released under the MIT License
// https://github.com/enricostara/telegram-mt-node

// Export the module
module.exports = exports = RequestPQ;

// Import dependencies
var flow = require('get-flow');
var logger = require('get-log')('auth.RequestPQ');
var mtproto = require('telegram-mt-node');
var utility = mtproto.utility;
var security = mtproto.security;

// Requires a callback function and the connection
function RequestPQ(callback, connection) {
flow.runSeries([
requestPQ,
findPAndQ,
findPublicKey,
], callback, connection);
};

// Request a PQ pair number
function requestPQ(callback, connection) {
// Create a nonce for the client
var clientNonce = utility.createNonce(16);
mtproto.service.req_pq({
props: {
nonce: clientNonce
},
conn: connection,
callback: function (ex, resPQ) {
if (clientNonce === resPQ.nonce) {
var context = {
resPQ: resPQ,
connection: connection
};
callback(null, context);
} else {
callback(createError('Nonce mismatch.', 'ENONCE'));
}
}
});
}

// Find the P and Q prime numbers
function findPAndQ(context) {
var pqFinder = new security.PQFinder(context.resPQ.pq);
if (logger.isDebugEnabled()) {
logger.debug('Start finding P and Q, with PQ = %s', pqFinder.getPQPairNumber());
}
var pq = pqFinder.findPQ();
if (logger.isDebugEnabled()) {
logger.debug('Found P = %s and Q = %s', pq[0], pq[1]);
}
context.pBuffer = pqFinder.getPQAsBuffer()[0];
context.qBuffer = pqFinder.getPQAsBuffer()[1];
return context;
}

// Find the correct Public Key using fingerprint from server response
function findPublicKey(context) {
var fingerprints = context.resPQ.server_public_key_fingerprints.getList();
if (logger.isDebugEnabled()) {
logger.debug('Public keys fingerprints from server: %s', fingerprints);
}
for (var i = 0; i < fingerprints.length; i++) {
var fingerprint = fingerprints[i];
if (logger.isDebugEnabled()) {
logger.debug('Searching fingerprint %s in store', fingerprint);
}
var publicKey = security.PublicKey.retrieveKey(fingerprint);
if (publicKey) {
if (logger.isDebugEnabled()) {
logger.debug('Fingerprint %s found in keyStore.', fingerprint);
}
context.fingerprint = fingerprint;
context.publicKey = publicKey;
return context;
}
}
throw createError('Fingerprints from server not found in keyStore.', 'EFINGERNOTFOUND');
}

function createError(msg, code) {
var error = new Error(msg);
error.code = code;
return error;
}

112 changes: 112 additions & 0 deletions lib/auth/set-client-dh-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// telegram.link
// Copyright 2014 Enrico Stara '[email protected]'
// Released under the MIT License
// https://github.com/enricostara/telegram-mt-node

// Export the class
module.exports = exports = SetClientDHParams;

// Import dependencies
var flow = require('get-flow');
var logger = require('get-log')('auth.SetClientDHParams');
var mtproto = require('telegram-mt-node');
var utility = mtproto.utility;
var security = mtproto.security;

// Requires a callback function and the connection
function SetClientDHParams(callback, context) {
flow.runSeries([
createClientDHInnerData,
encryptClientDHInnerDataWithAES,
setClientDHParams
], callback, context);
};

// Calculate the g_b = pow(g, b) mod dh_prime
// Create the client DH inner data
function createClientDHInnerData(context) {
var retryCount = 0;
if (logger.isDebugEnabled()) {
logger.debug('Start calculating g_b');
}
// Calculate g_b
var g = context.serverDHInnerData.g;
var b = utility.createNonce(256);
var dhPrime = context.serverDHInnerData.dh_prime;
var gb = utility.modPow(g, b, dhPrime);
if (logger.isDebugEnabled()) {
logger.debug('g_b(%s) = %s', gb.length, gb.toString('hex'));
}
// Create client DH inner data
context.clientDHInnerData = new mtproto.type.Client_DH_inner_data({props: {
nonce: context.resPQ.nonce,
server_nonce: context.resPQ.server_nonce,
retry_id: retryCount,
g_b: gb
}}).serialize();
return context;
}

// Encrypt Client DH inner data
function encryptClientDHInnerDataWithAES(context) {
var hash = utility.createSHA1Hash(context.clientDHInnerData);
var dataWithHash = Buffer.concat([hash, context.clientDHInnerData]);
if (logger.isDebugEnabled()) {
logger.debug('Data to be encrypted contains: hash(%s), clientDHInnerData(%s), total length %s',
hash.length, context.clientDHInnerData.length, dataWithHash.length);
}
context.encryptClientDHInnerData = security.cipher.aesEncrypt(
dataWithHash,
context.aes.key,
context.aes.iv
);
if (logger.isDebugEnabled()) {
logger.debug('encryptClientDHInnerData(%s) = %s',
context.encryptClientDHInnerData.length, context.encryptClientDHInnerData.toString('hex'));
}
return context;
}

// Set client DH parameters
function setClientDHParams(callback, context) {
mtproto.service.set_client_DH_params({
props: {
nonce: context.resPQ.nonce,
server_nonce: context.resPQ.server_nonce,
encrypted_data: context.encryptClientDHInnerData
},
conn: context.connection,
callback: function (ex, setClientDHParamsAnswer, duration) {
if (ex) {
logger.error(ex);
if (callback) {
callback(ex);
}
} else {
if (setClientDHParamsAnswer.typeName === 'mtproto.type.Dh_gen_ok') {
if (logger.isDebugEnabled()) {
logger.debug('\'Dh_gen_ok\' received from Telegram.');
}
context.setClientDHParamsAnswer = setClientDHParamsAnswer;
callback(null, context, duration);
} else if (setClientDHParamsAnswer.typeName === 'mtproto.type.Dh_gen_retry') {
logger.warn('\'Dh_gen_retry\' received from Telegram!');
callback(createError(JSON.stringify(setClientDHParamsAnswer), 'EDHPARAMRETRY'));
} else if (setClientDHParamsAnswer.typeName === 'mtproto.type.Dh_gen_fail') {
logger.warn('\'Dh_gen_fail\' received from Telegram!');
callback(createError(JSON.stringify(setClientDHParamsAnswer), 'EDHPARAMFAIL'));
} else {
var msg = 'Unknown error received from Telegram!';
logger.error(msg);
callback(createError(msg, 'EUNKNOWN'));
}
}
}
});
}

function createError(msg, code) {
var error = new Error(msg);
error.code = code;
return error;
}
Loading

0 comments on commit 1c91cdb

Please sign in to comment.