blob: 6782d8a314dc5560b26027fd95c355896dc289c4 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ios/web/js_messaging/web_frame_impl.h"
#import "base/base64.h"
#include "base/bind.h"
#include "base/json/json_reader.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#import "base/strings/sys_string_conversions.h"
#include "base/test/ios/wait_util.h"
#include "base/values.h"
#include "crypto/aead.h"
#import "ios/web/public/test/fakes/test_web_state.h"
#include "ios/web/public/test/web_test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using crypto::SymmetricKey;
namespace {
const char kFrameId[] = "1effd8f52a067c8d3a01762d3c41dfd8";
// A base64 encoded sample key.
const char kFrameKey[] = "R7lsXtR74c6R9A9k691gUQ8JAd0be+w//Lntgcbjwrc=";
// Returns a key which can be used to create a WebFrame.
std::unique_ptr<SymmetricKey> CreateKey() {
std::string decoded_frame_key_string;
base::Base64Decode(kFrameKey, &decoded_frame_key_string);
return crypto::SymmetricKey::Import(crypto::SymmetricKey::Algorithm::AES,
decoded_frame_key_string);
}
struct RouteMessageParameters {
NSString* encoded_function_json = nil;
NSString* encoded_iv = nil;
NSString* frame_id = nil;
};
RouteMessageParameters ParametersFromFunctionCallString(
NSString* function_call) {
NSRange parameters_start = [function_call rangeOfString:@"("];
NSRange parameters_end = [function_call rangeOfString:@")"];
NSString* parameter_string = [function_call
substringWithRange:NSMakeRange(parameters_start.location + 1,
parameters_end.location -
parameters_start.location - 1)];
NSArray* parameters = [parameter_string componentsSeparatedByString:@","];
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];
}
return parsed_params;
}
} // namespace
namespace web {
typedef web::WebTest WebFrameImplTest;
// Tests creation of a WebFrame for the main frame without an encryption key.
TEST_F(WebFrameImplTest, CreateWebFrameForMainFrame) {
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/true, security_origin,
&test_web_state);
EXPECT_EQ(&test_web_state, web_frame.GetWebState());
EXPECT_TRUE(web_frame.IsMainFrame());
EXPECT_TRUE(web_frame.CanCallJavaScriptFunction());
EXPECT_EQ(security_origin, web_frame.GetSecurityOrigin());
EXPECT_EQ(kFrameId, web_frame.GetFrameId());
}
// Tests creation of a WebFrame for the main frame with an encryption key.
TEST_F(WebFrameImplTest, CreateWebFrameForMainFrameWithKey) {
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/true, security_origin,
&test_web_state);
web_frame.SetEncryptionKey(CreateKey());
EXPECT_EQ(&test_web_state, web_frame.GetWebState());
EXPECT_TRUE(web_frame.IsMainFrame());
EXPECT_TRUE(web_frame.CanCallJavaScriptFunction());
EXPECT_EQ(security_origin, web_frame.GetSecurityOrigin());
EXPECT_EQ(kFrameId, web_frame.GetFrameId());
}
// Tests creation of a WebFrame for a frame which is not the main frame without
// an encryption key.
TEST_F(WebFrameImplTest, CreateWebFrameForIFrame) {
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/false, security_origin,
&test_web_state);
EXPECT_EQ(&test_web_state, web_frame.GetWebState());
EXPECT_FALSE(web_frame.IsMainFrame());
EXPECT_FALSE(web_frame.CanCallJavaScriptFunction());
EXPECT_EQ(security_origin, web_frame.GetSecurityOrigin());
EXPECT_EQ(kFrameId, web_frame.GetFrameId());
}
// Tests creation of a WebFrame for a frame which is not the main frame with an
// encryption key.
TEST_F(WebFrameImplTest, CreateWebFrameForIFrameWithKey) {
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/false, security_origin,
&test_web_state);
web_frame.SetEncryptionKey(CreateKey());
EXPECT_EQ(&test_web_state, web_frame.GetWebState());
EXPECT_FALSE(web_frame.IsMainFrame());
EXPECT_TRUE(web_frame.CanCallJavaScriptFunction());
EXPECT_EQ(security_origin, web_frame.GetSecurityOrigin());
EXPECT_EQ(kFrameId, web_frame.GetFrameId());
}
// Tests that |CallJavaScriptFunction| encrypts the message and passes it to
// __gCrWeb.message.routeMessage in the main frame.
TEST_F(WebFrameImplTest, CallJavaScriptFunction) {
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/false, security_origin,
&test_web_state);
web_frame.SetEncryptionKey(CreateKey());
std::vector<base::Value> function_params;
function_params.push_back(base::Value("plaintextParam"));
EXPECT_TRUE(
web_frame.CallJavaScriptFunction("functionName", function_params));
NSString* last_script =
base::SysUTF16ToNSString(test_web_state.GetLastExecutedJavascript());
EXPECT_TRUE([last_script hasPrefix:@"__gCrWeb.message.routeMessage"]);
// Verify the message does not contain the plaintext function name or
// parameters.
EXPECT_FALSE([last_script containsString:@"functionName"]);
EXPECT_FALSE([last_script containsString:@"plaintextParam"]);
RouteMessageParameters params = ParametersFromFunctionCallString(last_script);
// Verify that the message is a properly base64 encoded string.
std::string decoded_message;
EXPECT_TRUE(base::Base64Decode(
base::SysNSStringToUTF8(params.encoded_function_json), &decoded_message));
// Verify the message does not contain the plaintext function name or
// parameters.
EXPECT_FALSE([base::SysUTF8ToNSString(decoded_message)
containsString:@"functionName"]);
EXPECT_FALSE([base::SysUTF8ToNSString(decoded_message)
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));
// Ensure the frame ID matches.
EXPECT_NSEQ(base::SysUTF8ToNSString(kFrameId), params.frame_id);
}
// Tests that the WebFrame uses different initialization vectors for two
// sequential calls to |CallJavaScriptFunction|.
TEST_F(WebFrameImplTest, CallJavaScriptFunctionUniqueInitializationVector) {
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/false, security_origin,
&test_web_state);
web_frame.SetEncryptionKey(CreateKey());
std::vector<base::Value> function_params;
function_params.push_back(base::Value("plaintextParam"));
EXPECT_TRUE(
web_frame.CallJavaScriptFunction("functionName", function_params));
NSString* last_script1 =
base::SysUTF16ToNSString(test_web_state.GetLastExecutedJavascript());
RouteMessageParameters params1 =
ParametersFromFunctionCallString(last_script1);
// Call JavaScript Function again to verify that the same initialization
// vector is not reused and that the ciphertext is different.
EXPECT_TRUE(
web_frame.CallJavaScriptFunction("functionName", function_params));
NSString* last_script2 =
base::SysUTF16ToNSString(test_web_state.GetLastExecutedJavascript());
RouteMessageParameters params2 =
ParametersFromFunctionCallString(last_script2);
EXPECT_NSNE(params1.encoded_function_json, params2.encoded_function_json);
EXPECT_NSNE(params1.encoded_iv, params2.encoded_iv);
}
// Tests that the WebFrame properly encodes and encrypts all parameters for
// |CallJavaScriptFunction|.
TEST_F(WebFrameImplTest, CallJavaScriptFunctionMessageProperlyEncoded) {
std::unique_ptr<SymmetricKey> key = CreateKey();
const std::string key_string = key->key();
// Use an arbitrary nonzero message id to ensure it isn't matching a zero
// value by chance.
const int initial_message_id = 11;
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/false, security_origin,
&test_web_state);
web_frame.SetEncryptionKey(std::move(key));
web_frame.SetNextMessageId(initial_message_id);
std::vector<base::Value> function_params;
std::string plaintext_param("plaintextParam");
function_params.push_back(base::Value(plaintext_param));
EXPECT_TRUE(
web_frame.CallJavaScriptFunction("functionName", function_params));
NSString* last_script =
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_iv;
EXPECT_TRUE(base::Base64Decode(base::SysNSStringToUTF8(params.encoded_iv),
&decoded_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));
base::Optional<base::Value> parsed_result =
base::JSONReader::Read(plaintext, false);
EXPECT_TRUE(parsed_result.has_value());
ASSERT_TRUE(parsed_result.value().is_dict());
base::Optional<int> decrypted_message_id =
parsed_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");
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
// value when |CallJavaScriptFunction| is called with a callback.
TEST_F(WebFrameImplTest, CallJavaScriptFunctionRespondWithResult) {
std::unique_ptr<SymmetricKey> key = CreateKey();
const std::string key_string = key->key();
// Use an arbitrary nonzero message id to ensure it isn't matching a zero
// value by chance.
const int initial_message_id = 11;
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/false, security_origin,
&test_web_state);
web_frame.SetEncryptionKey(std::move(key));
web_frame.SetNextMessageId(initial_message_id);
std::vector<base::Value> function_params;
std::string plaintext_param("plaintextParam");
function_params.push_back(base::Value(plaintext_param));
EXPECT_TRUE(web_frame.CallJavaScriptFunction(
"functionName", function_params,
base::BindOnce(^(const base::Value* value){
}),
base::TimeDelta::FromSeconds(5)));
NSString* last_script =
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_iv;
EXPECT_TRUE(base::Base64Decode(base::SysNSStringToUTF8(params.encoded_iv),
&decoded_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));
base::Optional<base::Value> parsed_result =
base::JSONReader::Read(plaintext, false);
EXPECT_TRUE(parsed_result.has_value());
ASSERT_TRUE(parsed_result.value().is_dict());
base::Optional<bool> decrypted_respond_with_result =
parsed_result.value().FindBoolKey("replyWithResult");
ASSERT_TRUE(decrypted_respond_with_result.has_value());
EXPECT_TRUE(decrypted_respond_with_result.value());
}
// Tests that the WebFrame properly creates JavaScript for the main frame when
// there is no encryption key.
TEST_F(WebFrameImplTest, CallJavaScriptFunctionMainFrameWithoutKey) {
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/true, security_origin,
&test_web_state);
std::vector<base::Value> function_params;
EXPECT_TRUE(
web_frame.CallJavaScriptFunction("functionName", function_params));
NSString* last_script =
base::SysUTF16ToNSString(test_web_state.GetLastExecutedJavascript());
EXPECT_NSEQ(@"__gCrWeb.functionName()", last_script);
function_params.push_back(base::Value("param1"));
EXPECT_TRUE(
web_frame.CallJavaScriptFunction("functionName", function_params));
last_script =
base::SysUTF16ToNSString(test_web_state.GetLastExecutedJavascript());
EXPECT_NSEQ(@"__gCrWeb.functionName(\"param1\")", last_script);
function_params.push_back(base::Value(true));
function_params.push_back(base::Value(27));
function_params.push_back(base::Value(3.14));
EXPECT_TRUE(
web_frame.CallJavaScriptFunction("functionName", function_params));
last_script =
base::SysUTF16ToNSString(test_web_state.GetLastExecutedJavascript());
EXPECT_NSEQ(@"__gCrWeb.functionName(\"param1\",true,27,3.14)", last_script);
}
// Tests that the WebFrame does not create JavaScript for an iframe when there
// is no encryption key.
TEST_F(WebFrameImplTest, CallJavaScriptFunctionIFrameFrameWithoutKey) {
TestWebState test_web_state;
GURL security_origin;
WebFrameImpl web_frame(kFrameId, /*is_main_frame=*/false, security_origin,
&test_web_state);
std::vector<base::Value> function_params;
function_params.push_back(base::Value("plaintextParam"));
EXPECT_FALSE(
web_frame.CallJavaScriptFunction("functionName", function_params));
NSString* last_script =
base::SysUTF16ToNSString(test_web_state.GetLastExecutedJavascript());
EXPECT_EQ(last_script.length, 0ul);
}
} // namespace web