Skip to content

Commit fc41b4c

Browse files
authored
Migrate Base64 away from Msebera (in Android), Fixes AB#3077784 (#2389)
[AB#3077784](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3077784) (Part 2) ## Background Our entire code base is written to be compatible with Android.util.Base64, which is a fork of a legacy version of apache commons-codec. This means Android OS ships with apache commons-codec, and we cannot have that as a dependency in common4j project (otherwise the classloader would throw an error). The safest, previous workaround is to consume [msebera HttpClient](https://github.com/smarek/httpclient-android), which also contains commons-codec, but under a different namespace. [(This is officially suggested by Apache) ](https://hc.apache.org/httpcomponents-client-4.5.x/android.html) ## Issue msebera library is no longer maintained, and it was recently flagged with an apk tool. Even if the flagged part is NOT the code path we use. It's better that we migrate away from it (so that our customer don't see the false alarm) There are multiple ways to tackle this 1. (Least risky) Make the android project consume Android.util.base64, keep msebera for Linux (which is already in maintainance mode) and test classes 2. Migrate to other libraries. (e.g. guava) - NOTE: Java's Base64 is not an option because it requires min API 26 I decide to go with 1 because - the other option might increase the final apk/aar size by a lot. - It would also introduce a breaking change (e.g. The value in storage - before upgrade - was encoded with msebera, and might not be compatible with the new library. - The new library might also handle error cases/unexpected format differently. ## Changes All the Base64 usage is now consolidated into the Base64Util class. (most of the changes are under this bucket). This Base64Util class will use a classloader to pick the implementation to use. - In Android modules, it would load `AndroidBase64` - In Linux Broker, it would load `MseberaBase64ForLinux` - In Common4j's own unit test, it would load `MseberaBase64ForCommon4jTests` (from common4j's testImplementation) - In Broker4j's own unit test, it would load `MseberaBase64ForBroker4jTests` (from common4j's testImplementation) - In other tests, it would load `MseberaBase64ForTestUtils` (from :testutils)
1 parent 1441232 commit fc41b4c

File tree

31 files changed

+560
-148
lines changed

31 files changed

+560
-148
lines changed

changelog.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
vNext
2-
---------
2+
----------
3+
- [MINOR] Migrate Base64 away from Msebera httpclient (#2389)
34
- [PATCH] Provide empty image when no video is present on QR +PIN auth (#2424)
45
- [MINOR] On GetPreferredAuthMethod return NONE when Authenticator is not installed (#2523)
56
- [MINOR] Migrate URIbuilder to httpcore5 (#2522)

common/build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ dependencies {
156156
implementation(group: 'com.microsoft.device.display', name: 'display-mask', version: '0.3.0')
157157

158158
implementation 'org.apache.httpcomponents.core5:httpcore5:5.3'
159-
implementation "cz.msebera.android:httpclient:$rootProject.ext.mseberaApacheHttpClientVersion"
160159
implementation "com.nimbusds:nimbus-jose-jwt:$rootProject.ext.nimbusVersion"
161160
implementation "androidx.appcompat:appcompat:$rootProject.ext.appCompatVersion"
162161
implementation "com.google.code.gson:gson:$rootProject.ext.gsonVersion"

common/src/main/java/com/microsoft/identity/common/adal/internal/util/StringExtensions.java

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import android.util.Base64;
2727
import android.util.Log;
2828

29+
import com.microsoft.identity.common.java.base64.Base64Flags;
30+
import com.microsoft.identity.common.java.base64.Base64Util;
2931
import com.microsoft.identity.common.java.exception.ErrorStrings;
3032
import com.microsoft.identity.common.logging.Logger;
3133

@@ -112,20 +114,6 @@ public static String urlFormDecode(String source) throws UnsupportedEncodingExce
112114
return URLDecoder.decode(source, ENCODING_UTF8);
113115
}
114116

115-
/**
116-
* Encode Base64 URL Safe String.
117-
*
118-
* @param bytes byte[]
119-
* @return String
120-
* @throws UnsupportedEncodingException throws if encoding not supported.
121-
*/
122-
public static String encodeBase64URLSafeString(final byte[] bytes)
123-
throws UnsupportedEncodingException {
124-
return new String(
125-
Base64.encode(bytes, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE),
126-
ENCODING_UTF8);
127-
}
128-
129117
/**
130118
* create url from given endpoint. return null if format is not right.
131119
*
@@ -240,16 +228,6 @@ public static boolean hasPrefixInHeader(final String value, final String prefix)
240228
&& Character.isWhitespace(value.charAt(prefix.length()));
241229
}
242230

243-
/**
244-
* Based64URL encode the input string.
245-
*
246-
* @param message String
247-
* @return String
248-
*/
249-
public static String base64UrlEncodeToString(final String message) {
250-
return Base64.encodeToString(message.getBytes(Charset.forName(ENCODING_UTF8)), Base64.URL_SAFE | Base64.NO_WRAP);
251-
}
252-
253231
/**
254232
* Append parameter to the url. If the no query parameters, return the url originally passed in.
255233
*/
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.base64
24+
25+
import com.microsoft.identity.common.java.base64.Base64Flags
26+
import com.microsoft.identity.common.java.base64.Base64Util
27+
import com.microsoft.identity.common.java.base64.IBase64
28+
29+
/**
30+
* Base64 implementation based on android.util.Base64
31+
*
32+
* If you need to rename or change the namespace of this class,
33+
* you'll need to make change in [Base64Util] too.
34+
*
35+
* see [Base64Util] for more info.
36+
**/
37+
38+
class AndroidBase64 : IBase64 {
39+
40+
override fun encode(input: ByteArray, vararg flags: Base64Flags): ByteArray {
41+
return android.util.Base64.encode(input, combineFlags(*flags))
42+
}
43+
44+
override fun decode(input: ByteArray, vararg flags: Base64Flags): ByteArray {
45+
return android.util.Base64.decode(input, combineFlags(*flags))
46+
}
47+
48+
private fun combineFlags(vararg flags: Base64Flags): Int {
49+
var combinedFlag = android.util.Base64.DEFAULT
50+
51+
if (flags.contains(Base64Flags.URL_SAFE)) {
52+
combinedFlag = combinedFlag or android.util.Base64.URL_SAFE
53+
}
54+
if (flags.contains(Base64Flags.NO_WRAP)) {
55+
combinedFlag = combinedFlag or android.util.Base64.NO_WRAP
56+
}
57+
if (flags.contains(Base64Flags.NO_PADDING)) {
58+
combinedFlag = combinedFlag or android.util.Base64.NO_PADDING
59+
}
60+
61+
return combinedFlag
62+
}
63+
}

common/src/main/java/com/microsoft/identity/common/internal/cache/SharedPreferencesFileManager.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
import android.content.Context;
2626
import android.content.SharedPreferences;
27-
import android.os.Build;
2827
import android.util.LruCache;
2928

3029
import androidx.annotation.GuardedBy;

common/src/test/java/com/microsoft/identity/common/MicrosoftStsAccountCredentialAdapterTest.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
// THE SOFTWARE.
2323
package com.microsoft.identity.common;
2424

25-
import com.microsoft.identity.common.java.AuthenticationConstants;
2625
import com.microsoft.identity.common.java.authorities.Authority;
26+
import com.microsoft.identity.common.java.base64.Base64Flags;
27+
import com.microsoft.identity.common.java.base64.Base64Util;
2728
import com.microsoft.identity.common.java.cache.MicrosoftStsAccountCredentialAdapter;
2829
import com.microsoft.identity.common.java.commands.parameters.TokenCommandParameters;
2930
import com.microsoft.identity.common.java.dto.CredentialType;
@@ -49,21 +50,14 @@
4950

5051
import org.junit.Before;
5152
import org.junit.Test;
52-
import org.junit.runner.RunWith;
5353
import org.mockito.Mockito;
54-
import org.powermock.core.classloader.annotations.PowerMockIgnore;
55-
import org.powermock.modules.junit4.PowerMockRunner;
5654

5755
import java.net.MalformedURLException;
5856
import java.net.URL;
5957
import java.security.SecureRandom;
6058
import java.util.Calendar;
6159
import java.util.Date;
6260
import java.util.GregorianCalendar;
63-
import java.util.HashSet;
64-
import java.util.Set;
65-
66-
import cz.msebera.android.httpclient.extras.Base64;
6761

6862
import static com.microsoft.identity.common.java.providers.microsoft.MicrosoftAccount.AUTHORITY_TYPE_MS_STS;
6963
import static org.junit.Assert.assertEquals;
@@ -292,6 +286,6 @@ public void testCreateIdTokenRecord() {
292286

293287
static String createRawClientInfo(final String uid, final String utid) {
294288
final String claims = "{\"uid\":\"" + uid + "\",\"utid\":\"" + utid + "\"}";
295-
return Base64.encodeToString(StringUtil.toByteArray(claims), Base64.URL_SAFE);
289+
return Base64Util.encodeToString(StringUtil.toByteArray(claims), Base64Flags.URL_SAFE);
296290
}
297291
}

common4j/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,10 @@ dependencies {
218218
implementation "com.google.code.gson:gson:$rootProject.ext.gsonVersion"
219219
implementation "org.json:json:$rootProject.ext.jsonVersion"
220220
implementation "com.github.stephenc.jcip:jcip-annotations:$rootProject.ext.jcipAnnotationVersion"
221-
implementation "cz.msebera.android:httpclient:$rootProject.ext.mseberaApacheHttpClientVersion"
222221
implementation 'org.apache.httpcomponents.core5:httpcore5:5.3'
223222

223+
testImplementation "cz.msebera.android:httpclient:4.5.8"
224+
224225
testCompileOnly "com.github.spotbugs:spotbugs-annotations:$rootProject.ext.spotBugsAnnotationVersion"
225226
testCompileOnly "org.projectlombok:lombok:$rootProject.ext.lombokVersion"
226227
testAnnotationProcessor "org.projectlombok:lombok:$rootProject.ext.lombokVersion"

common4j/src/main/com/microsoft/identity/common/java/authorities/AuthorityDeserializer.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,12 @@
2929
import com.google.gson.JsonParseException;
3030
import com.microsoft.identity.common.java.logging.Logger;
3131
import com.microsoft.identity.common.java.util.CommonURIBuilder;
32+
import com.microsoft.identity.common.java.util.StringUtil;
3233

3334
import net.jcip.annotations.Immutable;
3435

3536
import java.lang.reflect.Type;
3637
import java.net.URI;
37-
import java.util.List;
38-
39-
import cz.msebera.android.httpclient.util.TextUtils;
4038

4139
@Immutable
4240
public class AuthorityDeserializer implements JsonDeserializer<Authority> {
@@ -65,7 +63,7 @@ public Authority deserialize(JsonElement json, Type typeOfT, JsonDeserialization
6563
final CommonURIBuilder uri = new CommonURIBuilder(URI.create(aadAuthority.mAuthorityUrlString));
6664
final String cloudUrl = uri.getScheme() + "://" + uri.getHost();
6765
final String tenant = uri.getLastPathSegment();
68-
if (!TextUtils.isEmpty(tenant)) {
66+
if (!StringUtil.isNullOrEmpty(tenant)) {
6967
aadAuthority.mAudience = AzureActiveDirectoryAudience.getAzureActiveDirectoryAudience(cloudUrl, tenant);
7068
}
7169
} catch (final IllegalArgumentException e) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.java.base64
24+
25+
/**
26+
* Flags for Base64 encoding/decoding options.
27+
**/
28+
enum class Base64Flags {
29+
30+
/**
31+
* Default values for encoder/decoder flags.
32+
*/
33+
DEFAULT,
34+
35+
/**
36+
* Encoder flag bit to omit the padding '=' characters at the end
37+
* of the output (if any).
38+
*/
39+
NO_PADDING,
40+
41+
/**
42+
* Encoder flag bit to omit all line terminators (i.e., the output
43+
* will be on one long line).
44+
*/
45+
NO_WRAP,
46+
47+
/**
48+
* Encoder/decoder flag bit to indicate using the "URL and
49+
* filename safe" variant of Base64 (see RFC 3548 section 4) where
50+
* `-` and `_` are used in place of `+` and
51+
* `/`.
52+
*/
53+
URL_SAFE
54+
}

0 commit comments

Comments
 (0)