Commit 5fcf81dd authored by Chris Lu's avatar Chris Lu Committed by Commit Bot

[ios] Split WebFrame JavaScript message payload

Separate function name and parameters from the rest of the Message payload.

This change encrypts the function name and its parameters in a separate payload
from the messageId and replyWithResult values. __gCrWeb.message.routeMessage() is
then passed two dictionaries, each containing an encrypted payload and its
initialization vector.

Both payloads are decrypted and their properties verified in executeMessage_().
This will allow for the function name and parameters to be dropped when
forwarding to a child frame, while still providing enough details for the
child frame to respond to the native side that it received the message.

Bug: 994968
Change-Id: I659d88b8119b2eba2b125d8c30cbea88485c88f1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1759204Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Reviewed-by: default avatarChris Palmer <palmer@chromium.org>
Reviewed-by: default avatarMike Dougherty <michaeldo@chromium.org>
Commit-Queue: Chris Lu <thegreenfrog@chromium.org>
Cr-Commit-Position: refs/heads/master@{#694992}
parent fd46f03c
......@@ -238,11 +238,17 @@ var callGCrWebFunction_ = function(functionPath, parameters) {
}
/**
* Decrypts and executes the function specified in |payload|.
* @param {!string} payload The encrypted message payload.
* @param {!string} iv The initialization vector used to encrypt the |payload|.
* Decrypts and executes the function specified in |functionPayload|.
* @param {Object} encryptedMessageDetails JSON containing encrypted
* information about the message and the initialization vector to decrypt
* the information.
* @param {!Object} encryptedFunctionDetails JSON containing encrypted
* information about the function and its parameters and the initialization
* vector to decrypt the information. If null, won't execute any call, but
* will respond to the native call if specified in |encryptedMessageDetails|.
*/
var executeMessage_ = function(payload, iv) {
var executeMessage_ = function(encryptedMessageDetails,
encryptedFunctionDetails) {
if (!frameSymmetricKey_) {
// Payload cannot be decrypted without a key. This message could be spam or
// sent by the native application by mistake.
......@@ -250,43 +256,84 @@ var executeMessage_ = function(payload, iv) {
}
// Decode the base64 payload.
var encryptedFunctionArray =
new Uint8Array(Array.from(atob(payload)).map(function(a) {
var encryptedMessageArray = new Uint8Array(Array.from(
atob(encryptedMessageDetails['payload'])).map(function(a) {
return a.charCodeAt(0);
}));
// Decode the base64 initialization buffer.
var ivbuf = new Uint8Array(Array.from(atob(iv)).map(function(a) {
var messageIvbuf = new Uint8Array(Array.from(
atob(encryptedMessageDetails['iv'])).map(function(a) {
return a.charCodeAt(0);
}));
var algorithm = {'name': 'AES-GCM', iv: ivbuf};
var messageAlgorithm = {'name': 'AES-GCM', iv: messageIvbuf};
getFrameSymmetricKey_(function(frameKey) {
window.crypto.subtle.decrypt(algorithm, frameKey, encryptedFunctionArray)
.then(function(decrypted) {
var callJSON = new TextDecoder().decode(new Uint8Array(decrypted));
var callDict = JSON.parse(callJSON);
window.crypto.subtle.decrypt(
messageAlgorithm, frameKey, encryptedMessageArray)
.then(function(decryptedMessagePayload) {
var messageJSONString = new TextDecoder().decode(
new Uint8Array(decryptedMessagePayload));
var messageDict = JSON.parse(messageJSONString);
// Verify that message id is valid.
if (!Number.isInteger(callDict['messageId']) ||
callDict['messageId'] <= lastReceivedMessageId_) {
if (!Number.isInteger(messageDict['messageId']) ||
messageDict['messageId'] <= lastReceivedMessageId_) {
return;
}
// Check that a function name and parameters are specified.
if (typeof callDict['functionName'] !== 'string' ||
callDict['functionName'].length < 1 ||
!Array.isArray(callDict['parameters'])) {
lastReceivedMessageId_ = messageDict['messageId'];
// Return early if the function payload was dropped.
if (!encryptedFunctionDetails) {
if (typeof messageDict['replyWithResult'] === 'boolean' &&
messageDict['replyWithResult']) {
replyWithResult_(messageDict['messageId'], null);
}
return;
}
lastReceivedMessageId_ = callDict['messageId'];
var result =
callGCrWebFunction_(callDict['functionName'], callDict['parameters']);
if (typeof callDict['replyWithResult'] === 'boolean' &&
callDict['replyWithResult']) {
replyWithResult_(callDict['messageId'], result);
}
// Decode the base64 payload.
var encryptedFunctionArray = new Uint8Array(Array.from(
atob(encryptedFunctionDetails['payload'])).map(
function(a) {
return a.charCodeAt(0);
}));
// Decode the base64 initialization buffer.
var functionIvbuf = new Uint8Array(Array.from(
atob(encryptedFunctionDetails['iv'])).map(
function(a) {
return a.charCodeAt(0);
}));
var additionalData = new Uint8Array(Array.from(
messageDict['messageId'].toString()).map(
function(a) {
return a.charCodeAt(0);
}));
var functionAlgorithm = {'name': 'AES-GCM',
iv: functionIvbuf,
additionalData: additionalData};
window.crypto.subtle.decrypt(
functionAlgorithm, frameKey, encryptedFunctionArray)
.then(function(decryptedFunctionPayload) {
var functionJSONPayload = new TextDecoder().decode(
new Uint8Array(decryptedFunctionPayload));
var functionDict = JSON.parse(functionJSONPayload);
let functionName = functionDict['functionName'];
let parameters = functionDict['parameters'];
var result = null;
if (typeof functionName === 'string' && functionName.length >= 1
&& Array.isArray(parameters)) {
result = callGCrWebFunction_(functionName, parameters);
}
if (typeof messageDict['replyWithResult'] === 'boolean'
&& messageDict['replyWithResult']) {
replyWithResult_(messageDict['messageId'], result);
}
});
});
});
};
......@@ -316,19 +363,24 @@ __gCrWeb.message['getFrameId'] = function() {
* Routes an encrypted message to the targeted frame. Once the target frame is
* found, the |payload| will be decrypted and executed. This function is called
* by the native code.
* @param {!string} payload The encrypted message payload.
* @param {!string} iv The initialization vector used to encrypt the |payload|.
* @param {!Object} encryptedMessageDetails JSON representing a dictionary
* containing encrypted information about the message and the initialization
* vector to decrypt the information.
* @param {!Object} encryptedFunctionDetails JSON representing a dictionary
* containing encrypted information about the function to call and the
* parameters to pass and the initialization vector to decrypt the information.
* @param {!string} target_frame_id The |frameId_| of the frame which should
* process the |payload|.
*/
__gCrWeb.message['routeMessage'] = function(payload, iv, target_frame_id) {
__gCrWeb.message['routeMessage'] = function(encryptedMessageDetails,
encryptedFunctionDetails, target_frame_id) {
if (!isFrameMessagingSupported_()) {
// API is unsupported.
return;
}
if (target_frame_id === __gCrWeb.message['getFrameId']()) {
executeMessage_(payload, iv);
executeMessage_(encryptedMessageDetails, encryptedFunctionDetails);
return;
}
......@@ -337,8 +389,8 @@ __gCrWeb.message['routeMessage'] = function(payload, iv, target_frame_id) {
window.frames[i].postMessage(
{
type: 'org.chromium.encryptedMessage',
payload: payload,
iv: iv,
message_payload: encryptedMessageDetails,
function_payload: encryptedFunctionDetails,
target_frame_id: target_frame_id
},
'*'
......
......@@ -37,12 +37,12 @@ window.addEventListener('message', function(message) {
__gCrWeb.message['getExistingFrames']();
} else if (payload.hasOwnProperty('type') &&
payload.type == 'org.chromium.encryptedMessage') {
if (payload.hasOwnProperty('payload') &&
payload.hasOwnProperty('iv') &&
if (payload.hasOwnProperty('message_payload') &&
payload.hasOwnProperty('function_payload') &&
payload.hasOwnProperty('target_frame_id')) {
__gCrWeb.message['routeMessage'](
payload['payload'],
payload['iv'],
payload['message_payload'],
payload['function_payload'],
payload['target_frame_id']
);
}
......
......@@ -70,6 +70,11 @@ class WebFrameImpl : public WebFrame, public web::WebStateObserver {
void DetachFromWebState();
// Returns the script command name to use for this WebFrame.
const std::string GetScriptCommandPrefix();
// Encrypts |payload| and returns a JSON string of a dictionary containing
// the encrypted metadata and its initialization vector. If encryption fails,
// an empty string will be returned.
const std::string EncryptPayload(base::DictionaryValue payload,
const std::string& additiona_data);
// A structure to store the callbacks associated with the
// |CallJavaScriptFunction| requests.
......
......@@ -9,6 +9,7 @@
#include "base/base64.h"
#include "base/bind.h"
#include "base/json/json_writer.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
......@@ -85,6 +86,36 @@ bool WebFrameImpl::CanCallJavaScriptFunction() const {
return is_main_frame_ || frame_key_;
}
const std::string WebFrameImpl::EncryptPayload(
base::DictionaryValue payload,
const std::string& additiona_data) {
crypto::Aead aead(crypto::Aead::AES_256_GCM);
aead.Init(&frame_key_->key());
std::string payload_json;
base::JSONWriter::Write(payload, &payload_json);
std::string payload_iv;
crypto::RandBytes(base::WriteInto(&payload_iv, aead.NonceLength() + 1),
aead.NonceLength());
std::string payload_ciphertext;
if (!aead.Seal(payload_json, payload_iv, additiona_data,
&payload_ciphertext)) {
LOG(ERROR) << "Error sealing message payload for WebFrame.";
return std::string();
}
std::string encoded_payload_iv;
base::Base64Encode(payload_iv, &encoded_payload_iv);
std::string encoded_payload;
base::Base64Encode(payload_ciphertext, &encoded_payload);
std::string payload_string;
base::DictionaryValue payload_dict;
payload_dict.SetKey("payload", base::Value(encoded_payload));
payload_dict.SetKey("iv", base::Value(encoded_payload_iv));
base::JSONWriter::Write(payload_dict, &payload_string);
return payload_string;
}
bool WebFrameImpl::CallJavaScriptFunction(
const std::string& name,
const std::vector<base::Value>& parameters,
......@@ -101,36 +132,28 @@ bool WebFrameImpl::CallJavaScriptFunction(
reply_with_result);
}
base::DictionaryValue message;
message.SetKey("messageId", base::Value(message_id));
message.SetKey("replyWithResult", base::Value(reply_with_result));
message.SetKey("functionName", base::Value(name));
base::ListValue parameters_value(parameters);
message.SetKey("parameters", std::move(parameters_value));
std::string json;
base::JSONWriter::Write(message, &json);
base::DictionaryValue message_payload;
message_payload.SetKey("messageId", base::Value(message_id));
message_payload.SetKey("replyWithResult", base::Value(reply_with_result));
const std::string& encrypted_message_json =
EncryptPayload(std::move(message_payload), std::string());
crypto::Aead aead(crypto::Aead::AES_256_GCM);
aead.Init(&frame_key_->key());
std::string iv;
crypto::RandBytes(base::WriteInto(&iv, aead.NonceLength() + 1),
aead.NonceLength());
base::DictionaryValue function_payload;
function_payload.SetKey("functionName", base::Value(name));
base::ListValue parameters_value(parameters);
function_payload.SetKey("parameters", std::move(parameters_value));
const std::string& encrypted_function_json = EncryptPayload(
std::move(function_payload), base::NumberToString(message_id));
std::string ciphertext;
if (!aead.Seal(json, iv, /*additional_data=*/nullptr, &ciphertext)) {
LOG(ERROR) << "Error sealing message for WebFrame.";
if (encrypted_message_json.empty() || encrypted_function_json.empty()) {
// Sealing the payload failed.
return false;
}
std::string encoded_iv;
base::Base64Encode(iv, &encoded_iv);
std::string encoded_message;
base::Base64Encode(ciphertext, &encoded_message);
std::string script = base::StringPrintf(
"__gCrWeb.message.routeMessage('%s', '%s', '%s')",
encoded_message.c_str(), encoded_iv.c_str(), frame_id_.c_str());
std::string script =
base::StringPrintf("__gCrWeb.message.routeMessage(%s, %s, '%s')",
encrypted_message_json.c_str(),
encrypted_function_json.c_str(), frame_id_.c_str());
GetWebState()->ExecuteJavaScript(base::UTF8ToUTF16(script));
return true;
......
......@@ -9,6 +9,7 @@
#include "base/json/json_reader.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#include "base/test/ios/wait_util.h"
#include "base/values.h"
......@@ -39,8 +40,10 @@ std::unique_ptr<SymmetricKey> CreateKey() {
}
struct RouteMessageParameters {
NSString* encoded_function_json = nil;
NSString* encoded_iv = nil;
NSString* encoded_message_payload = nil;
NSString* encoded_message_iv = nil;
NSString* encoded_function_payload = nil;
NSString* encoded_function_iv = nil;
NSString* frame_id = nil;
};
......@@ -48,25 +51,36 @@ RouteMessageParameters ParametersFromFunctionCallString(
NSString* function_call) {
NSRange parameters_start = [function_call rangeOfString:@"("];
NSRange parameters_end = [function_call rangeOfString:@")"];
NSString* parameter_string = [function_call
NSMutableString* parameter_string = [[function_call
substringWithRange:NSMakeRange(parameters_start.location + 1,
parameters_end.location -
parameters_start.location - 1)];
NSArray* parameters = [parameter_string componentsSeparatedByString:@","];
parameters_start.location - 1)]
mutableCopy];
// Create array string and replace single quotes with double quotes in
// preparation for JSON serialization.
[parameter_string insertString:@"[" atIndex:0];
[parameter_string appendString:@"]"];
NSString* final_string =
[parameter_string stringByReplacingOccurrencesOfString:@"'"
withString:@"\""];
NSData* data = [final_string dataUsingEncoding:NSUTF8StringEncoding];
NSError* error = nil;
NSArray* jsonArray =
[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers |
NSJSONReadingMutableLeaves
error:&error];
RouteMessageParameters parsed_params;
if (parameters.count == 3) {
NSMutableCharacterSet* trim_characters_set =
[NSMutableCharacterSet whitespaceCharacterSet];
[trim_characters_set addCharactersInString:@"'"];
parsed_params.encoded_function_json =
[parameters[0] stringByTrimmingCharactersInSet:trim_characters_set];
parsed_params.encoded_iv =
[parameters[1] stringByTrimmingCharactersInSet:trim_characters_set];
parsed_params.frame_id =
[parameters[2] stringByTrimmingCharactersInSet:trim_characters_set];
if (jsonArray.count == 3 && !error) {
parsed_params.encoded_message_iv = jsonArray[0][@"iv"];
parsed_params.encoded_message_payload = jsonArray[0][@"payload"];
parsed_params.encoded_function_iv = jsonArray[1][@"iv"];
parsed_params.encoded_function_payload = jsonArray[1][@"payload"];
parsed_params.frame_id = jsonArray[2];
}
return parsed_params;
}
......@@ -160,21 +174,33 @@ TEST_F(WebFrameImplTest, CallJavaScriptFunction) {
RouteMessageParameters params = ParametersFromFunctionCallString(last_script);
// Verify that the message is a properly base64 encoded string.
std::string decoded_message;
// Verify that the message and function payload are properly base64 encoded
// strings.
std::string decoded_function_payload;
EXPECT_TRUE(base::Base64Decode(
base::SysNSStringToUTF8(params.encoded_function_json), &decoded_message));
// Verify the message does not contain the plaintext function name or
base::SysNSStringToUTF8(params.encoded_function_payload),
&decoded_function_payload));
std::string decoded_message_payload;
EXPECT_TRUE(base::Base64Decode(
base::SysNSStringToUTF8(params.encoded_message_payload),
&decoded_message_payload));
// Verify the function does not contain the plaintext function name or
// parameters.
EXPECT_FALSE([base::SysUTF8ToNSString(decoded_message)
EXPECT_FALSE([base::SysUTF8ToNSString(decoded_function_payload)
containsString:@"functionName"]);
EXPECT_FALSE([base::SysUTF8ToNSString(decoded_message)
EXPECT_FALSE([base::SysUTF8ToNSString(decoded_function_payload)
containsString:@"plaintextParam"]);
std::string iv_string = base::SysNSStringToUTF8(params.encoded_iv);
std::string decoded_iv;
// Verify that the initialization vector is a properly base64 encoded string.
EXPECT_TRUE(base::Base64Decode(iv_string, &decoded_iv));
// Verify that the initialization vector is a properly base64 encoded string
// for both payloads.
std::string function_iv_string =
base::SysNSStringToUTF8(params.encoded_function_iv);
std::string decoded_function_iv;
EXPECT_TRUE(base::Base64Decode(function_iv_string, &decoded_function_iv));
std::string message_iv_string =
base::SysNSStringToUTF8(params.encoded_message_iv);
std::string decoded_message_iv;
EXPECT_TRUE(base::Base64Decode(message_iv_string, &decoded_message_iv));
// Ensure the frame ID matches.
EXPECT_NSEQ(base::SysUTF8ToNSString(kFrameId), params.frame_id);
......@@ -208,8 +234,9 @@ TEST_F(WebFrameImplTest, CallJavaScriptFunctionUniqueInitializationVector) {
RouteMessageParameters params2 =
ParametersFromFunctionCallString(last_script2);
EXPECT_NSNE(params1.encoded_function_json, params2.encoded_function_json);
EXPECT_NSNE(params1.encoded_iv, params2.encoded_iv);
EXPECT_NSNE(params1.encoded_function_payload,
params2.encoded_function_payload);
EXPECT_NSNE(params1.encoded_function_iv, params2.encoded_function_iv);
}
// Tests that the WebFrame properly encodes and encrypts all parameters for
......@@ -238,47 +265,67 @@ TEST_F(WebFrameImplTest, CallJavaScriptFunctionMessageProperlyEncoded) {
base::SysUTF16ToNSString(test_web_state.GetLastExecutedJavascript());
RouteMessageParameters params = ParametersFromFunctionCallString(last_script);
std::string decoded_ciphertext;
std::string decoded_function_ciphertext;
EXPECT_TRUE(base::Base64Decode(
base::SysNSStringToUTF8(params.encoded_function_payload),
&decoded_function_ciphertext));
std::string decoded_function_iv;
EXPECT_TRUE(
base::Base64Decode(base::SysNSStringToUTF8(params.encoded_function_json),
&decoded_ciphertext));
base::Base64Decode(base::SysNSStringToUTF8(params.encoded_function_iv),
&decoded_function_iv));
std::string decoded_message_ciphertext;
EXPECT_TRUE(base::Base64Decode(
base::SysNSStringToUTF8(params.encoded_message_payload),
&decoded_message_ciphertext));
std::string decoded_iv;
EXPECT_TRUE(base::Base64Decode(base::SysNSStringToUTF8(params.encoded_iv),
&decoded_iv));
std::string decoded_message_iv;
EXPECT_TRUE(base::Base64Decode(
base::SysNSStringToUTF8(params.encoded_message_iv), &decoded_message_iv));
// Decrypt message
crypto::Aead aead(crypto::Aead::AES_256_GCM);
aead.Init(&key_string);
std::string plaintext;
EXPECT_TRUE(aead.Open(decoded_ciphertext, decoded_iv,
/*additional_data=*/nullptr, &plaintext));
std::string function_plaintext;
EXPECT_TRUE(aead.Open(decoded_function_ciphertext, decoded_function_iv,
base::NumberToString(initial_message_id),
&function_plaintext));
std::string message_plaintext;
EXPECT_TRUE(aead.Open(decoded_message_ciphertext, decoded_message_iv,
/*additional_data=*/nullptr, &message_plaintext));
base::Optional<base::Value> parsed_function_result =
base::JSONReader::Read(function_plaintext, false);
EXPECT_TRUE(parsed_function_result.has_value());
ASSERT_TRUE(parsed_function_result.value().is_dict());
base::Optional<base::Value> parsed_result =
base::JSONReader::Read(plaintext, false);
EXPECT_TRUE(parsed_result.has_value());
ASSERT_TRUE(parsed_result.value().is_dict());
const std::string* decrypted_function_name =
parsed_function_result.value().FindStringKey("functionName");
ASSERT_TRUE(decrypted_function_name);
EXPECT_EQ("functionName", *decrypted_function_name);
base::Value* decrypted_parameters =
parsed_function_result.value().FindKeyOfType("parameters",
base::Value::Type::LIST);
ASSERT_TRUE(decrypted_parameters);
ASSERT_EQ(function_params.size(), decrypted_parameters->GetList().size());
EXPECT_EQ(plaintext_param, decrypted_parameters->GetList()[0].GetString());
base::Optional<base::Value> parsed_message_result =
base::JSONReader::Read(message_plaintext, false);
EXPECT_TRUE(parsed_message_result.has_value());
ASSERT_TRUE(parsed_message_result.value().is_dict());
base::Optional<int> decrypted_message_id =
parsed_result.value().FindIntKey("messageId");
parsed_message_result.value().FindIntKey("messageId");
ASSERT_TRUE(decrypted_message_id.has_value());
EXPECT_EQ(decrypted_message_id.value(), initial_message_id);
base::Optional<bool> decrypted_respond_with_result =
parsed_result.value().FindBoolKey("replyWithResult");
parsed_message_result.value().FindBoolKey("replyWithResult");
ASSERT_TRUE(decrypted_respond_with_result.has_value());
EXPECT_FALSE(decrypted_respond_with_result.value());
const std::string* decrypted_function_name =
parsed_result.value().FindStringKey("functionName");
ASSERT_TRUE(decrypted_function_name);
EXPECT_EQ("functionName", *decrypted_function_name);
base::Value* decrypted_parameters = parsed_result.value().FindKeyOfType(
"parameters", base::Value::Type::LIST);
ASSERT_TRUE(decrypted_parameters);
ASSERT_EQ(function_params.size(), decrypted_parameters->GetList().size());
EXPECT_EQ(plaintext_param, decrypted_parameters->GetList()[0].GetString());
}
// Tests that the WebFrame properly encodes and encrypts the respondWithResult
......@@ -310,24 +357,24 @@ TEST_F(WebFrameImplTest, CallJavaScriptFunctionRespondWithResult) {
base::SysUTF16ToNSString(test_web_state.GetLastExecutedJavascript());
RouteMessageParameters params = ParametersFromFunctionCallString(last_script);
std::string decoded_ciphertext;
EXPECT_TRUE(
base::Base64Decode(base::SysNSStringToUTF8(params.encoded_function_json),
&decoded_ciphertext));
std::string decoded_message_ciphertext;
EXPECT_TRUE(base::Base64Decode(
base::SysNSStringToUTF8(params.encoded_message_payload),
&decoded_message_ciphertext));
std::string decoded_iv;
EXPECT_TRUE(base::Base64Decode(base::SysNSStringToUTF8(params.encoded_iv),
&decoded_iv));
std::string decoded_message_iv;
EXPECT_TRUE(base::Base64Decode(
base::SysNSStringToUTF8(params.encoded_message_iv), &decoded_message_iv));
// Decrypt message
crypto::Aead aead(crypto::Aead::AES_256_GCM);
aead.Init(&key_string);
std::string plaintext;
EXPECT_TRUE(aead.Open(decoded_ciphertext, decoded_iv,
/*additional_data=*/nullptr, &plaintext));
std::string message_plaintext;
EXPECT_TRUE(aead.Open(decoded_message_ciphertext, decoded_message_iv,
/*additional_data=*/nullptr, &message_plaintext));
base::Optional<base::Value> parsed_result =
base::JSONReader::Read(plaintext, false);
base::JSONReader::Read(message_plaintext, false);
EXPECT_TRUE(parsed_result.has_value());
ASSERT_TRUE(parsed_result.value().is_dict());
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment