// Copyright (c) 2012 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 "google_apis/gaia/oauth_request_signer.h"
#include <cctype>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <map>
#include <string>
#include "base/base64.h"
#include "base/format_macros.h"
#include "base/logging.h"
#include "base/rand_util.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "crypto/hmac.h"
#include "url/gurl.h"
namespace {
const int kHexBase = 16;
char kHexDigits[] = "0123456789ABCDEF";
const size_t kHmacDigestLength = 20;
const int kMaxNonceLength = 30;
const int kMinNonceLength = 15;
const char kOAuthConsumerKeyLabel[] = "oauth_consumer_key";
const char kOAuthNonceCharacters[] =
"abcdefghijklmnopqrstuvwyz"
"ABCDEFGHIJKLMNOPQRSTUVWYZ"
"0123456789_";
const char kOAuthNonceLabel[] = "oauth_nonce";
const char kOAuthSignatureLabel[] = "oauth_signature";
const char kOAuthSignatureMethodLabel[] = "oauth_signature_method";
const char kOAuthTimestampLabel[] = "oauth_timestamp";
const char kOAuthTokenLabel[] = "oauth_token";
const char kOAuthVersion[] = "1.0";
const char kOAuthVersionLabel[] = "oauth_version";
enum ParseQueryState {
START_STATE,
KEYWORD_STATE,
VALUE_STATE,
};
const std::string HttpMethodName(OAuthRequestSigner::HttpMethod method) {
switch (method) {
case OAuthRequestSigner::GET_METHOD:
return "GET";
case OAuthRequestSigner::POST_METHOD:
return "POST";
}
NOTREACHED();
return std::string();
}
const std::string SignatureMethodName(
OAuthRequestSigner::SignatureMethod method) {
switch (method) {
case OAuthRequestSigner::HMAC_SHA1_SIGNATURE:
return "HMAC-SHA1";
case OAuthRequestSigner::RSA_SHA1_SIGNATURE:
return "RSA-SHA1";
case OAuthRequestSigner::PLAINTEXT_SIGNATURE:
return "PLAINTEXT";
}
NOTREACHED();
return std::string();
}
std::string BuildBaseString(const GURL& request_base_url,
OAuthRequestSigner::HttpMethod http_method,
const std::string& base_parameters) {
return base::StringPrintf("%s&%s&%s",
HttpMethodName(http_method).c_str(),
OAuthRequestSigner::Encode(
request_base_url.spec()).c_str(),
OAuthRequestSigner::Encode(
base_parameters).c_str());
}
std::string BuildBaseStringParameters(
const OAuthRequestSigner::Parameters& parameters) {
std::string result;
OAuthRequestSigner::Parameters::const_iterator cursor;
OAuthRequestSigner::Parameters::const_iterator limit;
bool first = true;
for (cursor = parameters.begin(), limit = parameters.end();
cursor != limit;
++cursor) {
if (first)
first = false;
else
result += '&';
result += OAuthRequestSigner::Encode(cursor->first);
result += '=';
result += OAuthRequestSigner::Encode(cursor->second);
}
return result;
}
std::string GenerateNonce() {
char result[kMaxNonceLength + 1];
int length = base::RandUint64() % (kMaxNonceLength - kMinNonceLength + 1) +
kMinNonceLength;
result[length] = '\0';
for (int index = 0; index < length; ++index)
result[index] = kOAuthNonceCharacters[
base::RandUint64() % (sizeof(kOAuthNonceCharacters) - 1)];
return result;
}
std::string GenerateTimestamp() {
return base::StringPrintf(
"%" PRId64,
(base::Time::NowFromSystemTime() - base::Time::UnixEpoch()).InSeconds());
}
// Creates a string-to-string, keyword-value map from a parameter/query string
// that uses ampersand (&) to seperate paris and equals (=) to seperate
// keyword from value.
bool ParseQuery(const std::string& query,
OAuthRequestSigner::Parameters* parameters_result) {
std::string::const_iterator cursor;
std::string keyword;
std::string::const_iterator limit;
OAuthRequestSigner::Parameters parameters;
ParseQueryState state;
std::string value;
state = START_STATE;
for (cursor = query.begin(), limit = query.end();
cursor != limit;
++cursor) {
char character = *cursor;
switch (state) {
case KEYWORD_STATE:
switch (character) {
case '&':
parameters[keyword] = value;
keyword = "";
value = "";
state = START_STATE;
break;
case '=':
state = VALUE_STATE;
break;
default:
keyword += character;
}
break;
case START_STATE:
switch (character) {
case '&': // Intentionally falling through
case '=':
return false;
default:
keyword += character;
state = KEYWORD_STATE;
}
break;
case VALUE_STATE:
switch (character) {
case '=':
return false;
case '&':
parameters[keyword] = value;
keyword = "";
value = "";
state = START_STATE;
break;
default:
value += character;
}
break;
}
}
switch (state) {
case START_STATE:
break;
case KEYWORD_STATE: // Intentionally falling through
case VALUE_STATE:
parameters[keyword] = value;
break;
default:
NOTREACHED();
}
*parameters_result = parameters;
return true;
}
// Creates the value for the oauth_signature parameter when the
// oauth_signature_method is HMAC-SHA1.
bool SignHmacSha1(const std::string& text,
const std::string& key,
std::string* signature_return) {
crypto::HMAC hmac(crypto::HMAC::SHA1);
DCHECK(hmac.DigestLength() == kHmacDigestLength);
unsigned char digest[kHmacDigestLength];
bool result = hmac.Init(key) &&
hmac.Sign(text, digest, kHmacDigestLength);
if (result) {
base::Base64Encode(
std::string(reinterpret_cast<const char*>(digest), kHmacDigestLength),
signature_return);
}
return result;
}
// Creates the value for the oauth_signature parameter when the
// oauth_signature_method is PLAINTEXT.
//
// Not yet implemented, and might never be.
bool SignPlaintext(const std::string& text,
const std::string& key,
std::string* result) {
NOTIMPLEMENTED();
return false;
}
// Creates the value for the oauth_signature parameter when the
// oauth_signature_method is RSA-SHA1.
//
// Not yet implemented, and might never be.
bool SignRsaSha1(const std::string& text,
const std::string& key,
std::string* result) {
NOTIMPLEMENTED();
return false;
}
// Adds parameters that are required by OAuth added as needed to |parameters|.
void PrepareParameters(OAuthRequestSigner::Parameters* parameters,
OAuthRequestSigner::SignatureMethod signature_method,
OAuthRequestSigner::HttpMethod http_method,
const std::string& consumer_key,
const std::string& token_key) {
if (parameters->find(kOAuthNonceLabel) == parameters->end())
(*parameters)[kOAuthNonceLabel] = GenerateNonce();
if (parameters->find(kOAuthTimestampLabel) == parameters->end())
(*parameters)[kOAuthTimestampLabel] = GenerateTimestamp();
(*parameters)[kOAuthConsumerKeyLabel] = consumer_key;
(*parameters)[kOAuthSignatureMethodLabel] =
SignatureMethodName(signature_method);
(*parameters)[kOAuthTokenLabel] = token_key;
(*parameters)[kOAuthVersionLabel] = kOAuthVersion;
}
// Implements shared signing logic, generating the signature and storing it in
// |parameters|. Returns true if the signature has been generated succesfully.
bool SignParameters(const GURL& request_base_url,
OAuthRequestSigner::SignatureMethod signature_method,
OAuthRequestSigner::HttpMethod http_method,
const std::string& consumer_key,
const std::string& consumer_secret,
const std::string& token_key,
const std::string& token_secret,
OAuthRequestSigner::Parameters* parameters) {
DCHECK(request_base_url.is_valid());
PrepareParameters(parameters, signature_method, http_method,
consumer_key, token_key);
std::string base_parameters = BuildBaseStringParameters(*parameters);
std::string base = BuildBaseString(request_base_url, http_method,
base_parameters);
std::string key = consumer_secret + '&' + token_secret;
bool is_signed = false;
std::string signature;
switch (signature_method) {
case OAuthRequestSigner::HMAC_SHA1_SIGNATURE:
is_signed = SignHmacSha1(base, key, &signature);
break;
case OAuthRequestSigner::RSA_SHA1_SIGNATURE:
is_signed = SignRsaSha1(base, key, &signature);
break;
case OAuthRequestSigner::PLAINTEXT_SIGNATURE:
is_signed = SignPlaintext(base, key, &signature);
break;
default:
NOTREACHED();
}
if (is_signed)
(*parameters)[kOAuthSignatureLabel] = signature;
return is_signed;
}
} // namespace
// static
bool OAuthRequestSigner::Decode(const std::string& text,
std::string* decoded_text) {
std::string accumulator;
std::string::const_iterator cursor;
std::string::const_iterator limit;
for (limit = text.end(), cursor = text.begin(); cursor != limit; ++cursor) {
char character = *cursor;
if (character == '%') {
++cursor;
if (cursor == limit)
return false;
char* first = strchr(kHexDigits, *cursor);
if (!first)
return false;
int high = first - kHexDigits;
DCHECK(high >= 0 && high < kHexBase);
++cursor;
if (cursor == limit)
return false;
char* second = strchr(kHexDigits, *cursor);
if (!second)
return false;
int low = second - kHexDigits;
DCHECK(low >= 0 || low < kHexBase);
char decoded = static_cast<char>(high * kHexBase + low);
DCHECK(!(IsAsciiAlpha(decoded) || IsAsciiDigit(decoded)));
DCHECK(!(decoded && strchr("-._~", decoded)));
accumulator += decoded;
} else {
accumulator += character;
}
}
*decoded_text = accumulator;
return true;
}
// static
std::string OAuthRequestSigner::Encode(const std::string& text) {
std::string result;
std::string::const_iterator cursor;
std::string::const_iterator limit;
for (limit = text.end(), cursor = text.begin(); cursor != limit; ++cursor) {
char character = *cursor;
if (IsAsciiAlpha(character) || IsAsciiDigit(character)) {
result += character;
} else {
switch (character) {
case '-':
case '.':
case '_':
case '~':
result += character;
break;
default:
unsigned char byte = static_cast<unsigned char>(character);
result = result + '%' + kHexDigits[byte / kHexBase] +
kHexDigits[byte % kHexBase];
}
}
}
return result;
}
// static
bool OAuthRequestSigner::ParseAndSign(const GURL& request_url_with_parameters,
SignatureMethod signature_method,
HttpMethod http_method,
const std::string& consumer_key,
const std::string& consumer_secret,
const std::string& token_key,
const std::string& token_secret,
std::string* result) {
DCHECK(request_url_with_parameters.is_valid());
Parameters parameters;
if (request_url_with_parameters.has_query()) {
const std::string& query = request_url_with_parameters.query();
if (!query.empty()) {
if (!ParseQuery(query, ¶meters))
return false;
}
}
std::string spec = request_url_with_parameters.spec();
std::string url_without_parameters = spec;
std::string::size_type question = spec.find("?");
if (question != std::string::npos)
url_without_parameters = spec.substr(0,question);
return SignURL(GURL(url_without_parameters), parameters, signature_method,
http_method, consumer_key, consumer_secret, token_key,
token_secret, result);
}
// static
bool OAuthRequestSigner::SignURL(
const GURL& request_base_url,
const Parameters& request_parameters,
SignatureMethod signature_method,
HttpMethod http_method,
const std::string& consumer_key,
const std::string& consumer_secret,
const std::string& token_key,
const std::string& token_secret,
std::string* signed_text_return) {
DCHECK(request_base_url.is_valid());
Parameters parameters(request_parameters);
bool is_signed = SignParameters(request_base_url, signature_method,
http_method, consumer_key, consumer_secret,
token_key, token_secret, ¶meters);
if (is_signed) {
std::string signed_text;
switch (http_method) {
case GET_METHOD:
signed_text = request_base_url.spec() + '?';
// Intentionally falling through
case POST_METHOD:
signed_text += BuildBaseStringParameters(parameters);
break;
default:
NOTREACHED();
}
*signed_text_return = signed_text;
}
return is_signed;
}
// static
bool OAuthRequestSigner::SignAuthHeader(
const GURL& request_base_url,
const Parameters& request_parameters,
SignatureMethod signature_method,
HttpMethod http_method,
const std::string& consumer_key,
const std::string& consumer_secret,
const std::string& token_key,
const std::string& token_secret,
std::string* signed_text_return) {
DCHECK(request_base_url.is_valid());
Parameters parameters(request_parameters);
bool is_signed = SignParameters(request_base_url, signature_method,
http_method, consumer_key, consumer_secret,
token_key, token_secret, ¶meters);
if (is_signed) {
std::string signed_text = "OAuth ";
bool first = true;
for (Parameters::const_iterator param = parameters.begin();
param != parameters.end();
++param) {
if (first)
first = false;
else
signed_text += ", ";
signed_text +=
base::StringPrintf(
"%s=\"%s\"",
OAuthRequestSigner::Encode(param->first).c_str(),
OAuthRequestSigner::Encode(param->second).c_str());
}
*signed_text_return = signed_text;
}
return is_signed;
}