ssh-agent.js at [d756b0ea34]

File build/chrome/ssh-agent.js artifact 2a1297f569 part of check-in d756b0ea34


/*
 * CACKey SSH Agent for ChromeOS
 */

cackeySSHAgentApprovedApps = [
	"pnhechapfaindjhompbnflcldabbghjo",
	"okddffdblfhhnmhodogpojmfkjmhinfp"
];

/*
 * SSH Element Encoding/Decoding
 */
function cackeySSHAgentEncodeInt(uint32) {
	var result;

	result = [
		(uint32 >> 24) & 0xff,
		(uint32 >> 16) & 0xff,
		(uint32 >>  8) & 0xff,
		 uint32        & 0xff
	];

	return(result);
}

function cackeySSHAgentDecodeInt(input) {
	var result;

	result = 0;
	result |= (input[0] << 24);
	result |= (input[1] << 16);
	result |= (input[2] << 8);
	result |=  input[3];

	return({
		value: result,
		output: input.slice(4)
	});
}

function cackeySSHAgentEncodeBigInt(bigInt) {
	var result = [];

	switch (typeof(bigInt)) {
		case "number":
			while (bigInt) {
				result.push(bigInt & 0xff);
				bigInt = bigInt >> 8;
			}
			result.reverse();
			break;
		case "object":
			result = [];
			new Uint8Array(bigInt.toByteArray()).forEach(function(e) {
				result.push(e);
			});
			break;
	}

	result = cackeySSHAgentEncodeLV(result);

	return(result);
}

function cackeySSHAgentEncodeLV(input) {
	var result;

	result = cackeySSHAgentEncodeInt(input.length);
	result = result.concat(input);

	return(result);
}

function cackeySSHAgentDecodeLV(input) {
	var result, info;

	info = cackeySSHAgentDecodeInt(input);
	if (info.value >= input.length) {
		throw(new Error("Invalid data"));
	}

	input = info.output;

	result = input.slice(0, info.value);

	return({
		value: result,
		output: input.slice(info.value)
	});
}

function cackeySSHAgentEncodeToUTF8Array(str) {
	var utf8 = [];

	if (typeof(str) === "string") {
		str = str.split("").map(function(c) {
			return(c.charCodeAt(0));
		});
	}

	for (var i = 0; i < str.length; i++) {
		var charcode = str[i];

		if (charcode < 0x80) {
			utf8.push(charcode);
		} else if (charcode < 0x800) {
			utf8.push(0xc0 | (charcode >> 6), 
			          0x80 | (charcode & 0x3f));
		} else if (charcode < 0xd800 || charcode >= 0xe000) {
			utf8.push(0xe0 | (charcode >> 12), 
			          0x80 | ((charcode >> 6) & 0x3f), 
			          0x80 | (charcode & 0x3f));
		} else {
			// surrogate pair
			i++;
			// UTF-16 encodes 0x10000-0x10FFFF by
			// subtracting 0x10000 and splitting the
			// 20 bits of 0x0-0xFFFFF into two halves
			charcode = 0x10000 + (((charcode & 0x3ff) << 10)
			           | (str[i] & 0x3ff));

			utf8.push(0xf0 | (charcode >>18), 
			          0x80 | ((charcode >> 12) & 0x3f), 
			          0x80 | ((charcode >> 6) & 0x3f), 
			          0x80 | (charcode & 0x3f));
		}
	}

	return utf8;
}

function cackeySSHAgentEncodeString(string) {
	var result;

	result = cackeySSHAgentEncodeLV(cackeySSHAgentEncodeToUTF8Array(string));

	return(result);
}

function cackeySSHAgentEncodeBinaryToHex(binaryString) {
	var buffer;

	switch (typeof(binaryString)) {
		case "string":
			buffer = binaryString.split("").map(function(c) {
				return(c.charCodeAt(0).toString(16).padStart(2, '0'));
			}).join("");
			break;
		default:
			buffer = [];
			new Uint8Array(binaryString).map(function(c) {
				buffer.push(c.toString(16).padStart(2, '0'));
			});
			buffer = buffer.join("");
			break;
	}

	return(buffer);
}

function cackeySSHAgentEncodeCertToKeyAndID(cert) {
	var result = null, resultKey = null;
	var certObj;
	var publicKey;

	certObj = new X509;
	if (!certObj) {
		return(result);
	}

	certObj.readCertHex(cackeySSHAgentEncodeBinaryToHex(cert));

	publicKey = certObj.getPublicKey();

	switch (publicKey.type) {
		case "RSA":
			resultKey = cackeySSHAgentEncodeString("ssh-rsa");
			resultKey = resultKey.concat(cackeySSHAgentEncodeBigInt(publicKey.e));
			resultKey = resultKey.concat(cackeySSHAgentEncodeBigInt(publicKey.n));
			break;
		default:
			console.log("[cackeySSH] Unsupported public key type:", publicKey.type, "-- ignoring.");
	}

	if (resultKey) {
		result = {
			id: certObj.getSubjectString(),
			type: publicKey.type,
			key: resultKey
		};
	}

	return(result);
}

/*
 * Command Handlers
 */
async function cackeySSHAgentCommandRequestIdentity(request) {
	var response;
	var certs = [];
	var keys = [];

	/*
	 * Get a list of certificates
	 */
	certs = await cackeyListCertificates();

	/*
	 * Convert each certificate to an SSH key blob
	 */
	certs.forEach(function(cert) {
		var key;

		key = cackeySSHAgentEncodeCertToKeyAndID(cert.certificate);

		if (key) {
			keys.push(key);
		}
	});

	/*
	 * Encode response
	 */
	response = [];

	response.push(cackeySSHAgentMessage.SSH_AGENT_IDENTITIES_ANSWER);
	response = response.concat(cackeySSHAgentEncodeInt(keys.length));
	keys.forEach(function(key) {
		response = response.concat(cackeySSHAgentEncodeLV(key.key));
		response = response.concat(cackeySSHAgentEncodeString("CACKey: " + key.id));
	});

	return(response);
}

async function cackeySSHAgentCommandSignRequest(request) {
	var keyInfo, data, flags;
	var certs, certToUse, certToUseType;
	var hashMethod, signedData, signedDataHeader, signRequest;
	var response;
	var flagMeaning = {
		SSH_AGENT_RSA_SHA2_256: 2,
		SSH_AGENT_RSA_SHA2_512: 4
	};

	/*
	 * Strip off the command
	 */
	request = request.slice(1);

	/*
	 * Get certificate to sign using
	 */
	keyInfo = cackeySSHAgentDecodeLV(request);
	request = keyInfo.output;
	keyInfo = keyInfo.value;

	/*
	 * Get the data to sign
	 */
	data = cackeySSHAgentDecodeLV(request);
	request = data.output;
	data = data.value;

	/*
	 * Get the flags
	 */
	flags = cackeySSHAgentDecodeInt(request);
	request = flags.output;
	flags = flags.value;

	/*
	 * Find the certificate that matches the requested key
	 */
	certs = await cackeyListCertificates();
	certToUse = null;
	certs.forEach(function(cert) {
		var key;

		key = cackeySSHAgentEncodeCertToKeyAndID(cert.certificate);

		if (key.key.join() == keyInfo.join()) {
			certToUse = cert;
			certToUseType = key.type;
		}
	});

	/*
	 * If no certificate is found, return an error
	 */
	if (!certToUse) {
		console.info("[cackeySSH] Unable to find a certificate to match the requested key:", keyInfo);

		return(null);
	}

	/*
	 * Perform hashing of the data as specified by the flags
	 */
	switch (certToUseType) {
		case "RSA":
			if ((flags & flagMeaning.SSH_AGENT_RSA_SHA2_512) == flagMeaning.SSH_AGENT_RSA_SHA2_512) {
				hashMethod = "SHA512";
				data = await crypto.subtle.digest("SHA-512", new Uint8Array(data));
			} else if ((flags & flagMeaning.SSH_AGENT_RSA_SHA2_256) == flagMeaning.SSH_AGENT_RSA_SHA2_256) {
				hashMethod = "SHA256";
				data = await crypto.subtle.digest("SHA-256", new Uint8Array(data));
			} else if (flags == 0) {
				hashMethod = "SHA1";
				data = await crypto.subtle.digest("SHA-1", new Uint8Array(data));
			} else {
				console.info("[cackeySSH] Sign request with flags set to", flags, "which is unsupported, failing the request.");

				return(null);
			}

			switch (hashMethod) {
				case "SHA1":
					signedDataHeader = cackeySSHAgentEncodeString("ssh-rsa");
					break;
				case "SHA256":
					signedDataHeader = cackeySSHAgentEncodeString("rsa-sha2-256");
					break;
				case "SHA512":
					signedDataHeader = cackeySSHAgentEncodeString("rsa-sha2-512");
					break;
				default:
					console.info("[cackeySSH] Unsupported hashing method for RSA:", hashMethod, "failing the request.");

					return(null);
					break;
			}
			break;
		default:
			console.info("[cackeySSH] Unsupported public key type:", certToUseType, "failing the request.");

			return(null);
			break;
	}

	/*
	 * Sign the data
	 */
	signRequest = {
		hash: hashMethod,
		digest: new Uint8Array(data)
	};
	signedData = await cackeySignMessage(signRequest);
	signedData = Array.from(new Uint8Array(signedData));

	/*
	 * Encode signature
	 */
	signedData = signedDataHeader.concat(cackeySSHAgentEncodeLV(signedData));

	/*
	 * Encode response
	 */
	response = [];

	response.push(cackeySSHAgentMessage.SSH_AGENT_SIGN_RESPONSE);
	response = response.concat(cackeySSHAgentEncodeLV(signedData));

	return(response);
}

/*
 * Session handling
 */
async function cackeySSHAgentHandleMessage(socket, request) {
	var sshRequestID, sshRequest, response, sshResponse;
	var sshHandlerError;

	if (!request.type || request.type !== "auth-agent@openssh.com") {
		return;
	}

	if (!request.data || request.data.length < 1) {
		return;
	}

	sshRequestID = request.data[0];
	sshRequest = {};
	if (sshRequestID < cackeySSHAgentCommands.length) {
		sshRequest = cackeySSHAgentCommands[sshRequestID];
	}

	response = null;
	if (!sshRequest.name) {
		if (goog.DEBUG) {
			console.log("[cackeySSH] Unsupported request: ", request, "; from: ", socket.sender.id);
		}
	} else {
		if (goog.DEBUG) {
			console.log("[cackeySSH] Request: ", sshRequest.name, "; from: ", socket.sender.id);
		}

		try {
			response = await sshRequest.handler(request.data);
		} catch (sshHandlerError) {
			response = null;

			console.error("[cackeySSH] Request:", sshRequest.name, "(", request, ") ERROR:", sshHandlerError);
		}
	}

	if (!response) {
		response = [cackeySSHAgentMessage.SSH_AGENT_FAILURE];
	}

	sshResponse = {
		type: "auth-agent@openssh.com",
		data: response
	};

	if (goog.DEBUG) {
		console.log("[cackeySSH] Response: ", sshResponse);
	}

	socket.postMessage(sshResponse);

	return;
}

function cackeySSHAgentAcceptConnection(socket) {
	if (!socket) {
		return;
	}

	/*
	 * Only accept connections from approved apps
	 */
	if (!socket.sender || !socket.sender.id || !cackeySSHAgentApprovedApps.includes(socket.sender.id)) {
		console.log("[cackeySSH] Ignoring unapproved app: ", socket.sender);

		return;
	}

	console.log("[cackeySSH] Accepted connection from: ", socket.sender.id);
	socket.onMessage.addListener(function(request) {
		cackeySSHAgentHandleMessage(socket, request);
	});
}

function cackeySSHAgentInit() {
	chrome.runtime.onConnectExternal.addListener(cackeySSHAgentAcceptConnection);
}

function cackeySSHAgentUninit() {
	chrome.runtime.onConnectExternal.removeListener(cackeySSHAgentAcceptConnection);
}

cackeySSHAgentCommands = [
	{ /* 0: Not implemented */ },
	{ /* 1: Not implemented */ },
	{ /* 2: Not implemented */ },
	{ /* 3: Not implemented */ },
	{ /* 4: Not implemented */ },
	{ /* 5: Not implemented */ },
	{ /* 6: Not implemented */ },
	{ /* 7: Not implemented */ },
	{ /* 8: Not implemented */ },
	{ /* 9: Not implemented */ },
	{ /* 10: Not implemented */ },
	{
		name: "requestIdentities",
		handler: cackeySSHAgentCommandRequestIdentity
	},
	{ /* 12: Not implemented */ },
	{
		name: "signRequest",
		handler: cackeySSHAgentCommandSignRequest
	},
	{ /* 14: Not implemented */ },
	{ /* 15: Not implemented */ },
	{ /* 16: Not implemented */ },
	{ /* 17: Not implemented */ },
	{ /* 18: Not implemented */ },
	{ /* 19: Not implemented */ },
	{ /* 20: Not implemented */ },
	{ /* 21: Not implemented */ },
	{ /* 22: Not implemented */ },
	{ /* 23: Not implemented */ },
	{ /* 24: Not implemented */ },
	{ /* 25: Not implemented */ },
	{ /* 26: Not implemented */ },
	{ /* 27: Not implemented */ },
	{ /* 28: Not implemented */ }
];

cackeySSHAgentMessage = {
	SSH_AGENT_FAILURE: 5,
	SSH_AGENT_SUCCESS: 6,
	SSH_AGENT_EXTENSION_FAILURE: 28,
	SSH_AGENT_IDENTITIES_ANSWER: 12,
	SSH_AGENT_SIGN_RESPONSE: 14
};

cackeySSHAgentInit();