blob: 192adc20cd786608e8be53628c0f26f308d86335 [file] [log] [blame] [edit]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/push_messaging/app_identifier.h"
#include <string.h>
#include "base/check_op.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "base/values.h"
namespace push_messaging {
const char kAppIdentifierPrefix[] = "wp:";
// sizeof is strlen + 1 since it's null-terminated.
const size_t kPrefixLength = sizeof(kAppIdentifierPrefix) - 1;
} // namespace push_messaging
namespace {
constexpr char kInstanceIDGuidSuffix[] = "-V2";
constexpr size_t kGuidSuffixLength = sizeof(kInstanceIDGuidSuffix) - 1;
constexpr char kPrefValueSeparator = '#';
std::string FromTimeToString(base::Time time) {
DCHECK(!time.is_null());
return base::NumberToString(time.ToDeltaSinceWindowsEpoch().InMilliseconds());
}
// Converts a string representation of the time into base::Time; if the string
// cannot be decoded, this function returns false and the |time| won't be
// changed.
bool FromStringToTime(std::string_view time_string,
std::optional<base::Time>& time) {
DCHECK(!time_string.empty());
int64_t milliseconds;
if (base::StringToInt64(time_string, &milliseconds) && milliseconds > 0) {
time = std::make_optional(base::Time::FromDeltaSinceWindowsEpoch(
base::Milliseconds(milliseconds)));
return true;
}
return false;
}
} // namespace
namespace push_messaging {
// static
bool AppIdentifier::UseInstanceID(const std::string& app_id) {
return base::EndsWith(app_id, kInstanceIDGuidSuffix,
base::CompareCase::SENSITIVE);
}
// static
AppIdentifier AppIdentifier::Generate(
const GURL& origin,
int64_t service_worker_registration_id,
const std::optional<base::Time>& expiration_time) {
// All new push subscriptions use Instance ID tokens.
return GenerateInternal(origin, service_worker_registration_id,
true /* use_instance_id */, expiration_time);
}
// static
AppIdentifier AppIdentifier::GenerateInvalid() {
return AppIdentifier();
}
// static
AppIdentifier AppIdentifier::GenerateInternal(
const GURL& origin,
int64_t service_worker_registration_id,
bool use_instance_id,
const std::optional<base::Time>& expiration_time) {
// Use uppercase GUID for consistency with GUIDs Push has already sent to GCM.
// Also allows detecting case mangling; see code commented "crbug.com/461867".
std::string guid =
base::ToUpperASCII(base::Uuid::GenerateRandomV4().AsLowercaseString());
if (use_instance_id) {
guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength,
kInstanceIDGuidSuffix);
}
CHECK(!guid.empty());
std::string app_id =
kAppIdentifierPrefix + origin.spec() + kPrefValueSeparator + guid;
AppIdentifier app_identifier(app_id, origin, service_worker_registration_id,
expiration_time);
app_identifier.DCheckValid();
return app_identifier;
}
AppIdentifier::AppIdentifier() : service_worker_registration_id_(-1) {}
AppIdentifier::AppIdentifier(const std::string& app_id,
const GURL& origin,
int64_t service_worker_registration_id,
const std::optional<base::Time>& expiration_time)
: app_id_(app_id),
origin_(origin),
service_worker_registration_id_(service_worker_registration_id),
expiration_time_(expiration_time) {}
bool AppIdentifier::IsExpired() const {
// TODO(crbug.com/444713031): Should DCHECK(!is_null()) as other getters.
return (expiration_time_) ? *expiration_time_ < base::Time::Now() : false;
}
void AppIdentifier::DCheckValid() const {
#if DCHECK_IS_ON()
DCHECK_GE(service_worker_registration_id_, 0);
DCHECK(origin_.is_valid());
DCHECK_EQ(origin_.DeprecatedGetOriginAsURL(), origin_);
// "wp:"
DCHECK_EQ(kAppIdentifierPrefix, app_id_.substr(0, kPrefixLength));
// Optional (origin.spec() + '#')
if (app_id_.size() != kPrefixLength + kGuidLength) {
constexpr size_t suffix_length = 1 /* kPrefValueSeparator */ + kGuidLength;
DCHECK_GT(app_id_.size(), kPrefixLength + suffix_length);
DCHECK_EQ(origin_, GURL(app_id_.substr(
kPrefixLength,
app_id_.size() - kPrefixLength - suffix_length)));
DCHECK_EQ(std::string(1, kPrefValueSeparator),
app_id_.substr(app_id_.size() - suffix_length, 1));
}
// GUID. In order to distinguish them, an app_id created for an InstanceID
// based subscription has the last few characters of the GUID overwritten with
// kInstanceIDGuidSuffix (which contains non-hex characters invalid in GUIDs).
std::string guid = app_id_.substr(app_id_.size() - kGuidLength);
if (UseInstanceID(app_id_)) {
DCHECK(!base::Uuid::ParseCaseInsensitive(guid).is_valid());
// Replace suffix with valid hex so we can validate the rest of the string.
guid = guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength,
kGuidSuffixLength, 'C');
}
DCHECK(base::Uuid::ParseCaseInsensitive(guid).is_valid());
#endif // DCHECK_IS_ON()
}
std::string AppIdentifier::ToPrefValue() const {
std::string result = origin().spec() + kPrefValueSeparator +
base::NumberToString(service_worker_registration_id());
if (expiration_time()) {
result += kPrefValueSeparator + FromTimeToString(*expiration_time());
}
return result;
}
// static
std::optional<AppIdentifier> AppIdentifier::FromPrefValue(
const std::string& app_id,
std::string_view pref_value) {
GURL origin;
int64_t service_worker_registration_id;
std::optional<base::Time> expiration_time;
std::vector<std::string> parts =
base::SplitString(pref_value, std::string(1, kPrefValueSeparator),
base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if (parts.size() < 2 || parts.size() > 3) {
return std::nullopt;
}
if (!base::StringToInt64(parts[1], &service_worker_registration_id)) {
return std::nullopt;
}
origin = GURL(parts[0]);
if (!origin.is_valid()) {
return std::nullopt;
}
if (parts.size() == 3 && !FromStringToTime(parts[2], expiration_time)) {
return std::nullopt;
}
AppIdentifier result{app_id, origin, service_worker_registration_id,
expiration_time};
result.DCheckValid();
return result;
}
} // namespace push_messaging