Open yugui opened 4 months ago
This is the minimal reproducible example:
Encoder.cs
:
using System;
using System.Text;
using System.Text.RegularExpressions;
class Encoder {
static void Main(string[] args) {
// c.f. https://github.com/bitwarden/mobile/blob/8a43bb465515d3a775e60eb083cf88ef0e3e70c8/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs#L343
// c.f. https://github.com/bitwarden/mobile/blob/8a43bb465515d3a775e60eb083cf88ef0e3e70c8/src/Core/Resources/Localization/AppResources.ja.resx#L2128
var message = "WebAuthn の認証";
var json = $"{{\"btnText\": \"{message}\"}}";
// c.f. https://github.com/bitwarden/mobile/blob/main/src/Core/Utilities/AppHelpers.cs#L523
string EncodeMultibyte(Match match)
{
return Convert.ToChar(Convert.ToUInt32($"0x{match.Groups[1].Value}", 16)).ToString();
}
var escaped = Uri.EscapeDataString(json);
Console.WriteLine(escaped);
var multiByteEscaped = Regex.Replace(escaped, "%([0-9A-F]{2})", EncodeMultibyte);
var data = Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped));
Console.WriteLine(data);
}
}
Decoder.js
:
// c.f. https://github.com/bitwarden/clients/blob/48de33fc7a360e0a23df3351b3ad63d1c5579aac/apps/web/src/connectors/common.ts#L18
function b64Decode(str) {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), (c) => {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join(""),
);
}
// c.f. https://github.com/bitwarden/clients/blob/48de33fc7a360e0a23df3351b3ad63d1c5579aac/apps/web/src/connectors/webauthn.ts#L88
function decode(data) {
const dataObj = JSON.parse(b64Decode(data));
console.log(dataObj.btnText);
}
decode(process.argv[2]);
The next command would generate the same mojibake text as the one that WebAuth Mobile Connector displayed:
$ node Decoder.js $(mono Encoder.exe)
WebAuthn ã®èªè
The next version of the Encoder
would fix the issue, and make the message consumer correctly recover the original message
value.
using System;
using System.Text;
using System.Text.RegularExpressions;
class Encoder {
static void Main(string[] args) {
// c.f. https://github.com/bitwarden/mobile/blob/8a43bb465515d3a775e60eb083cf88ef0e3e70c8/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs#L343
// c.f. https://github.com/bitwarden/mobile/blob/8a43bb465515d3a775e60eb083cf88ef0e3e70c8/src/Core/Resources/Localization/AppResources.ja.resx#L2128
var message = "WebAuthn の認証";
var json = $"{{\"btnText\": \"{message}\"}}";
var data = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
Console.WriteLine(data);
}
}
$ node Decoder.js $(mono Encoder.exe)
WebAuthn の認証
Let's take a look at the first multibyte character in the original message, "の" (U+306E), for example.
"
(double quotation) or \
(backslash).Uri.EscapeDataString
internally calls System.UriHelper.EscapeStringToBuilder
and it encodes the character into its UTF-8 byte sequence representation, and then individual bytes in the sequence are encoded into the form of "%xx".
0xE3, 0x81, 0xAE
in UTF-8Regex.Replace
replaces the individual occurrences of "%xx" form with EncodeMultibyte
Convert.ToUInt32
in EncodeMultibyte
converts the regex captures of "E3", "81", and "AE" into 0xE3u
, 0x81u
, and 0xAEu
, respectivelyConvert.ToChar
converts individual integer values into their corresponding unicode code points.
0xE3u
, 0x81u
, and 0xAEu
are converted into the following characters, respectively:
ã
(U+00E3, LATIN SMALL LETTER A WITH TILDE)®
(U+00AE, REGISTERED SIGN)Encoding.UTF8.GetBytes
returns the UTF-8 byte sequence representation of the given string.
ã
, ®
are converted into their corresponding byte sequences as the followings:
ã
(U+00E3) --> 0xC3, 0xA3®
(U+00AE) --> 0xC2", 0xAEConvert.ToBase64String
converts the 6 bytes into "w6PCgcKu".The consumer-side implementation reverses just the step (5), (4), and (1). Therefore, we get ã®
(U+00E3, U+0031, U+00AE) instead of "の" (U306E).
If I understand correctly, the intention of this conversion in AppHelpers::EncodeDataParameter
is to keep the multibyte characters url-safe.
Therefore, just WebUtility.UrlEncode
would be sufficient. It internally converts the given string into its UTF-8 byte sequence representation, and individual bytes into a url-safe characters.
In practice, Base64 encoding is also necessary for consistency with the consumer-side.
In any way, the earlier steps of (2), and (3) are not necessary at all. Both of WebUtility.UrlEncode
and Convert.ToBase64String(Encoding.UTF8.GetBytes(str))
are safe to multibyte characters, and they are what the consumer-side implementation reverses.
Hi there,
This issue has been escalated for further investigation. If you have more information that can help us, please add it below.
Thanks!
Thank you for your response, @daniellbw.
Just in case, I have already prepared a fix for this issue in #3346
Production Build
Steps To Reproduce
Expected Result
The Web Authn mobile connector has a blue button whose display text is "Web Authn の認証".
The display text should be consistent with the string "Fido2AuthenticateWebAuthn" in the locale file
src/Core/Resources/Localization/AppResources.ja.resx
.Actual Result
The Web Authn mobile connector has a blue button whose display text is "WebAuthn ã®èªè¨¼".
The actual byte sequence of the display text is:
Screenshots or Videos
Expected
Screenshot of the expected result (the display text of the button was modified by the inspection tool in Chrome):
Actual
Screenshot of the actual result:
Ref
FYI: Screenshot of the equivalent page in the Bitwarden Chrome Extension (
webauthn-fallback-connector.html
).This is consistent with the expected result.
Additional Context
I believe you can reproduce an equivalent result by inserting a unicode emoji into the
Fido2AuthenticateWebAuthn
entry in the message localization file in an arbitrary locale.If I understand correctly, the root cause of the issue is that the escape of multibyte characters is inconsistently implemented to the unescaping in the webauthn connector, and the mobile application passes a wrong
data
query parameter to https://vault.bitwarden.com/webauthn-mobile-connector.html.data
query parameter by first decoding it as BASE64 and then parsing it as a JSON object:TwoFactorPageViewModel.cs
encodes thedata
parameter with an inconsistent way:Fido2AuthenticateAsync
AppHelpers.EncodeDataParameter
Bit.App.Pages.TwoFactorPageViewModel.Fido2AuthenticateAsync
method generates thedata
parameter by passing the values toBit.App.Utilities.AppHelpers.EncodeDataParameter
.EncodeDataParameter
first encodes the given object into JSON (JsonConvert.SerializeObject)
), and then escape it with URL encoding (Uri.EscapeDataString
)Convert.ToChar(Convert.ToUInt32($"0x{match.Groups[1].Value}", 16)).ToString()
, and then the generated single byte string into a byte sequence withEncoding.UTF8.GetBytes
.Operating System
Android
Operating System Version
14
Device
Pixel6
Build Version
2024.6.0 (10746)