Merge "Added HIDL based software implementation of gatekeeper"

This commit is contained in:
Janis Danisevskis 2019-06-28 00:43:10 +00:00 committed by Gerrit Code Review
commit 7350a53137
12 changed files with 666 additions and 0 deletions

View file

@ -0,0 +1,2 @@
jdanis@google.com
swillden@google.com

View file

@ -0,0 +1,28 @@
cc_binary {
name: "android.hardware.gatekeeper@1.0-service.software",
defaults: ["hidl_defaults"],
relative_install_path: "hw",
vendor: true,
init_rc: ["android.hardware.gatekeeper@1.0-service.software.rc"],
srcs: [
"service.cpp",
"SoftGateKeeperDevice.cpp",
],
shared_libs: [
"android.hardware.gatekeeper@1.0",
"libbase",
"libhardware",
"libhidlbase",
"libhidltransport",
"libutils",
"liblog",
"libcrypto",
"libgatekeeper",
],
static_libs: ["libscrypt_static"],
vintf_fragments: ["android.hardware.gatekeeper@1.0-service.software.xml"],
}

View file

@ -0,0 +1,2 @@
jdanis@google.com
swillden@google.com

View file

@ -0,0 +1,171 @@
/*
* Copyright 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#ifndef SOFT_GATEKEEPER_H_
#define SOFT_GATEKEEPER_H_
extern "C" {
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <crypto_scrypt.h>
}
#include <android-base/memory.h>
#include <gatekeeper/gatekeeper.h>
#include <iostream>
#include <memory>
#include <unordered_map>
namespace gatekeeper {
struct fast_hash_t {
uint64_t salt;
uint8_t digest[SHA256_DIGEST_LENGTH];
};
class SoftGateKeeper : public GateKeeper {
public:
static const uint32_t SIGNATURE_LENGTH_BYTES = 32;
// scrypt params
static const uint64_t N = 16384;
static const uint32_t r = 8;
static const uint32_t p = 1;
static const int MAX_UINT_32_CHARS = 11;
SoftGateKeeper() {
key_.reset(new uint8_t[SIGNATURE_LENGTH_BYTES]);
memset(key_.get(), 0, SIGNATURE_LENGTH_BYTES);
}
virtual ~SoftGateKeeper() {}
virtual bool GetAuthTokenKey(const uint8_t** auth_token_key, uint32_t* length) const {
if (auth_token_key == NULL || length == NULL) return false;
*auth_token_key = key_.get();
*length = SIGNATURE_LENGTH_BYTES;
return true;
}
virtual void GetPasswordKey(const uint8_t** password_key, uint32_t* length) {
if (password_key == NULL || length == NULL) return;
*password_key = key_.get();
*length = SIGNATURE_LENGTH_BYTES;
}
virtual void ComputePasswordSignature(uint8_t* signature, uint32_t signature_length,
const uint8_t*, uint32_t, const uint8_t* password,
uint32_t password_length, salt_t salt) const {
if (signature == NULL) return;
crypto_scrypt(password, password_length, reinterpret_cast<uint8_t*>(&salt), sizeof(salt), N,
r, p, signature, signature_length);
}
virtual void GetRandom(void* random, uint32_t requested_length) const {
if (random == NULL) return;
RAND_pseudo_bytes((uint8_t*)random, requested_length);
}
virtual void ComputeSignature(uint8_t* signature, uint32_t signature_length, const uint8_t*,
uint32_t, const uint8_t*, const uint32_t) const {
if (signature == NULL) return;
memset(signature, 0, signature_length);
}
virtual uint64_t GetMillisecondsSinceBoot() const {
struct timespec time;
int res = clock_gettime(CLOCK_BOOTTIME, &time);
if (res < 0) return 0;
return (time.tv_sec * 1000) + (time.tv_nsec / 1000 / 1000);
}
virtual bool IsHardwareBacked() const { return false; }
virtual bool GetFailureRecord(uint32_t uid, secure_id_t user_id, failure_record_t* record,
bool /* secure */) {
failure_record_t* stored = &failure_map_[uid];
if (user_id != stored->secure_user_id) {
stored->secure_user_id = user_id;
stored->last_checked_timestamp = 0;
stored->failure_counter = 0;
}
memcpy(record, stored, sizeof(*record));
return true;
}
virtual bool ClearFailureRecord(uint32_t uid, secure_id_t user_id, bool /* secure */) {
failure_record_t* stored = &failure_map_[uid];
stored->secure_user_id = user_id;
stored->last_checked_timestamp = 0;
stored->failure_counter = 0;
return true;
}
virtual bool WriteFailureRecord(uint32_t uid, failure_record_t* record, bool /* secure */) {
failure_map_[uid] = *record;
return true;
}
fast_hash_t ComputeFastHash(const SizedBuffer& password, uint64_t salt) {
fast_hash_t fast_hash;
size_t digest_size = password.size() + sizeof(salt);
std::unique_ptr<uint8_t[]> digest(new uint8_t[digest_size]);
memcpy(digest.get(), &salt, sizeof(salt));
memcpy(digest.get() + sizeof(salt), password.Data<uint8_t>(), password.size());
SHA256(digest.get(), digest_size, (uint8_t*)&fast_hash.digest);
fast_hash.salt = salt;
return fast_hash;
}
bool VerifyFast(const fast_hash_t& fast_hash, const SizedBuffer& password) {
fast_hash_t computed = ComputeFastHash(password, fast_hash.salt);
return memcmp(computed.digest, fast_hash.digest, SHA256_DIGEST_LENGTH) == 0;
}
bool DoVerify(const password_handle_t* expected_handle, const SizedBuffer& password) {
uint64_t user_id = android::base::get_unaligned<secure_id_t>(&expected_handle->user_id);
FastHashMap::const_iterator it = fast_hash_map_.find(user_id);
if (it != fast_hash_map_.end() && VerifyFast(it->second, password)) {
return true;
} else {
if (GateKeeper::DoVerify(expected_handle, password)) {
uint64_t salt;
GetRandom(&salt, sizeof(salt));
fast_hash_map_[user_id] = ComputeFastHash(password, salt);
return true;
}
}
return false;
}
private:
typedef std::unordered_map<uint32_t, failure_record_t> FailureRecordMap;
typedef std::unordered_map<uint64_t, fast_hash_t> FastHashMap;
std::unique_ptr<uint8_t[]> key_;
FailureRecordMap failure_map_;
FastHashMap fast_hash_map_;
};
} // namespace gatekeeper
#endif // SOFT_GATEKEEPER_H_

View file

@ -0,0 +1,114 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SoftGateKeeperDevice.h"
#include "SoftGateKeeper.h"
using ::android::hardware::hidl_vec;
using ::android::hardware::Return;
using ::android::hardware::gatekeeper::V1_0::GatekeeperStatusCode;
using ::gatekeeper::EnrollRequest;
using ::gatekeeper::EnrollResponse;
using ::gatekeeper::ERROR_INVALID;
using ::gatekeeper::ERROR_MEMORY_ALLOCATION_FAILED;
using ::gatekeeper::ERROR_NONE;
using ::gatekeeper::ERROR_RETRY;
using ::gatekeeper::SizedBuffer;
using ::gatekeeper::VerifyRequest;
using ::gatekeeper::VerifyResponse;
#include <limits>
namespace android {
inline SizedBuffer hidl_vec2sized_buffer(const hidl_vec<uint8_t>& vec) {
if (vec.size() == 0 || vec.size() > std::numeric_limits<uint32_t>::max()) return {};
auto dummy = new uint8_t[vec.size()];
std::copy(vec.begin(), vec.end(), dummy);
return {dummy, static_cast<uint32_t>(vec.size())};
}
Return<void> SoftGateKeeperDevice::enroll(uint32_t uid,
const hidl_vec<uint8_t>& currentPasswordHandle,
const hidl_vec<uint8_t>& currentPassword,
const hidl_vec<uint8_t>& desiredPassword,
enroll_cb _hidl_cb) {
if (desiredPassword.size() == 0) {
_hidl_cb({GatekeeperStatusCode::ERROR_GENERAL_FAILURE, 0, {}});
return {};
}
EnrollRequest request(uid, hidl_vec2sized_buffer(currentPasswordHandle),
hidl_vec2sized_buffer(desiredPassword),
hidl_vec2sized_buffer(currentPassword));
EnrollResponse response;
impl_->Enroll(request, &response);
if (response.error == ERROR_RETRY) {
_hidl_cb({GatekeeperStatusCode::ERROR_RETRY_TIMEOUT, response.retry_timeout, {}});
} else if (response.error != ERROR_NONE) {
_hidl_cb({GatekeeperStatusCode::ERROR_GENERAL_FAILURE, 0, {}});
} else {
hidl_vec<uint8_t> new_handle(response.enrolled_password_handle.Data<uint8_t>(),
response.enrolled_password_handle.Data<uint8_t>() +
response.enrolled_password_handle.size());
_hidl_cb({GatekeeperStatusCode::STATUS_OK, response.retry_timeout, new_handle});
}
return {};
}
Return<void> SoftGateKeeperDevice::verify(
uint32_t uid, uint64_t challenge,
const ::android::hardware::hidl_vec<uint8_t>& enrolledPasswordHandle,
const ::android::hardware::hidl_vec<uint8_t>& providedPassword, verify_cb _hidl_cb) {
if (enrolledPasswordHandle.size() == 0) {
_hidl_cb({GatekeeperStatusCode::ERROR_GENERAL_FAILURE, 0, {}});
return {};
}
VerifyRequest request(uid, challenge, hidl_vec2sized_buffer(enrolledPasswordHandle),
hidl_vec2sized_buffer(providedPassword));
VerifyResponse response;
impl_->Verify(request, &response);
if (response.error == ERROR_RETRY) {
_hidl_cb({GatekeeperStatusCode::ERROR_RETRY_TIMEOUT, response.retry_timeout, {}});
} else if (response.error != ERROR_NONE) {
_hidl_cb({GatekeeperStatusCode::ERROR_GENERAL_FAILURE, 0, {}});
} else {
hidl_vec<uint8_t> auth_token(
response.auth_token.Data<uint8_t>(),
response.auth_token.Data<uint8_t>() + response.auth_token.size());
_hidl_cb({response.request_reenroll ? GatekeeperStatusCode::STATUS_REENROLL
: GatekeeperStatusCode::STATUS_OK,
response.retry_timeout, auth_token});
}
return {};
}
Return<void> SoftGateKeeperDevice::deleteUser(uint32_t /*uid*/, deleteUser_cb _hidl_cb) {
_hidl_cb({GatekeeperStatusCode::ERROR_NOT_IMPLEMENTED, 0, {}});
return {};
}
Return<void> SoftGateKeeperDevice::deleteAllUsers(deleteAllUsers_cb _hidl_cb) {
_hidl_cb({GatekeeperStatusCode::ERROR_NOT_IMPLEMENTED, 0, {}});
return {};
}
} // namespace android

View file

@ -0,0 +1,80 @@
/*
* Copyright 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef SOFT_GATEKEEPER_DEVICE_H_
#define SOFT_GATEKEEPER_DEVICE_H_
#include <android/hardware/gatekeeper/1.0/IGatekeeper.h>
#include <hidl/Status.h>
#include <memory>
#include "SoftGateKeeper.h"
namespace android {
/**
* Software based GateKeeper implementation
*/
class SoftGateKeeperDevice : public ::android::hardware::gatekeeper::V1_0::IGatekeeper {
public:
SoftGateKeeperDevice() { impl_.reset(new ::gatekeeper::SoftGateKeeper()); }
// Wrappers to translate the gatekeeper HAL API to the Kegyuard Messages API.
/**
* Enrolls password_payload, which should be derived from a user selected pin or password,
* with the authentication factor private key used only for enrolling authentication
* factor data.
*
* Returns: 0 on success or an error code less than 0 on error.
* On error, enrolled_password_handle will not be allocated.
*/
::android::hardware::Return<void> enroll(
uint32_t uid, const ::android::hardware::hidl_vec<uint8_t>& currentPasswordHandle,
const ::android::hardware::hidl_vec<uint8_t>& currentPassword,
const ::android::hardware::hidl_vec<uint8_t>& desiredPassword,
enroll_cb _hidl_cb) override;
/**
* Verifies provided_password matches enrolled_password_handle.
*
* Implementations of this module may retain the result of this call
* to attest to the recency of authentication.
*
* On success, writes the address of a verification token to auth_token,
* usable to attest password verification to other trusted services. Clients
* may pass NULL for this value.
*
* Returns: 0 on success or an error code less than 0 on error
* On error, verification token will not be allocated
*/
::android::hardware::Return<void> verify(
uint32_t uid, uint64_t challenge,
const ::android::hardware::hidl_vec<uint8_t>& enrolledPasswordHandle,
const ::android::hardware::hidl_vec<uint8_t>& providedPassword,
verify_cb _hidl_cb) override;
::android::hardware::Return<void> deleteUser(uint32_t uid, deleteUser_cb _hidl_cb) override;
::android::hardware::Return<void> deleteAllUsers(deleteAllUsers_cb _hidl_cb) override;
private:
std::unique_ptr<::gatekeeper::SoftGateKeeper> impl_;
};
} // namespace android
#endif // SOFT_GATEKEEPER_DEVICE_H_

View file

@ -0,0 +1,4 @@
service vendor.gatekeeper-1-0 /vendor/bin/hw/android.hardware.gatekeeper@1.0-service.software
class hal
user system
group system

View file

@ -0,0 +1,11 @@
<manifest version="1.0" type="device">
<hal format="hidl">
<name>android.hardware.gatekeeper</name>
<transport>hwbinder</transport>
<version>1.0</version>
<interface>
<name>IGatekeeper</name>
<instance>default</instance>
</interface>
</hal>
</manifest>

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "android.hardware.gatekeeper@1.0-service"
#include <android-base/logging.h>
#include <android/hardware/gatekeeper/1.0/IGatekeeper.h>
#include <hidl/LegacySupport.h>
#include "SoftGateKeeperDevice.h"
// Generated HIDL files
using android::SoftGateKeeperDevice;
using android::hardware::gatekeeper::V1_0::IGatekeeper;
int main() {
::android::hardware::configureRpcThreadpool(1, true /* willJoinThreadpool */);
android::sp<SoftGateKeeperDevice> gatekeeper(new SoftGateKeeperDevice());
auto status = gatekeeper->registerAsService();
if (status != android::OK) {
LOG(FATAL) << "Could not register service for Gatekeeper 1.0 (software) (" << status << ")";
}
android::hardware::joinRpcThreadpool();
return -1; // Should never get here.
}

View file

@ -0,0 +1,34 @@
//
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
cc_test {
name: "gatekeeper-software-device-unit-tests",
cflags: [
"-g",
"-Wall",
"-Werror",
"-Wno-missing-field-initializers",
],
shared_libs: [
"libgatekeeper",
"libcrypto",
"libbase",
],
static_libs: ["libscrypt_static"],
srcs: ["gatekeeper_test.cpp"],
}

View file

@ -0,0 +1,178 @@
/*
* Copyright 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <arpa/inet.h>
#include <iostream>
#include <gtest/gtest.h>
#include <hardware/hw_auth_token.h>
#include "../SoftGateKeeper.h"
using ::gatekeeper::EnrollRequest;
using ::gatekeeper::EnrollResponse;
using ::gatekeeper::secure_id_t;
using ::gatekeeper::SizedBuffer;
using ::gatekeeper::SoftGateKeeper;
using ::gatekeeper::VerifyRequest;
using ::gatekeeper::VerifyResponse;
using ::testing::Test;
static SizedBuffer makePasswordBuffer(int init = 0) {
constexpr const uint32_t pw_buffer_size = 16;
auto pw_buffer = new uint8_t[pw_buffer_size];
memset(pw_buffer, init, pw_buffer_size);
return {pw_buffer, pw_buffer_size};
}
static SizedBuffer makeAndInitializeSizedBuffer(const uint8_t* data, uint32_t size) {
auto buffer = new uint8_t[size];
memcpy(buffer, data, size);
return {buffer, size};
}
static SizedBuffer copySizedBuffer(const SizedBuffer& rhs) {
return makeAndInitializeSizedBuffer(rhs.Data<uint8_t>(), rhs.size());
}
static void do_enroll(SoftGateKeeper& gatekeeper, EnrollResponse* response) {
EnrollRequest request(0, {}, makePasswordBuffer(), {});
gatekeeper.Enroll(request, response);
}
TEST(GateKeeperTest, EnrollSuccess) {
SoftGateKeeper gatekeeper;
EnrollResponse response;
do_enroll(gatekeeper, &response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, response.error);
}
TEST(GateKeeperTest, EnrollBogusData) {
SoftGateKeeper gatekeeper;
EnrollResponse response;
EnrollRequest request(0, {}, {}, {});
gatekeeper.Enroll(request, &response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_INVALID, response.error);
}
TEST(GateKeeperTest, VerifySuccess) {
SoftGateKeeper gatekeeper;
EnrollResponse enroll_response;
do_enroll(gatekeeper, &enroll_response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, enroll_response.error);
VerifyRequest request(0, 1, std::move(enroll_response.enrolled_password_handle),
makePasswordBuffer());
VerifyResponse response;
gatekeeper.Verify(request, &response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, response.error);
auto auth_token = response.auth_token.Data<hw_auth_token_t>();
ASSERT_NE(nullptr, auth_token);
ASSERT_EQ((uint32_t)HW_AUTH_PASSWORD, ntohl(auth_token->authenticator_type));
ASSERT_EQ((uint64_t)1, auth_token->challenge);
ASSERT_NE(~((uint32_t)0), auth_token->timestamp);
ASSERT_NE((uint64_t)0, auth_token->user_id);
ASSERT_NE((uint64_t)0, auth_token->authenticator_id);
}
TEST(GateKeeperTest, TrustedReEnroll) {
SoftGateKeeper gatekeeper;
EnrollResponse enroll_response;
// do_enroll enrolls an all 0 password
do_enroll(gatekeeper, &enroll_response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, enroll_response.error);
// verify first password
VerifyRequest request(0, 0, copySizedBuffer(enroll_response.enrolled_password_handle),
makePasswordBuffer());
VerifyResponse response;
gatekeeper.Verify(request, &response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, response.error);
auto auth_token = response.auth_token.Data<hw_auth_token_t>();
ASSERT_NE(nullptr, auth_token);
secure_id_t secure_id = auth_token->user_id;
// enroll new password
EnrollRequest enroll_request(0, std::move(enroll_response.enrolled_password_handle),
makePasswordBuffer(1) /* new password */,
makePasswordBuffer() /* old password */);
gatekeeper.Enroll(enroll_request, &enroll_response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, enroll_response.error);
// verify new password
VerifyRequest new_request(0, 0, std::move(enroll_response.enrolled_password_handle),
makePasswordBuffer(1));
gatekeeper.Verify(new_request, &response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, response.error);
ASSERT_NE(nullptr, response.auth_token.Data<hw_auth_token_t>());
ASSERT_EQ(secure_id, response.auth_token.Data<hw_auth_token_t>()->user_id);
}
TEST(GateKeeperTest, UntrustedReEnroll) {
SoftGateKeeper gatekeeper;
SizedBuffer provided_password;
EnrollResponse enroll_response;
// do_enroll enrolls an all 0 password
provided_password = makePasswordBuffer();
do_enroll(gatekeeper, &enroll_response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, enroll_response.error);
// verify first password
VerifyRequest request(0, 0, std::move(enroll_response.enrolled_password_handle),
std::move(provided_password));
VerifyResponse response;
gatekeeper.Verify(request, &response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, response.error);
auto auth_token = response.auth_token.Data<hw_auth_token_t>();
ASSERT_NE(nullptr, auth_token);
secure_id_t secure_id = auth_token->user_id;
EnrollRequest enroll_request(0, {}, makePasswordBuffer(1), {});
gatekeeper.Enroll(enroll_request, &enroll_response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, enroll_response.error);
// verify new password
VerifyRequest new_request(0, 0, std::move(enroll_response.enrolled_password_handle),
makePasswordBuffer(1));
gatekeeper.Verify(new_request, &response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_NONE, response.error);
ASSERT_NE(nullptr, response.auth_token.Data<hw_auth_token_t>());
ASSERT_NE(secure_id, response.auth_token.Data<hw_auth_token_t>()->user_id);
}
TEST(GateKeeperTest, VerifyBogusData) {
SoftGateKeeper gatekeeper;
VerifyResponse response;
VerifyRequest request(0, 0, {}, {});
gatekeeper.Verify(request, &response);
ASSERT_EQ(::gatekeeper::gatekeeper_error_t::ERROR_INVALID, response.error);
}

View file

@ -0,0 +1,3 @@
jdanis@google.com
swillden@google.com
guangzhu@google.com