Bug 1256488 - Add a Base64 URL-decoder for C++ and chrome JS callers. r=mt,baku

MozReview-Commit-ID: IrDwImYfHRu

--HG--
extra : rebase_source : ed7da7447e5d70c596234961625fcd4b8139814f
This commit is contained in:
Kit Cambridge 2016-03-22 12:09:04 -07:00
Родитель 033ba99b0d
Коммит d21d6da5e8
20 изменённых файлов: 472 добавлений и 94 удалений

Просмотреть файл

@ -4427,10 +4427,12 @@ this.DOMApplicationRegistry = {
return "INVALID_SEGMENTS_NUMBER";
}
// We need to translate the base64 alphabet used in JWT to our base64 alphabet
// before calling atob.
let decodedReceipt = JSON.parse(atob(segments[1].replace(/-/g, '+')
.replace(/_/g, '/')));
let jwtBuffer = ChromeUtils.base64URLDecode(segments[1], {
// JWT/JWS prohibits padding per RFC 7515, section 2.
padding: "reject",
});
let textDecoder = new TextDecoder("utf-8");
let decodedReceipt = JSON.parse(textDecoder.decode(jwtBuffer));
if (!decodedReceipt) {
return "INVALID_RECEIPT_ENCODING";
}

Просмотреть файл

@ -5,6 +5,7 @@
#include "ChromeUtils.h"
#include "mozilla/Base64.h"
#include "mozilla/BasePrincipal.h"
namespace mozilla {
@ -50,6 +51,61 @@ ThreadSafeChromeUtils::NondeterministicGetWeakSetKeys(GlobalObject& aGlobal,
}
}
/* static */ void
ThreadSafeChromeUtils::Base64URLEncode(GlobalObject& aGlobal,
const ArrayBufferViewOrArrayBuffer& aSource,
const Base64URLEncodeOptions& aOptions,
nsACString& aResult,
ErrorResult& aRv)
{
size_t length = 0;
uint8_t* data = nullptr;
if (aSource.IsArrayBuffer()) {
const ArrayBuffer& buffer = aSource.GetAsArrayBuffer();
buffer.ComputeLengthAndData();
length = buffer.Length();
data = buffer.Data();
} else if (aSource.IsArrayBufferView()) {
const ArrayBufferView& view = aSource.GetAsArrayBufferView();
view.ComputeLengthAndData();
length = view.Length();
data = view.Data();
} else {
MOZ_CRASH("Uninitialized union: expected buffer or view");
}
nsresult rv = mozilla::Base64URLEncode(length, data, aOptions, aResult);
if (NS_WARN_IF(NS_FAILED(rv))) {
aResult.Truncate();
aRv.Throw(rv);
}
}
/* static */ void
ThreadSafeChromeUtils::Base64URLDecode(GlobalObject& aGlobal,
const nsACString& aString,
const Base64URLDecodeOptions& aOptions,
JS::MutableHandle<JSObject*> aRetval,
ErrorResult& aRv)
{
FallibleTArray<uint8_t> data;
nsresult rv = mozilla::Base64URLDecode(aString, aOptions, data);
if (NS_WARN_IF(NS_FAILED(rv))) {
aRv.Throw(rv);
return;
}
JS::Rooted<JSObject*> buffer(aGlobal.Context(),
ArrayBuffer::Create(aGlobal.Context(),
data.Length(),
data.Elements()));
if (NS_WARN_IF(!buffer)) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
aRetval.set(buffer);
}
/* static */ void
ChromeUtils::OriginAttributesToSuffix(dom::GlobalObject& aGlobal,
const dom::OriginAttributesDictionary& aAttrs,

Просмотреть файл

@ -20,6 +20,8 @@ class HeapSnapshot;
namespace dom {
class ArrayBufferViewOrArrayBuffer;
class ThreadSafeChromeUtils
{
public:
@ -43,6 +45,18 @@ public:
JS::Handle<JS::Value> aSet,
JS::MutableHandle<JS::Value> aRetval,
ErrorResult& aRv);
static void Base64URLEncode(GlobalObject& aGlobal,
const ArrayBufferViewOrArrayBuffer& aSource,
const Base64URLEncodeOptions& aOptions,
nsACString& aResult,
ErrorResult& aRv);
static void Base64URLDecode(GlobalObject& aGlobal,
const nsACString& aString,
const Base64URLDecodeOptions& aOptions,
JS::MutableHandle<JSObject*> aRetval,
ErrorResult& aRv);
};
class ChromeUtils : public ThreadSafeChromeUtils

Просмотреть файл

@ -0,0 +1,103 @@
"use strict";
function run_test() {
test_base64URLEncode();
test_base64URLDecode();
}
// Test vectors from RFC 4648, section 10.
let textTests = {
"": "",
"f": "Zg",
"fo": "Zm8",
"foo": "Zm9v",
"foob": "Zm9vYg",
"fooba": "Zm9vYmE",
"foobar": "Zm9vYmFy",
}
// Examples from RFC 4648, section 9.
let binaryTests = [{
decoded: new Uint8Array([0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e]),
encoded: "FPucA9l-",
}, {
decoded: new Uint8Array([0x14, 0xfb, 0x9c, 0x03, 0xd9]),
encoded: "FPucA9k",
}, {
decoded: new Uint8Array([0x14, 0xfb, 0x9c, 0x03]),
encoded: "FPucAw",
}];
function padEncodedValue(value) {
switch (value.length % 4) {
case 0:
return value;
case 2:
return value + "==";
case 3:
return value + "=";
default:
throw new TypeError("Invalid encoded value");
}
}
function testEncode(input, encoded) {
equal(ChromeUtils.base64URLEncode(input, { pad: false }),
encoded, encoded + " without padding");
equal(ChromeUtils.base64URLEncode(input, { pad: true }),
padEncodedValue(encoded), encoded + " with padding");
}
function test_base64URLEncode() {
throws(_ => ChromeUtils.base64URLEncode(new Uint8Array(0)), /TypeError/,
"Should require encoding options");
throws(_ => ChromeUtils.base64URLEncode(new Uint8Array(0), {}), /TypeError/,
"Encoding should require the padding option");
for (let {decoded, encoded} of binaryTests) {
testEncode(decoded, encoded);
}
let textEncoder = new TextEncoder("utf-8");
for (let decoded of Object.keys(textTests)) {
let input = textEncoder.encode(decoded);
testEncode(input, textTests[decoded]);
}
}
function testDecode(input, decoded) {
let buffer = ChromeUtils.base64URLDecode(input, { padding: "reject" });
deepEqual(new Uint8Array(buffer), decoded, input + " with padding rejected");
let paddedValue = padEncodedValue(input);
buffer = ChromeUtils.base64URLDecode(paddedValue, { padding: "ignore" });
deepEqual(new Uint8Array(buffer), decoded, input + " with padding ignored");
if (paddedValue.length > input.length) {
throws(_ => ChromeUtils.base64URLDecode(paddedValue, { padding: "reject" }),
paddedValue + " with padding rejected should throw");
throws(_ => ChromeUtils.base64URLDecode(input, { padding: "require" }),
input + " with padding required should throw");
buffer = ChromeUtils.base64URLDecode(paddedValue, { padding: "require" });
deepEqual(new Uint8Array(buffer), decoded, paddedValue + " with padding required");
}
}
function test_base64URLDecode() {
throws(_ => ChromeUtils.base64URLDecode(""), /TypeError/,
"Should require decoding options");
throws(_ => ChromeUtils.base64URLEncode("", {}), /TypeError/,
"Decoding should require the padding option");
for (let {decoded, encoded} of binaryTests) {
testDecode(encoded, decoded);
}
let textEncoder = new TextEncoder("utf-8");
for (let decoded of Object.keys(textTests)) {
let expectedBuffer = textEncoder.encode(decoded);
testDecode(textTests[decoded], expectedBuffer);
}
}

Просмотреть файл

@ -51,3 +51,4 @@ head = head_xml.js
head = head_xml.js
[test_xmlserializer.js]
[test_cancelPrefetch.js]
[test_chromeutils_base64.js]

Просмотреть файл

@ -10,8 +10,7 @@ const Cu = Components.utils;
Cu.importGlobalProperties(['crypto']);
this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray',
'getCryptoParams',
'base64UrlDecode'];
'getCryptoParams'];
var UTF8 = new TextEncoder('utf-8');
@ -118,34 +117,6 @@ function chunkArray(array, size) {
return result;
}
this.base64UrlDecode = function(s) {
s = s.replace(/-/g, '+').replace(/_/g, '/');
// Replace padding if it was stripped by the sender.
// See http://tools.ietf.org/html/rfc4648#section-4
switch (s.length % 4) {
case 0:
break; // No pad chars in this case
case 2:
s += '==';
break; // Two pad chars
case 3:
s += '=';
break; // One pad char
default:
throw new Error('Illegal base64url string!');
}
// With correct padding restored, apply the standard base64 decoder
var decoded = atob(s);
var array = new Uint8Array(new ArrayBuffer(decoded.length));
for (var i = 0; i < decoded.length; i++) {
array[i] = decoded.charCodeAt(i);
}
return array;
};
this.concatArray = function(arrays) {
var size = arrays.reduce((total, a) => total + a.byteLength, 0);
var index = 0;
@ -226,7 +197,11 @@ this.PushCrypto = {
return Promise.reject(new Error('Data truncated'));
}
let senderKey = base64UrlDecode(aSenderPublicKey)
let senderKey = ChromeUtils.base64URLDecode(aSenderPublicKey, {
// draft-ietf-httpbis-encryption-encoding-01 prohibits padding.
padding: "reject",
});
return Promise.all([
crypto.subtle.importKey('raw', senderKey, ECDH_KEY,
false, ['deriveBits']),
@ -238,7 +213,8 @@ this.PushCrypto = {
subscriptionPrivateKey, 256))
.then(ikm => this._deriveKeyAndNonce(aPadSize,
new Uint8Array(ikm),
base64UrlDecode(aSalt),
ChromeUtils.base64URLDecode(aSalt,
{ padding: "reject" }),
aPublicKey,
senderKey,
aAuthenticationSecret))

Просмотреть файл

@ -22,7 +22,6 @@ const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bi
const {
PushCrypto,
base64UrlDecode,
concatArray,
getCryptoParams,
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
@ -118,7 +117,10 @@ this.PushServiceAndroidGCM = {
};
cryptoParams = getCryptoParams(headers);
// Ciphertext is (urlsafe) Base 64 encoded.
message = base64UrlDecode(data.message);
message = ChromeUtils.base64URLDecode(data.message, {
// The Push server may append padding.
padding: "ignore",
});
}
console.debug("Delivering message to main PushService:", message, cryptoParams);

Просмотреть файл

@ -21,7 +21,6 @@ const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
const {
PushCrypto,
base64UrlDecode,
getCryptoParams,
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
@ -936,7 +935,10 @@ this.PushServiceWebSocket = {
} else {
let params = getCryptoParams(update.headers);
if (params) {
let message = base64UrlDecode(update.data);
let message = ChromeUtils.base64URLDecode(update.data, {
// The Push server may append padding.
padding: "ignore",
});
promise = this._mainPushService.receivedPushMessage(
update.channelID,
update.version,

Просмотреть файл

@ -331,21 +331,31 @@ PushSubscription::GetKey(JSContext* aCx,
}
void
PushSubscription::ToJSON(PushSubscriptionJSON& aJSON)
PushSubscription::ToJSON(PushSubscriptionJSON& aJSON, ErrorResult& aRv)
{
aJSON.mEndpoint.Construct();
aJSON.mEndpoint.Value() = mEndpoint;
Base64URLEncodeOptions encodeOptions;
encodeOptions.mPad = false;
aJSON.mKeys.mP256dh.Construct();
nsresult rv = Base64URLEncode(mRawP256dhKey.Length(),
mRawP256dhKey.Elements(),
encodeOptions,
aJSON.mKeys.mP256dh.Value());
Unused << NS_WARN_IF(NS_FAILED(rv));
if (NS_WARN_IF(NS_FAILED(rv))) {
aRv.Throw(rv);
return;
}
aJSON.mKeys.mAuth.Construct();
rv = Base64URLEncode(mAuthSecret.Length(), mAuthSecret.Elements(),
aJSON.mKeys.mAuth.Value());
Unused << NS_WARN_IF(NS_FAILED(rv));
encodeOptions, aJSON.mKeys.mAuth.Value());
if (NS_WARN_IF(NS_FAILED(rv))) {
aRv.Throw(rv);
return;
}
}
already_AddRefed<Promise>

Просмотреть файл

@ -73,7 +73,7 @@ public:
Unsubscribe(ErrorResult& aRv);
void
ToJSON(PushSubscriptionJSON& aJSON);
ToJSON(PushSubscriptionJSON& aJSON, ErrorResult& aRv);
protected:
~PushSubscription();

Просмотреть файл

@ -1,7 +1,6 @@
'use strict';
const {
base64UrlDecode,
getCryptoParams,
PushCrypto,
} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
@ -138,7 +137,9 @@ add_task(function* test_crypto_decodeMsg() {
x: 'sd85ZCbEG6dEkGMCmDyGBIt454Qy-Yo-1xhbaT2Jlk4',
y: 'vr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs',
};
let publicKey = base64UrlDecode('BLHfOWQmxBunRJBjApg8hgSLeOeEMvmKPtcYW2k9iZZOvr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs');
let publicKey = ChromeUtils.base64URLDecode('BLHfOWQmxBunRJBjApg8hgSLeOeEMvmKPtcYW2k9iZZOvr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs', {
padding: "reject",
});
let expectedSuccesses = [{
desc: 'padSize = 2, rs = 24, pad = 0',
@ -177,8 +178,13 @@ add_task(function* test_crypto_decodeMsg() {
padSize: 2,
}];
for (let test of expectedSuccesses) {
let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null;
let result = yield PushCrypto.decodeMsg(base64UrlDecode(test.data),
let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
padding: "reject",
}) : null;
let data = ChromeUtils.base64URLDecode(test.data, {
padding: "reject",
});
let result = yield PushCrypto.decodeMsg(data,
privateKey, publicKey,
test.senderPublicKey, test.salt,
test.rs, authSecret, test.padSize);
@ -223,9 +229,14 @@ add_task(function* test_crypto_decodeMsg() {
rs: 25,
}];
for (let test of expectedFailures) {
let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null;
let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
padding: "reject",
}) : null;
let data = ChromeUtils.base64URLDecode(test.data, {
padding: "reject",
});
yield rejects(
PushCrypto.decodeMsg(base64UrlDecode(test.data), privateKey, publicKey,
PushCrypto.decodeMsg(data, privateKey, publicKey,
test.senderPublicKey, test.salt, test.rs,
authSecret, test.padSize),
test.desc

Просмотреть файл

@ -4,7 +4,6 @@
'use strict';
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
const {base64UrlDecode} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
let db;
let userAgentID = 'f5b47f8d-771f-4ea3-b999-91c135f8766d';
@ -27,9 +26,13 @@ function putRecord(channelID, scope, publicKey, privateKey, authSecret) {
originAttributes: '',
quota: Infinity,
systemRecord: true,
p256dhPublicKey: base64UrlDecode(publicKey),
p256dhPublicKey: ChromeUtils.base64URLDecode(publicKey, {
padding: "reject",
}),
p256dhPrivateKey: privateKey,
authenticationSecret: base64UrlDecode(authSecret),
authenticationSecret: ChromeUtils.base64URLDecode(authSecret, {
padding: "reject",
}),
});
}

Просмотреть файл

@ -6,7 +6,6 @@
Cu.import("resource://gre/modules/Services.jsm");
const {PushDB, PushService, PushServiceHttp2} = serviceExports;
const {base64UrlDecode} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
var prefs;
var tlsProfile;
@ -129,7 +128,9 @@ add_task(function* test_pushNotifications() {
pushEndpoint: serverURL + '/pushEndpoint4',
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint4',
scope: 'https://example.com/page/4',
p256dhPublicKey: base64UrlDecode('BEcvDzkWCrUtjU_wygL98sbQCQrW1lY9irtgGnlCc4B0JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'),
p256dhPublicKey: ChromeUtils.base64URLDecode('BEcvDzkWCrUtjU_wygL98sbQCQrW1lY9irtgGnlCc4B0JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU', {
padding: "reject",
}),
p256dhPrivateKey: {
crv: 'P-256',
d: 'fWi7tZaX0Pk6WnLrjQ3kiRq_g5XStL5pdH4pllNCqXw',
@ -139,7 +140,9 @@ add_task(function* test_pushNotifications() {
x: 'Ry8PORYKtS2NT_DKAv3yxtAJCtbWVj2Ku2AaeUJzgHQ',
y: 'JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'
},
authenticationSecret: base64UrlDecode('cwDVC1iwAn8E37mkR3tMSg'),
authenticationSecret: ChromeUtils.base64URLDecode('cwDVC1iwAn8E37mkR3tMSg', {
padding: "reject",
}),
originAttributes: ChromeUtils.originAttributesToSuffix(
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
quota: Infinity,

Просмотреть файл

@ -38,5 +38,6 @@ interface PushSubscription
Promise<boolean> unsubscribe();
// Implements the custom serializer specified in Push API, section 9.
[Throws]
PushSubscriptionJSON toJSON();
};

Просмотреть файл

@ -55,6 +55,28 @@ interface ThreadSafeChromeUtils {
*/
[Throws, NewObject]
static any nondeterministicGetWeakSetKeys(any aSet);
/**
* Converts a buffer to a Base64 URL-encoded string per RFC 4648.
*
* @param source The buffer to encode.
* @param options Additional encoding options.
* @returns The encoded string.
*/
[Throws]
static ByteString base64URLEncode(BufferSource source,
Base64URLEncodeOptions options);
/**
* Decodes a Base64 URL-encoded string per RFC 4648.
*
* @param string The string to decode.
* @param options Additional decoding options.
* @returns The decoded buffer.
*/
[Throws, NewObject]
static ArrayBuffer base64URLDecode(ByteString string,
Base64URLDecodeOptions options);
};
/**
@ -88,3 +110,31 @@ dictionary HeapSnapshotBoundaries {
object debugger;
boolean runtime;
};
dictionary Base64URLEncodeOptions {
/** Specifies whether the output should be padded with "=" characters. */
required boolean pad;
};
enum Base64URLDecodePadding {
/**
* Fails decoding if the input is unpadded. RFC 4648, section 3.2 requires
* padding, unless the referring specification prohibits it.
*/
"require",
/** Tolerates padded and unpadded input. */
"ignore",
/**
* Fails decoding if the input is padded. This follows the strict base64url
* variant used in JWS (RFC 7515, Appendix C) and HTTP Encrypted
* Content-Encoding (draft-ietf-httpbis-encryption-encoding-01).
*/
"reject"
};
dictionary Base64URLDecodeOptions {
/** Specifies the padding mode for decoding the input. */
required Base64URLDecodePadding padding;
};

Просмотреть файл

@ -1002,10 +1002,12 @@ this.MobileIdentityManager = {
return Promise.reject(ERROR_INVALID_ASSERTION);
}
// We need to translate the base64 alphabet used in JWT to our base64
// alphabet before calling atob.
let decodedPayload = JSON.parse(atob(segments[1].replace(/-/g, '+')
.replace(/_/g, '/')));
let payloadBuffer = ChromeUtils.base64URLDecode(segments[1], {
// `IdentityCryptoService` pads output.
padding: "require",
});
let textDecoder = new TextDecoder("utf-8");
let decodedPayload = JSON.parse(textDecoder.decode(payloadBuffer));
if (!decodedPayload || !decodedPayload.verifiedMSISDN) {
return Promise.reject(ERROR_INVALID_ASSERTION);

Просмотреть файл

@ -44,28 +44,6 @@ HexEncode(const SECItem * it, nsACString & result)
}
}
nsresult
Base64UrlEncodeImpl(const nsACString & utf8Input, nsACString & result)
{
nsresult rv = Base64Encode(utf8Input, result);
NS_ENSURE_SUCCESS(rv, rv);
nsACString::char_type * out = result.BeginWriting();
nsACString::size_type length = result.Length();
// base64url encoding is defined in RFC 4648. It replaces the last two
// alphabet characters of base64 encoding with '-' and '_' respectively.
for (unsigned int i = 0; i < length; ++i) {
if (out[i] == '+') {
out[i] = '-';
} else if (out[i] == '/') {
out[i] = '_';
}
}
return NS_OK;
}
#define DSA_KEY_TYPE_STRING (NS_LITERAL_CSTRING("DS160"))
#define RSA_KEY_TYPE_STRING (NS_LITERAL_CSTRING("RS256"))
@ -237,7 +215,11 @@ NS_IMETHODIMP
IdentityCryptoService::Base64UrlEncode(const nsACString & utf8Input,
nsACString & result)
{
return Base64UrlEncodeImpl(utf8Input, result);
dom::Base64URLEncodeOptions options;
options.mPad = true;
return Base64URLEncode(utf8Input.Length(),
reinterpret_cast<const uint8_t*>(utf8Input.BeginReading()), options,
result);
}
KeyPair::KeyPair(SECKEYPrivateKey * privateKey, SECKEYPublicKey * publicKey)
@ -531,9 +513,9 @@ SignRunnable::Run()
mRv = MapSECStatus(PK11_Sign(mPrivateKey, &sig, &hashItem));
}
if (NS_SUCCEEDED(mRv)) {
nsDependentCSubstring sigString(
reinterpret_cast<const char*>(sig.data), sig.len);
mRv = Base64UrlEncodeImpl(sigString, mSignature);
dom::Base64URLEncodeOptions encodeOptions;
encodeOptions.mPad = true;
mRv = Base64URLEncode(sig.len, sig.data, encodeOptions, mSignature);
}
SECITEM_FreeItem(&sig, false);
}

Просмотреть файл

@ -17,7 +17,7 @@ interface nsIIdentitySignCallback;
* byte sequence.
* e.g. "eyJhbGciOiJSUzI1NiJ9"
* http://en.wikipedia.org/wiki/Base64#Variants_summary_table
* we use the no-padding approach to base64-url-encoding
* we use the padded approach to base64-url-encoding
*
* Callbacks take an "in nsresult rv" argument that indicates whether the async
* operation succeeded. On success, rv will be a success code

Просмотреть файл

@ -8,6 +8,7 @@
#include "nsIInputStream.h"
#include "nsString.h"
#include "nsTArray.h"
#include "plbase64.h"
@ -228,6 +229,36 @@ EncodeInputStream(nsIInputStream* aInputStream,
static const char kBase64URLAlphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
// Maps an encoded character to a value in the Base64 URL alphabet, per
// RFC 4648, Table 2. Invalid input characters map to UINT8_MAX.
static const uint8_t kBase64URLDecodeTable[] = {
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255,
62 /* - */,
255, 255,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, /* 0 - 9 */
255, 255, 255, 255, 255, 255, 255,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, /* A - Z */
255, 255, 255, 255,
63 /* _ */,
255,
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
42, 43, 44, 45, 46, 47, 48, 49, 50, 51, /* a - z */
255, 255, 255, 255,
};
bool
Base64URLCharToValue(char aChar, uint8_t* aValue) {
uint8_t index = static_cast<uint8_t>(aChar);
*aValue = kBase64URLDecodeTable[index & 0x7f];
return (*aValue != 255) && !(index & ~0x7f);
}
} // namespace
namespace mozilla {
@ -359,7 +390,105 @@ Base64Decode(const nsAString& aBinaryData, nsAString& aString)
}
nsresult
Base64URLEncode(uint32_t aLength, const uint8_t* aData, nsACString& aString)
Base64URLDecode(const nsACString& aString,
const dom::Base64URLDecodeOptions& aOptions,
FallibleTArray<uint8_t>& aOutput)
{
// Don't decode empty strings.
if (aString.IsEmpty()) {
aOutput.Clear();
return NS_OK;
}
// Check for overflow.
uint32_t sourceLength = aString.Length();
if (sourceLength > UINT32_MAX / 3) {
return NS_ERROR_FAILURE;
}
const char* source = aString.BeginReading();
// The decoded length may be 1-2 bytes over, depending on the final quantum.
uint32_t decodedLength = (sourceLength * 3) / 4;
// Determine whether to check for and ignore trailing padding.
bool maybePadded = false;
switch (aOptions.mPadding) {
case dom::Base64URLDecodePadding::Require:
if (sourceLength % 4) {
// Padded input length must be a multiple of 4.
return NS_ERROR_INVALID_ARG;
}
maybePadded = true;
break;
case dom::Base64URLDecodePadding::Ignore:
// Check for padding only if the length is a multiple of 4.
maybePadded = !(sourceLength % 4);
break;
// If we're expecting unpadded input, no need for additional checks.
// `=` isn't in the decode table, so padded strings will fail to decode.
default:
MOZ_FALLTHROUGH_ASSERT("Invalid decode padding option");
case dom::Base64URLDecodePadding::Reject:
break;
}
if (maybePadded && source[sourceLength - 1] == '=') {
if (source[sourceLength - 2] == '=') {
sourceLength -= 2;
} else {
sourceLength -= 1;
}
}
if (NS_WARN_IF(!aOutput.SetCapacity(decodedLength, mozilla::fallible))) {
return NS_ERROR_OUT_OF_MEMORY;
}
aOutput.SetLengthAndRetainStorage(decodedLength);
uint8_t* output = aOutput.Elements();
for (; sourceLength >= 4; sourceLength -= 4) {
uint8_t w, x, y, z;
if (!Base64URLCharToValue(*source++, &w) ||
!Base64URLCharToValue(*source++, &x) ||
!Base64URLCharToValue(*source++, &y) ||
!Base64URLCharToValue(*source++, &z)) {
return NS_ERROR_INVALID_ARG;
}
*output++ = w << 2 | x >> 4;
*output++ = x << 4 | y >> 2;
*output++ = y << 6 | z;
}
if (sourceLength == 3) {
uint8_t w, x, y;
if (!Base64URLCharToValue(*source++, &w) ||
!Base64URLCharToValue(*source++, &x) ||
!Base64URLCharToValue(*source++, &y)) {
return NS_ERROR_INVALID_ARG;
}
*output++ = w << 2 | x >> 4;
*output++ = x << 4 | y >> 2;
} else if (sourceLength == 2) {
uint8_t w, x;
if (!Base64URLCharToValue(*source++, &w) ||
!Base64URLCharToValue(*source++, &x)) {
return NS_ERROR_INVALID_ARG;
}
*output++ = w << 2 | x >> 4;
} else if (sourceLength) {
return NS_ERROR_INVALID_ARG;
}
// Set the length to the actual number of decoded bytes.
aOutput.TruncateLength(output - aOutput.Elements());
return NS_OK;
}
nsresult
Base64URLEncode(uint32_t aLength, const uint8_t* aData,
const dom::Base64URLEncodeOptions& aOptions,
nsACString& aString)
{
// Don't encode empty strings.
if (aLength == 0) {
@ -368,11 +497,14 @@ Base64URLEncode(uint32_t aLength, const uint8_t* aData, nsACString& aString)
}
// Check for overflow.
if ((static_cast<uint64_t>(aLength) * 6 + 7) / 8 > UINT32_MAX) {
if (aLength > (UINT32_MAX / 4) * 3) {
return NS_ERROR_FAILURE;
}
if (!aString.SetLength((aLength * 8 + 5) / 6, fallible)) {
// Allocate a buffer large enough to hold the encoded string with padding.
// Add one byte for null termination.
uint32_t encodedLength = ((aLength + 2) / 3) * 4;
if (NS_WARN_IF(!aString.SetCapacity(encodedLength + 1, fallible))) {
aString.Truncate();
return NS_ERROR_FAILURE;
}
@ -400,6 +532,22 @@ Base64URLEncode(uint32_t aLength, const uint8_t* aData, nsACString& aString)
*rawBuffer++ = kBase64URLAlphabet[((aData[index + 1] & 0xf) << 2)];
}
uint32_t length = rawBuffer - aString.BeginWriting();
if (aOptions.mPad) {
if (length % 4 == 2) {
*rawBuffer++ = '=';
*rawBuffer++ = '=';
length += 2;
} else if (length % 4 == 3) {
*rawBuffer++ = '=';
length += 1;
}
}
// Null terminate and truncate to the actual number of characters.
*rawBuffer = '\0';
aString.SetLength(length);
return NS_OK;
}

Просмотреть файл

@ -9,6 +9,8 @@
#include "nsString.h"
#include "mozilla/dom/ThreadSafeChromeUtilsBinding.h"
class nsIInputStream;
namespace mozilla {
@ -36,11 +38,21 @@ Base64Decode(const nsAString& aBinaryData, nsAString& aString);
/**
* Converts |aData| to an unpadded, Base64 URL-encoded string per RFC 4648.
* Aims to encode the data in constant time. The caller may free |aData| once
* this function returns.
* Aims to encode the data in constant time. The caller retains ownership
* of |aData|.
*/
nsresult
Base64URLEncode(uint32_t aLength, const uint8_t* aData, nsACString& aString);
Base64URLEncode(uint32_t aLength, const uint8_t* aData,
const dom::Base64URLEncodeOptions& aOptions,
nsACString& aString);
/**
* Decodes a Base64 URL-encoded |aString| into |aOutput|.
*/
nsresult
Base64URLDecode(const nsACString& aString,
const dom::Base64URLDecodeOptions& aOptions,
FallibleTArray<uint8_t>& aOutput);
} // namespace mozilla