feat: updated engine version to 4.4-rc1
This commit is contained in:
parent
ee00efde1f
commit
21ba8e33af
5459 changed files with 1128836 additions and 198305 deletions
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
from misc.utility.scons_hints import *
|
||||
|
||||
from glob import glob
|
||||
from pathlib import Path
|
||||
|
|
@ -15,12 +16,12 @@ def export_icon_builder(target, source, env):
|
|||
src_path = Path(str(source[0]))
|
||||
src_name = src_path.stem
|
||||
platform = src_path.parent.parent.stem
|
||||
with open(str(source[0]), "rb") as file:
|
||||
svg = "".join([f"\\{hex(x)[1:]}" for x in file.read()])
|
||||
with open(str(source[0]), "r") as file:
|
||||
svg = file.read()
|
||||
with methods.generated_wrapper(target, prefix=platform) as file:
|
||||
file.write(
|
||||
f"""\
|
||||
static const char *_{platform}_{src_name}_svg = "{svg}";
|
||||
static const char *_{platform}_{src_name}_svg = {methods.to_raw_cstring(svg)};
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ register_platform_apis = env.CommandNoCache(
|
|||
)
|
||||
env.add_source_files(env.platform_sources, register_platform_apis)
|
||||
for platform in env.platform_apis:
|
||||
env.add_source_files(env.platform_sources, f"{platform}/api/api.cpp")
|
||||
env.add_source_files(env.platform_sources, f"{platform}/api/*.cpp")
|
||||
|
||||
lib = env.add_library("platform", env.platform_sources)
|
||||
env.Prepend(LIBS=[lib])
|
||||
|
|
|
|||
2
engine/platform/android/.editorconfig
Normal file
2
engine/platform/android/.editorconfig
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[{*.gradle,AndroidManifest.xml}]
|
||||
indent_style = space
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
from misc.utility.scons_hints import *
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
|
@ -27,6 +28,9 @@ android_files = [
|
|||
"display_server_android.cpp",
|
||||
"plugin/godot_plugin_jni.cpp",
|
||||
"rendering_context_driver_vulkan_android.cpp",
|
||||
"variant/callable_jni.cpp",
|
||||
"dialog_utils_jni.cpp",
|
||||
"game_menu_utils_jni.cpp",
|
||||
]
|
||||
|
||||
env_android = env.Clone()
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod
|
|||
|
||||
_set_key_modifier_state(ev, keycode);
|
||||
|
||||
if (p_physical_keycode == AKEYCODE_BACK) {
|
||||
if (p_physical_keycode == AKEYCODE_BACK && p_pressed) {
|
||||
if (DisplayServerAndroid *dsa = Object::cast_to<DisplayServerAndroid>(DisplayServer::get_singleton())) {
|
||||
dsa->send_window_event(DisplayServer::WINDOW_EVENT_GO_BACK_REQUEST, true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,14 +41,13 @@ static JavaClassWrapper *java_class_wrapper = nullptr;
|
|||
|
||||
void register_android_api() {
|
||||
#if !defined(ANDROID_ENABLED)
|
||||
// On Android platforms, the `java_class_wrapper` instantiation and the
|
||||
// `JNISingleton` registration occurs in
|
||||
// On Android platforms, the `java_class_wrapper` instantiation occurs in
|
||||
// `platform/android/java_godot_lib_jni.cpp#Java_org_godotengine_godot_GodotLib_setup`
|
||||
java_class_wrapper = memnew(JavaClassWrapper); // Dummy
|
||||
GDREGISTER_CLASS(JNISingleton);
|
||||
java_class_wrapper = memnew(JavaClassWrapper);
|
||||
#endif
|
||||
|
||||
GDREGISTER_CLASS(JNISingleton);
|
||||
GDREGISTER_CLASS(JavaClass);
|
||||
GDREGISTER_CLASS(JavaObject);
|
||||
GDREGISTER_CLASS(JavaClassWrapper);
|
||||
Engine::get_singleton()->add_singleton(Engine::Singleton("JavaClassWrapper", JavaClassWrapper::get_singleton()));
|
||||
}
|
||||
|
|
@ -59,26 +58,59 @@ void unregister_android_api() {
|
|||
#endif
|
||||
}
|
||||
|
||||
void JavaClass::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("get_java_class_name"), &JavaClass::get_java_class_name);
|
||||
ClassDB::bind_method(D_METHOD("get_java_method_list"), &JavaClass::get_java_method_list);
|
||||
ClassDB::bind_method(D_METHOD("get_java_parent_class"), &JavaClass::get_java_parent_class);
|
||||
}
|
||||
|
||||
void JavaObject::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("get_java_class"), &JavaObject::get_java_class);
|
||||
}
|
||||
|
||||
void JavaClassWrapper::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("wrap", "name"), &JavaClassWrapper::wrap);
|
||||
ClassDB::bind_method(D_METHOD("get_exception"), &JavaClassWrapper::get_exception);
|
||||
}
|
||||
|
||||
#if !defined(ANDROID_ENABLED)
|
||||
bool JavaClass::_get(const StringName &p_name, Variant &r_ret) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
Variant JavaClass::callp(const StringName &, const Variant **, int, Callable::CallError &) {
|
||||
return Variant();
|
||||
}
|
||||
|
||||
String JavaClass::get_java_class_name() const {
|
||||
return "";
|
||||
}
|
||||
|
||||
TypedArray<Dictionary> JavaClass::get_java_method_list() const {
|
||||
return TypedArray<Dictionary>();
|
||||
}
|
||||
|
||||
Ref<JavaClass> JavaClass::get_java_parent_class() const {
|
||||
return Ref<JavaClass>();
|
||||
}
|
||||
|
||||
JavaClass::JavaClass() {
|
||||
}
|
||||
|
||||
JavaClass::~JavaClass() {
|
||||
}
|
||||
|
||||
Variant JavaObject::callp(const StringName &, const Variant **, int, Callable::CallError &) {
|
||||
return Variant();
|
||||
}
|
||||
|
||||
Ref<JavaClass> JavaObject::get_java_class() const {
|
||||
return Ref<JavaClass>();
|
||||
}
|
||||
|
||||
JavaClassWrapper *JavaClassWrapper::singleton = nullptr;
|
||||
|
||||
Ref<JavaClass> JavaClassWrapper::wrap(const String &) {
|
||||
Ref<JavaClass> JavaClassWrapper::_wrap(const String &, bool) {
|
||||
return Ref<JavaClass>();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
#define JAVA_CLASS_WRAPPER_H
|
||||
|
||||
#include "core/object/ref_counted.h"
|
||||
#include "core/variant/typed_array.h"
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
#include <android/log.h>
|
||||
|
|
@ -46,7 +47,7 @@ class JavaClass : public RefCounted {
|
|||
GDCLASS(JavaClass, RefCounted);
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
enum ArgumentType{
|
||||
enum ArgumentType {
|
||||
ARG_TYPE_VOID,
|
||||
ARG_TYPE_BOOLEAN,
|
||||
ARG_TYPE_BYTE,
|
||||
|
|
@ -57,6 +58,8 @@ class JavaClass : public RefCounted {
|
|||
ARG_TYPE_FLOAT,
|
||||
ARG_TYPE_DOUBLE,
|
||||
ARG_TYPE_STRING, //special case
|
||||
ARG_TYPE_CHARSEQUENCE,
|
||||
ARG_TYPE_CALLABLE,
|
||||
ARG_TYPE_CLASS,
|
||||
ARG_ARRAY_BIT = 1 << 16,
|
||||
ARG_NUMBER_CLASS_BIT = 1 << 17,
|
||||
|
|
@ -67,6 +70,7 @@ class JavaClass : public RefCounted {
|
|||
|
||||
struct MethodInfo {
|
||||
bool _static = false;
|
||||
bool _constructor = false;
|
||||
Vector<uint32_t> param_types;
|
||||
Vector<StringName> param_sigs;
|
||||
uint32_t return_type = 0;
|
||||
|
|
@ -121,8 +125,12 @@ class JavaClass : public RefCounted {
|
|||
likelihood = 0.5;
|
||||
break;
|
||||
case ARG_TYPE_STRING:
|
||||
case ARG_TYPE_CHARSEQUENCE:
|
||||
r_type = Variant::STRING;
|
||||
break;
|
||||
case ARG_TYPE_CALLABLE:
|
||||
r_type = Variant::CALLABLE;
|
||||
break;
|
||||
case ARG_TYPE_CLASS:
|
||||
r_type = Variant::OBJECT;
|
||||
break;
|
||||
|
|
@ -161,9 +169,11 @@ class JavaClass : public RefCounted {
|
|||
likelihood = 0.5;
|
||||
break;
|
||||
case ARG_ARRAY_BIT | ARG_TYPE_STRING:
|
||||
case ARG_ARRAY_BIT | ARG_TYPE_CHARSEQUENCE:
|
||||
r_type = Variant::PACKED_STRING_ARRAY;
|
||||
break;
|
||||
case ARG_ARRAY_BIT | ARG_TYPE_CLASS:
|
||||
case ARG_ARRAY_BIT | ARG_TYPE_CALLABLE:
|
||||
r_type = Variant::ARRAY;
|
||||
break;
|
||||
}
|
||||
|
|
@ -174,14 +184,30 @@ class JavaClass : public RefCounted {
|
|||
bool _call_method(JavaObject *p_instance, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error, Variant &ret);
|
||||
|
||||
friend class JavaClassWrapper;
|
||||
friend class JavaObject;
|
||||
String java_class_name;
|
||||
String java_constructor_name;
|
||||
HashMap<StringName, List<MethodInfo>> methods;
|
||||
jclass _class;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
bool _get(const StringName &p_name, Variant &r_ret) const;
|
||||
|
||||
public:
|
||||
virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override;
|
||||
|
||||
String get_java_class_name() const;
|
||||
TypedArray<Dictionary> get_java_method_list() const;
|
||||
Ref<JavaClass> get_java_parent_class() const;
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
virtual String to_string() override;
|
||||
#endif
|
||||
|
||||
JavaClass();
|
||||
~JavaClass();
|
||||
};
|
||||
|
||||
class JavaObject : public RefCounted {
|
||||
|
|
@ -191,14 +217,24 @@ class JavaObject : public RefCounted {
|
|||
Ref<JavaClass> base_class;
|
||||
friend class JavaClass;
|
||||
|
||||
jobject instance;
|
||||
jobject instance = nullptr;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
public:
|
||||
virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override;
|
||||
|
||||
Ref<JavaClass> get_java_class() const;
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
JavaObject(const Ref<JavaClass> &p_base, jobject *p_instance);
|
||||
virtual String to_string() override;
|
||||
|
||||
jobject get_instance() { return instance; }
|
||||
|
||||
JavaObject();
|
||||
JavaObject(const Ref<JavaClass> &p_base, jobject p_instance);
|
||||
~JavaObject();
|
||||
#endif
|
||||
};
|
||||
|
|
@ -209,13 +245,17 @@ class JavaClassWrapper : public Object {
|
|||
#ifdef ANDROID_ENABLED
|
||||
RBMap<String, Ref<JavaClass>> class_cache;
|
||||
friend class JavaClass;
|
||||
jmethodID getDeclaredMethods;
|
||||
jmethodID getFields;
|
||||
jmethodID getParameterTypes;
|
||||
jmethodID getReturnType;
|
||||
jmethodID getModifiers;
|
||||
jmethodID getName;
|
||||
jmethodID Class_getDeclaredConstructors;
|
||||
jmethodID Class_getDeclaredMethods;
|
||||
jmethodID Class_getFields;
|
||||
jmethodID Class_getName;
|
||||
jmethodID Class_getSuperclass;
|
||||
jmethodID Constructor_getParameterTypes;
|
||||
jmethodID Constructor_getModifiers;
|
||||
jmethodID Method_getParameterTypes;
|
||||
jmethodID Method_getReturnType;
|
||||
jmethodID Method_getModifiers;
|
||||
jmethodID Method_getName;
|
||||
jmethodID Field_getName;
|
||||
jmethodID Field_getModifiers;
|
||||
jmethodID Field_get;
|
||||
|
|
@ -231,6 +271,10 @@ class JavaClassWrapper : public Object {
|
|||
bool _get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, String &strsig);
|
||||
#endif
|
||||
|
||||
Ref<JavaObject> exception;
|
||||
|
||||
Ref<JavaClass> _wrap(const String &p_class, bool p_allow_private_methods_access);
|
||||
|
||||
static JavaClassWrapper *singleton;
|
||||
|
||||
protected:
|
||||
|
|
@ -239,13 +283,18 @@ protected:
|
|||
public:
|
||||
static JavaClassWrapper *get_singleton() { return singleton; }
|
||||
|
||||
Ref<JavaClass> wrap(const String &p_class);
|
||||
Ref<JavaClass> wrap(const String &p_class) {
|
||||
return _wrap(p_class, false);
|
||||
}
|
||||
|
||||
Ref<JavaObject> get_exception() {
|
||||
return exception;
|
||||
}
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
JavaClassWrapper(jobject p_activity = nullptr);
|
||||
#else
|
||||
JavaClassWrapper();
|
||||
Ref<JavaClass> wrap_jclass(jclass p_class, bool p_allow_private_methods_access = false);
|
||||
#endif
|
||||
JavaClassWrapper();
|
||||
};
|
||||
|
||||
#endif // JAVA_CLASS_WRAPPER_H
|
||||
|
|
|
|||
|
|
@ -31,188 +31,53 @@
|
|||
#ifndef JNI_SINGLETON_H
|
||||
#define JNI_SINGLETON_H
|
||||
|
||||
#include "java_class_wrapper.h"
|
||||
|
||||
#include "core/config/engine.h"
|
||||
#include "core/variant/variant.h"
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
#include "jni_utils.h"
|
||||
#endif
|
||||
|
||||
class JNISingleton : public Object {
|
||||
GDCLASS(JNISingleton, Object);
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
struct MethodData {
|
||||
jmethodID method;
|
||||
Variant::Type ret_type;
|
||||
Vector<Variant::Type> argtypes;
|
||||
};
|
||||
|
||||
jobject instance;
|
||||
RBMap<StringName, MethodData> method_map;
|
||||
#endif
|
||||
Ref<JavaObject> wrapped_object;
|
||||
|
||||
public:
|
||||
virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override {
|
||||
#ifdef ANDROID_ENABLED
|
||||
RBMap<StringName, MethodData>::Element *E = method_map.find(p_method);
|
||||
if (wrapped_object.is_valid()) {
|
||||
RBMap<StringName, MethodData>::Element *E = method_map.find(p_method);
|
||||
|
||||
// Check the method we're looking for is in the JNISingleton map and that
|
||||
// the arguments match.
|
||||
bool call_error = !E || E->get().argtypes.size() != p_argcount;
|
||||
if (!call_error) {
|
||||
for (int i = 0; i < p_argcount; i++) {
|
||||
if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) {
|
||||
call_error = true;
|
||||
break;
|
||||
// Check the method we're looking for is in the JNISingleton map and that
|
||||
// the arguments match.
|
||||
bool call_error = !E || E->get().argtypes.size() != p_argcount;
|
||||
if (!call_error) {
|
||||
for (int i = 0; i < p_argcount; i++) {
|
||||
if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) {
|
||||
call_error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (call_error) {
|
||||
// The method is not in this map, defaulting to the regular instance calls.
|
||||
return Object::callp(p_method, p_args, p_argcount, r_error);
|
||||
}
|
||||
|
||||
ERR_FAIL_NULL_V(instance, Variant());
|
||||
|
||||
r_error.error = Callable::CallError::CALL_OK;
|
||||
|
||||
jvalue *v = nullptr;
|
||||
|
||||
if (p_argcount) {
|
||||
v = (jvalue *)alloca(sizeof(jvalue) * p_argcount);
|
||||
}
|
||||
|
||||
JNIEnv *env = get_jni_env();
|
||||
|
||||
int res = env->PushLocalFrame(16);
|
||||
|
||||
ERR_FAIL_COND_V(res != 0, Variant());
|
||||
|
||||
List<jobject> to_erase;
|
||||
for (int i = 0; i < p_argcount; i++) {
|
||||
jvalret vr = _variant_to_jvalue(env, E->get().argtypes[i], p_args[i]);
|
||||
v[i] = vr.val;
|
||||
if (vr.obj) {
|
||||
to_erase.push_back(vr.obj);
|
||||
if (!call_error) {
|
||||
return wrapped_object->callp(p_method, p_args, p_argcount, r_error);
|
||||
}
|
||||
}
|
||||
|
||||
Variant ret;
|
||||
|
||||
switch (E->get().ret_type) {
|
||||
case Variant::NIL: {
|
||||
env->CallVoidMethodA(instance, E->get().method, v);
|
||||
} break;
|
||||
case Variant::BOOL: {
|
||||
ret = env->CallBooleanMethodA(instance, E->get().method, v) == JNI_TRUE;
|
||||
} break;
|
||||
case Variant::INT: {
|
||||
ret = env->CallIntMethodA(instance, E->get().method, v);
|
||||
} break;
|
||||
case Variant::FLOAT: {
|
||||
ret = env->CallFloatMethodA(instance, E->get().method, v);
|
||||
} break;
|
||||
case Variant::STRING: {
|
||||
jobject o = env->CallObjectMethodA(instance, E->get().method, v);
|
||||
ret = jstring_to_string((jstring)o, env);
|
||||
env->DeleteLocalRef(o);
|
||||
} break;
|
||||
case Variant::PACKED_STRING_ARRAY: {
|
||||
jobjectArray arr = (jobjectArray)env->CallObjectMethodA(instance, E->get().method, v);
|
||||
|
||||
ret = _jobject_to_variant(env, arr);
|
||||
|
||||
env->DeleteLocalRef(arr);
|
||||
} break;
|
||||
case Variant::PACKED_INT32_ARRAY: {
|
||||
jintArray arr = (jintArray)env->CallObjectMethodA(instance, E->get().method, v);
|
||||
|
||||
int fCount = env->GetArrayLength(arr);
|
||||
Vector<int> sarr;
|
||||
sarr.resize(fCount);
|
||||
|
||||
int *w = sarr.ptrw();
|
||||
env->GetIntArrayRegion(arr, 0, fCount, w);
|
||||
ret = sarr;
|
||||
env->DeleteLocalRef(arr);
|
||||
} break;
|
||||
case Variant::PACKED_INT64_ARRAY: {
|
||||
jlongArray arr = (jlongArray)env->CallObjectMethodA(instance, E->get().method, v);
|
||||
|
||||
int fCount = env->GetArrayLength(arr);
|
||||
Vector<int64_t> sarr;
|
||||
sarr.resize(fCount);
|
||||
|
||||
int64_t *w = sarr.ptrw();
|
||||
env->GetLongArrayRegion(arr, 0, fCount, w);
|
||||
ret = sarr;
|
||||
env->DeleteLocalRef(arr);
|
||||
} break;
|
||||
case Variant::PACKED_FLOAT32_ARRAY: {
|
||||
jfloatArray arr = (jfloatArray)env->CallObjectMethodA(instance, E->get().method, v);
|
||||
|
||||
int fCount = env->GetArrayLength(arr);
|
||||
Vector<float> sarr;
|
||||
sarr.resize(fCount);
|
||||
|
||||
float *w = sarr.ptrw();
|
||||
env->GetFloatArrayRegion(arr, 0, fCount, w);
|
||||
ret = sarr;
|
||||
env->DeleteLocalRef(arr);
|
||||
} break;
|
||||
case Variant::PACKED_FLOAT64_ARRAY: {
|
||||
jdoubleArray arr = (jdoubleArray)env->CallObjectMethodA(instance, E->get().method, v);
|
||||
|
||||
int fCount = env->GetArrayLength(arr);
|
||||
Vector<double> sarr;
|
||||
sarr.resize(fCount);
|
||||
|
||||
double *w = sarr.ptrw();
|
||||
env->GetDoubleArrayRegion(arr, 0, fCount, w);
|
||||
ret = sarr;
|
||||
env->DeleteLocalRef(arr);
|
||||
} break;
|
||||
case Variant::DICTIONARY: {
|
||||
jobject obj = env->CallObjectMethodA(instance, E->get().method, v);
|
||||
ret = _jobject_to_variant(env, obj);
|
||||
env->DeleteLocalRef(obj);
|
||||
|
||||
} break;
|
||||
default: {
|
||||
env->PopLocalFrame(nullptr);
|
||||
ERR_FAIL_V(Variant());
|
||||
} break;
|
||||
}
|
||||
|
||||
while (to_erase.size()) {
|
||||
env->DeleteLocalRef(to_erase.front()->get());
|
||||
to_erase.pop_front();
|
||||
}
|
||||
|
||||
env->PopLocalFrame(nullptr);
|
||||
|
||||
return ret;
|
||||
#else // ANDROID_ENABLED
|
||||
|
||||
// Defaulting to the regular instance calls.
|
||||
return Object::callp(p_method, p_args, p_argcount, r_error);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
jobject get_instance() const {
|
||||
return instance;
|
||||
Ref<JavaObject> get_wrapped_object() const {
|
||||
return wrapped_object;
|
||||
}
|
||||
|
||||
void set_instance(jobject p_instance) {
|
||||
instance = p_instance;
|
||||
}
|
||||
|
||||
void add_method(const StringName &p_name, jmethodID p_method, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) {
|
||||
void add_method(const StringName &p_name, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) {
|
||||
MethodData md;
|
||||
md.method = p_method;
|
||||
md.argtypes = p_args;
|
||||
md.ret_type = p_ret_type;
|
||||
method_map[p_name] = md;
|
||||
|
|
@ -227,24 +92,15 @@ public:
|
|||
ADD_SIGNAL(mi);
|
||||
}
|
||||
|
||||
#endif
|
||||
JNISingleton() {}
|
||||
|
||||
JNISingleton() {
|
||||
#ifdef ANDROID_ENABLED
|
||||
instance = nullptr;
|
||||
#endif
|
||||
JNISingleton(const Ref<JavaObject> &p_wrapped_object) {
|
||||
wrapped_object = p_wrapped_object;
|
||||
}
|
||||
|
||||
~JNISingleton() {
|
||||
#ifdef ANDROID_ENABLED
|
||||
method_map.clear();
|
||||
if (instance) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL(env);
|
||||
|
||||
env->DeleteGlobalRef(instance);
|
||||
}
|
||||
#endif
|
||||
wrapped_object.unref();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import sys
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from methods import print_error, print_warning
|
||||
from platform_methods import validate_arch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from SCons.Script.SConscript import SConsEnvironment
|
||||
|
|
@ -18,6 +19,10 @@ def can_build():
|
|||
return os.path.exists(get_env_android_sdk_root())
|
||||
|
||||
|
||||
def get_tools(env: "SConsEnvironment"):
|
||||
return ["clang", "clang++", "as", "ar", "link"]
|
||||
|
||||
|
||||
def get_opts():
|
||||
from SCons.Variables import BoolVariable
|
||||
|
||||
|
|
@ -30,6 +35,7 @@ def get_opts():
|
|||
),
|
||||
BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False),
|
||||
BoolVariable("generate_apk", "Generate an APK/AAB after building Android library by calling Gradle", False),
|
||||
BoolVariable("swappy", "Use Swappy Frame Pacing library", False),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -95,15 +101,19 @@ def install_ndk_if_needed(env: "SConsEnvironment"):
|
|||
env["ANDROID_NDK_ROOT"] = get_android_ndk_root(env)
|
||||
|
||||
|
||||
def detect_swappy():
|
||||
archs = ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"]
|
||||
has_swappy = True
|
||||
for arch in archs:
|
||||
if not os.path.isfile(f"thirdparty/swappy-frame-pacing/{arch}/libswappy_static.a"):
|
||||
has_swappy = False
|
||||
return has_swappy
|
||||
|
||||
|
||||
def configure(env: "SConsEnvironment"):
|
||||
# Validate arch.
|
||||
supported_arches = ["x86_32", "x86_64", "arm32", "arm64"]
|
||||
if env["arch"] not in supported_arches:
|
||||
print_error(
|
||||
'Unsupported CPU architecture "%s" for Android. Supported architectures are: %s.'
|
||||
% (env["arch"], ", ".join(supported_arches))
|
||||
)
|
||||
sys.exit(255)
|
||||
validate_arch(env["arch"], get_name(), supported_arches)
|
||||
|
||||
if get_min_sdk_version(env["ndk_platform"]) < get_min_target_api():
|
||||
print_warning(
|
||||
|
|
@ -171,31 +181,52 @@ def configure(env: "SConsEnvironment"):
|
|||
env["AS"] = compiler_path + "/clang"
|
||||
|
||||
env.Append(
|
||||
CCFLAGS=(
|
||||
"-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split()
|
||||
)
|
||||
CCFLAGS=(["-fpic", "-ffunction-sections", "-funwind-tables", "-fstack-protector-strong", "-fvisibility=hidden"])
|
||||
)
|
||||
|
||||
has_swappy = detect_swappy()
|
||||
if not has_swappy:
|
||||
print_warning(
|
||||
"Swappy Frame Pacing not detected! It is strongly recommended you download it from https://github.com/darksylinc/godot-swappy/releases and extract it so that the following files can be found:\n"
|
||||
+ " thirdparty/swappy-frame-pacing/arm64-v8a/libswappy_static.a\n"
|
||||
+ " thirdparty/swappy-frame-pacing/armeabi-v7a/libswappy_static.a\n"
|
||||
+ " thirdparty/swappy-frame-pacing/x86/libswappy_static.a\n"
|
||||
+ " thirdparty/swappy-frame-pacing/x86_64/libswappy_static.a\n"
|
||||
+ "Without Swappy, Godot apps on Android will inevitable suffer stutter and struggle to keep consistent 30/60/90/120 fps. Though Swappy cannot guarantee your app will be stutter-free, not having Swappy will guarantee there will be stutter even on the best phones and the most simple of scenes."
|
||||
)
|
||||
if env["swappy"]:
|
||||
print_error("Use build option `swappy=no` to ignore missing Swappy dependency and build without it.")
|
||||
sys.exit(255)
|
||||
|
||||
if get_min_sdk_version(env["ndk_platform"]) >= 24:
|
||||
env.Append(CPPDEFINES=[("_FILE_OFFSET_BITS", 64)])
|
||||
|
||||
if env["arch"] == "x86_32":
|
||||
# The NDK adds this if targeting API < 24, so we can drop it when Godot targets it at least
|
||||
env.Append(CCFLAGS=["-mstackrealign"])
|
||||
if has_swappy:
|
||||
env.Append(LIBPATH=["#thirdparty/swappy-frame-pacing/x86"])
|
||||
elif env["arch"] == "x86_64":
|
||||
if has_swappy:
|
||||
env.Append(LIBPATH=["#thirdparty/swappy-frame-pacing/x86_64"])
|
||||
elif env["arch"] == "arm32":
|
||||
env.Append(CCFLAGS="-march=armv7-a -mfloat-abi=softfp".split())
|
||||
env.Append(CCFLAGS=["-march=armv7-a", "-mfloat-abi=softfp"])
|
||||
env.Append(CPPDEFINES=["__ARM_ARCH_7__", "__ARM_ARCH_7A__"])
|
||||
env.Append(CPPDEFINES=["__ARM_NEON__"])
|
||||
if has_swappy:
|
||||
env.Append(LIBPATH=["#thirdparty/swappy-frame-pacing/armeabi-v7a"])
|
||||
elif env["arch"] == "arm64":
|
||||
env.Append(CCFLAGS=["-mfix-cortex-a53-835769"])
|
||||
env.Append(CPPDEFINES=["__ARM_ARCH_8A__"])
|
||||
if has_swappy:
|
||||
env.Append(LIBPATH=["#thirdparty/swappy-frame-pacing/arm64-v8a"])
|
||||
|
||||
env.Append(CCFLAGS=["-ffp-contract=off"])
|
||||
|
||||
# Link flags
|
||||
|
||||
env.Append(LINKFLAGS="-Wl,--gc-sections -Wl,--no-undefined -Wl,-z,now".split())
|
||||
env.Append(LINKFLAGS="-Wl,-soname,libgodot_android.so")
|
||||
env.Append(LINKFLAGS=["-Wl,--gc-sections", "-Wl,--no-undefined", "-Wl,-z,now"])
|
||||
env.Append(LINKFLAGS=["-Wl,-soname,libgodot_android.so"])
|
||||
|
||||
env.Prepend(CPPPATH=["#platform/android"])
|
||||
env.Append(CPPDEFINES=["ANDROID_ENABLED", "UNIX_ENABLED"])
|
||||
|
|
@ -203,6 +234,9 @@ def configure(env: "SConsEnvironment"):
|
|||
|
||||
if env["vulkan"]:
|
||||
env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"])
|
||||
if has_swappy:
|
||||
env.Append(CPPDEFINES=["SWAPPY_FRAME_PACING_ENABLED"])
|
||||
env.Append(LIBS=["swappy_static"])
|
||||
if not env["use_volk"]:
|
||||
env.Append(LIBS=["vulkan"])
|
||||
|
||||
|
|
|
|||
52
engine/platform/android/dialog_utils_jni.cpp
Normal file
52
engine/platform/android/dialog_utils_jni.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**************************************************************************/
|
||||
/* dialog_utils_jni.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "dialog_utils_jni.h"
|
||||
|
||||
#include "display_server_android.h"
|
||||
#include "jni_utils.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_DialogUtils_dialogCallback(JNIEnv *env, jclass clazz, jint p_button_index) {
|
||||
DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
|
||||
if (ds) {
|
||||
ds->emit_dialog_callback(p_button_index);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_DialogUtils_inputDialogCallback(JNIEnv *env, jclass clazz, jstring p_text) {
|
||||
DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
|
||||
if (ds) {
|
||||
String text = jstring_to_string(p_text, env);
|
||||
ds->emit_input_dialog_callback(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**************************************************************************/
|
||||
/* string_android.h */
|
||||
/* dialog_utils_jni.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
|
|
@ -28,34 +28,14 @@
|
|||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#ifndef STRING_ANDROID_H
|
||||
#define STRING_ANDROID_H
|
||||
|
||||
#include "thread_jandroid.h"
|
||||
|
||||
#include "core/string/ustring.h"
|
||||
#ifndef DIALOG_UTILS_JNI_H
|
||||
#define DIALOG_UTILS_JNI_H
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
/**
|
||||
* Converts JNI jstring to Godot String.
|
||||
* @param source Source JNI string. If null an empty string is returned.
|
||||
* @param env JNI environment instance. If null obtained by get_jni_env().
|
||||
* @return Godot string instance.
|
||||
*/
|
||||
static inline String jstring_to_string(jstring source, JNIEnv *env = nullptr) {
|
||||
String result;
|
||||
if (source) {
|
||||
if (!env) {
|
||||
env = get_jni_env();
|
||||
}
|
||||
const char *const source_utf8 = env->GetStringUTFChars(source, nullptr);
|
||||
if (source_utf8) {
|
||||
result.parse_utf8(source_utf8);
|
||||
env->ReleaseStringUTFChars(source, source_utf8);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
extern "C" {
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_DialogUtils_dialogCallback(JNIEnv *env, jclass clazz, jint p_button_index);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_DialogUtils_inputDialogCallback(JNIEnv *env, jclass clazz, jstring p_text);
|
||||
}
|
||||
|
||||
#endif // STRING_ANDROID_H
|
||||
#endif // DIALOG_UTILS_JNI_H
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
#include "dir_access_jandroid.h"
|
||||
|
||||
#include "string_android.h"
|
||||
#include "jni_utils.h"
|
||||
#include "thread_jandroid.h"
|
||||
|
||||
#include "core/string/print_string.h"
|
||||
|
|
@ -68,7 +68,7 @@ String DirAccessJAndroid::get_next() {
|
|||
if (_dir_next) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL_V(env, "");
|
||||
jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id);
|
||||
jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, id);
|
||||
if (!str) {
|
||||
return "";
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ bool DirAccessJAndroid::current_is_dir() const {
|
|||
if (_dir_is_dir) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL_V(env, false);
|
||||
return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id);
|
||||
return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ bool DirAccessJAndroid::current_is_hidden() const {
|
|||
if (_current_is_hidden) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL_V(env, false);
|
||||
return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id);
|
||||
return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -218,7 +218,7 @@ bool DirAccessJAndroid::dir_exists(String p_dir) {
|
|||
}
|
||||
}
|
||||
|
||||
Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
|
||||
Error DirAccessJAndroid::make_dir(String p_dir) {
|
||||
// Check if the directory exists already
|
||||
if (dir_exists(p_dir)) {
|
||||
return ERR_ALREADY_EXISTS;
|
||||
|
|
@ -242,8 +242,12 @@ Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
|
|||
}
|
||||
}
|
||||
|
||||
Error DirAccessJAndroid::make_dir(String p_dir) {
|
||||
return make_dir_recursive(p_dir);
|
||||
Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
|
||||
Error err = make_dir(p_dir);
|
||||
if (err != OK && err != ERR_ALREADY_EXISTS) {
|
||||
ERR_FAIL_V_MSG(err, "Could not create directory: " + p_dir);
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error DirAccessJAndroid::rename(String p_from, String p_to) {
|
||||
|
|
@ -307,9 +311,9 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
|
|||
cls = (jclass)env->NewGlobalRef(c);
|
||||
|
||||
_dir_open = env->GetMethodID(cls, "dirOpen", "(ILjava/lang/String;)I");
|
||||
_dir_next = env->GetMethodID(cls, "dirNext", "(II)Ljava/lang/String;");
|
||||
_dir_close = env->GetMethodID(cls, "dirClose", "(II)V");
|
||||
_dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(II)Z");
|
||||
_dir_next = env->GetMethodID(cls, "dirNext", "(I)Ljava/lang/String;");
|
||||
_dir_close = env->GetMethodID(cls, "dirClose", "(I)V");
|
||||
_dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(I)Z");
|
||||
_dir_exists = env->GetMethodID(cls, "dirExists", "(ILjava/lang/String;)Z");
|
||||
_file_exists = env->GetMethodID(cls, "fileExists", "(ILjava/lang/String;)Z");
|
||||
_get_drive_count = env->GetMethodID(cls, "getDriveCount", "(I)I");
|
||||
|
|
@ -318,7 +322,7 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
|
|||
_get_space_left = env->GetMethodID(cls, "getSpaceLeft", "(I)J");
|
||||
_rename = env->GetMethodID(cls, "rename", "(ILjava/lang/String;Ljava/lang/String;)Z");
|
||||
_remove = env->GetMethodID(cls, "remove", "(ILjava/lang/String;)Z");
|
||||
_current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z");
|
||||
_current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(I)Z");
|
||||
}
|
||||
|
||||
void DirAccessJAndroid::terminate() {
|
||||
|
|
@ -355,6 +359,6 @@ void DirAccessJAndroid::dir_close(int p_id) {
|
|||
if (_dir_close) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL(env);
|
||||
env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id);
|
||||
env->CallVoidMethod(dir_access_handler, _dir_close, p_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ public:
|
|||
|
||||
virtual bool is_link(String p_file) override { return false; }
|
||||
virtual String read_link(String p_file) override { return p_file; }
|
||||
virtual Error create_link(String p_source, String p_target) override { return FAILED; }
|
||||
virtual Error create_link(String p_source, String p_target) override { return ERR_UNAVAILABLE; }
|
||||
|
||||
virtual uint64_t get_space_left() override;
|
||||
|
||||
|
|
|
|||
|
|
@ -70,9 +70,11 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const {
|
|||
//case FEATURE_IME:
|
||||
case FEATURE_MOUSE:
|
||||
//case FEATURE_MOUSE_WARP:
|
||||
//case FEATURE_NATIVE_DIALOG:
|
||||
//case FEATURE_NATIVE_DIALOG_INPUT:
|
||||
//case FEATURE_NATIVE_DIALOG_FILE:
|
||||
case FEATURE_NATIVE_DIALOG:
|
||||
case FEATURE_NATIVE_DIALOG_INPUT:
|
||||
case FEATURE_NATIVE_DIALOG_FILE:
|
||||
//case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
|
||||
case FEATURE_NATIVE_DIALOG_FILE_MIME:
|
||||
//case FEATURE_NATIVE_ICON:
|
||||
//case FEATURE_WINDOW_TRANSPARENCY:
|
||||
case FEATURE_CLIPBOARD:
|
||||
|
|
@ -176,6 +178,57 @@ bool DisplayServerAndroid::clipboard_has() const {
|
|||
}
|
||||
}
|
||||
|
||||
Error DisplayServerAndroid::dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) {
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL_V(godot_java, FAILED);
|
||||
dialog_callback = p_callback;
|
||||
return godot_java->show_dialog(p_title, p_description, p_buttons);
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::emit_dialog_callback(int p_button_index) {
|
||||
if (dialog_callback.is_valid()) {
|
||||
dialog_callback.call_deferred(p_button_index);
|
||||
}
|
||||
}
|
||||
|
||||
Error DisplayServerAndroid::dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) {
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL_V(godot_java, FAILED);
|
||||
input_dialog_callback = p_callback;
|
||||
return godot_java->show_input_dialog(p_title, p_description, p_partial);
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::emit_input_dialog_callback(String p_text) {
|
||||
if (input_dialog_callback.is_valid()) {
|
||||
input_dialog_callback.call_deferred(p_text);
|
||||
}
|
||||
}
|
||||
|
||||
Error DisplayServerAndroid::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL_V(godot_java, FAILED);
|
||||
file_picker_callback = p_callback;
|
||||
return godot_java->show_file_picker(p_current_directory, p_filename, p_mode, p_filters);
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::emit_file_picker_callback(bool p_ok, const Vector<String> &p_selected_paths) {
|
||||
if (file_picker_callback.is_valid()) {
|
||||
file_picker_callback.call_deferred(p_ok, p_selected_paths, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Color DisplayServerAndroid::get_accent_color() const {
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL_V(godot_java, Color(0, 0, 0, 0));
|
||||
return godot_java->get_accent_color();
|
||||
}
|
||||
|
||||
Color DisplayServerAndroid::get_base_color() const {
|
||||
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
|
||||
ERR_FAIL_NULL_V(godot_java, Color(0, 0, 0, 0));
|
||||
return godot_java->get_base_color();
|
||||
}
|
||||
|
||||
TypedArray<Rect2> DisplayServerAndroid::get_display_cutouts() const {
|
||||
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
|
||||
ERR_FAIL_NULL_V(godot_io_java, Array());
|
||||
|
|
@ -258,7 +311,6 @@ float DisplayServerAndroid::screen_get_scale(int p_screen) const {
|
|||
screen_scale = MIN(screen_scale, MIN(width_scale, height_scale));
|
||||
}
|
||||
|
||||
print_line("Selected screen scale: ", screen_scale);
|
||||
return screen_scale;
|
||||
}
|
||||
|
||||
|
|
@ -305,6 +357,13 @@ int DisplayServerAndroid::virtual_keyboard_get_height() const {
|
|||
return godot_io_java->get_vk_height();
|
||||
}
|
||||
|
||||
bool DisplayServerAndroid::has_hardware_keyboard() const {
|
||||
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
|
||||
ERR_FAIL_NULL_V(godot_io_java, false);
|
||||
|
||||
return godot_io_java->has_hardware_keyboard();
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::window_set_window_event_callback(const Callable &p_callable, DisplayServer::WindowID p_window) {
|
||||
window_event_callback = p_callable;
|
||||
}
|
||||
|
|
@ -383,6 +442,14 @@ int64_t DisplayServerAndroid::window_get_native_handle(HandleType p_handle_type,
|
|||
}
|
||||
return 0;
|
||||
}
|
||||
case EGL_DISPLAY: {
|
||||
// @todo Find a way to get this from the Java side.
|
||||
return 0;
|
||||
}
|
||||
case EGL_CONFIG: {
|
||||
// @todo Find a way to get this from the Java side.
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
default: {
|
||||
return 0;
|
||||
|
|
@ -455,11 +522,15 @@ Size2i DisplayServerAndroid::window_get_size_with_decorations(DisplayServer::Win
|
|||
}
|
||||
|
||||
void DisplayServerAndroid::window_set_mode(DisplayServer::WindowMode p_mode, DisplayServer::WindowID p_window) {
|
||||
// Not supported on Android.
|
||||
OS_Android::get_singleton()->get_godot_java()->enable_immersive_mode(p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN);
|
||||
}
|
||||
|
||||
DisplayServer::WindowMode DisplayServerAndroid::window_get_mode(DisplayServer::WindowID p_window) const {
|
||||
return WINDOW_MODE_FULLSCREEN;
|
||||
if (OS_Android::get_singleton()->get_godot_java()->is_in_immersive_mode()) {
|
||||
return WINDOW_MODE_FULLSCREEN;
|
||||
} else {
|
||||
return WINDOW_MODE_MAXIMIZED;
|
||||
}
|
||||
}
|
||||
|
||||
bool DisplayServerAndroid::window_is_maximize_allowed(DisplayServer::WindowID p_window) const {
|
||||
|
|
@ -511,8 +582,8 @@ Vector<String> DisplayServerAndroid::get_rendering_drivers_func() {
|
|||
return drivers;
|
||||
}
|
||||
|
||||
DisplayServer *DisplayServerAndroid::create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error) {
|
||||
DisplayServer *ds = memnew(DisplayServerAndroid(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_position, p_resolution, p_screen, p_context, r_error));
|
||||
DisplayServer *DisplayServerAndroid::create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error) {
|
||||
DisplayServer *ds = memnew(DisplayServerAndroid(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_position, p_resolution, p_screen, p_context, p_parent_window, r_error));
|
||||
if (r_error != OK) {
|
||||
if (p_rendering_driver == "vulkan") {
|
||||
OS::get_singleton()->alert(
|
||||
|
|
@ -579,19 +650,13 @@ void DisplayServerAndroid::notify_surface_changed(int p_width, int p_height) {
|
|||
}
|
||||
}
|
||||
|
||||
DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error) {
|
||||
DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error) {
|
||||
rendering_driver = p_rendering_driver;
|
||||
|
||||
keep_screen_on = GLOBAL_GET("display/window/energy_saving/keep_screen_on");
|
||||
|
||||
native_menu = memnew(NativeMenu);
|
||||
|
||||
#if defined(GLES3_ENABLED)
|
||||
if (rendering_driver == "opengl3") {
|
||||
RasterizerGLES3::make_current(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(RD_ENABLED)
|
||||
rendering_context = nullptr;
|
||||
rendering_device = nullptr;
|
||||
|
|
@ -604,13 +669,26 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
|
|||
|
||||
if (rendering_context) {
|
||||
if (rendering_context->initialize() != OK) {
|
||||
ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver));
|
||||
memdelete(rendering_context);
|
||||
rendering_context = nullptr;
|
||||
r_error = ERR_UNAVAILABLE;
|
||||
return;
|
||||
#if defined(GLES3_ENABLED)
|
||||
bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3");
|
||||
if (fallback_to_opengl3 && rendering_driver != "opengl3") {
|
||||
WARN_PRINT("Your device seem not to support Vulkan, switching to OpenGL 3.");
|
||||
rendering_driver = "opengl3";
|
||||
OS::get_singleton()->set_current_rendering_method("gl_compatibility");
|
||||
OS::get_singleton()->set_current_rendering_driver_name(rendering_driver);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver));
|
||||
r_error = ERR_UNAVAILABLE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rendering_context) {
|
||||
union {
|
||||
#ifdef VULKAN_ENABLED
|
||||
RenderingContextDriverVulkanAndroid::WindowPlatformData vulkan;
|
||||
|
|
@ -650,6 +728,12 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
|
|||
}
|
||||
#endif
|
||||
|
||||
#if defined(GLES3_ENABLED)
|
||||
if (rendering_driver == "opengl3") {
|
||||
RasterizerGLES3::make_current(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events);
|
||||
|
||||
r_error = OK;
|
||||
|
|
@ -687,33 +771,68 @@ void DisplayServerAndroid::process_gyroscope(const Vector3 &p_gyroscope) {
|
|||
Input::get_singleton()->set_gyroscope(p_gyroscope);
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::mouse_set_mode(MouseMode p_mode) {
|
||||
void DisplayServerAndroid::_mouse_update_mode() {
|
||||
MouseMode wanted_mouse_mode = mouse_mode_override_enabled
|
||||
? mouse_mode_override
|
||||
: mouse_mode_base;
|
||||
|
||||
if (!OS_Android::get_singleton()->get_godot_java()->get_godot_view()->can_update_pointer_icon() || !OS_Android::get_singleton()->get_godot_java()->get_godot_view()->can_capture_pointer()) {
|
||||
return;
|
||||
}
|
||||
if (mouse_mode == p_mode) {
|
||||
if (mouse_mode == wanted_mouse_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (p_mode == MouseMode::MOUSE_MODE_HIDDEN) {
|
||||
if (wanted_mouse_mode == MouseMode::MOUSE_MODE_HIDDEN) {
|
||||
OS_Android::get_singleton()->get_godot_java()->get_godot_view()->set_pointer_icon(CURSOR_TYPE_NULL);
|
||||
} else {
|
||||
cursor_set_shape(cursor_shape);
|
||||
}
|
||||
|
||||
if (p_mode == MouseMode::MOUSE_MODE_CAPTURED) {
|
||||
if (wanted_mouse_mode == MouseMode::MOUSE_MODE_CAPTURED) {
|
||||
OS_Android::get_singleton()->get_godot_java()->get_godot_view()->request_pointer_capture();
|
||||
} else {
|
||||
OS_Android::get_singleton()->get_godot_java()->get_godot_view()->release_pointer_capture();
|
||||
}
|
||||
|
||||
mouse_mode = p_mode;
|
||||
mouse_mode = wanted_mouse_mode;
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::mouse_set_mode(MouseMode p_mode) {
|
||||
ERR_FAIL_INDEX(p_mode, MouseMode::MOUSE_MODE_MAX);
|
||||
if (p_mode == mouse_mode_base) {
|
||||
return;
|
||||
}
|
||||
mouse_mode_base = p_mode;
|
||||
_mouse_update_mode();
|
||||
}
|
||||
|
||||
DisplayServer::MouseMode DisplayServerAndroid::mouse_get_mode() const {
|
||||
return mouse_mode;
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::mouse_set_mode_override(MouseMode p_mode) {
|
||||
ERR_FAIL_INDEX(p_mode, MouseMode::MOUSE_MODE_MAX);
|
||||
if (p_mode == mouse_mode_override) {
|
||||
return;
|
||||
}
|
||||
mouse_mode_override = p_mode;
|
||||
_mouse_update_mode();
|
||||
}
|
||||
|
||||
DisplayServer::MouseMode DisplayServerAndroid::mouse_get_mode_override() const {
|
||||
return mouse_mode_override;
|
||||
}
|
||||
|
||||
void DisplayServerAndroid::mouse_set_mode_override_enabled(bool p_override_enabled) {
|
||||
mouse_mode_override_enabled = p_override_enabled;
|
||||
_mouse_update_mode();
|
||||
}
|
||||
|
||||
bool DisplayServerAndroid::mouse_is_mode_override_enabled() const {
|
||||
return mouse_mode_override_enabled;
|
||||
}
|
||||
|
||||
Point2i DisplayServerAndroid::mouse_get_position() const {
|
||||
return Input::get_singleton()->get_mouse_position();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ class DisplayServerAndroid : public DisplayServer {
|
|||
};
|
||||
const int CURSOR_TYPE_NULL = 0;
|
||||
MouseMode mouse_mode = MouseMode::MOUSE_MODE_VISIBLE;
|
||||
MouseMode mouse_mode_base = MouseMode::MOUSE_MODE_VISIBLE;
|
||||
MouseMode mouse_mode_override = MouseMode::MOUSE_MODE_VISIBLE;
|
||||
bool mouse_mode_override_enabled = false;
|
||||
void _mouse_update_mode();
|
||||
|
||||
bool keep_screen_on;
|
||||
bool swap_buffers_flag;
|
||||
|
|
@ -87,6 +91,11 @@ class DisplayServerAndroid : public DisplayServer {
|
|||
|
||||
Callable system_theme_changed;
|
||||
|
||||
Callable dialog_callback;
|
||||
Callable input_dialog_callback;
|
||||
|
||||
Callable file_picker_callback;
|
||||
|
||||
void _window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred = false) const;
|
||||
|
||||
static void _dispatch_input_events(const Ref<InputEvent> &p_event);
|
||||
|
|
@ -116,6 +125,18 @@ public:
|
|||
virtual String clipboard_get() const override;
|
||||
virtual bool clipboard_has() const override;
|
||||
|
||||
virtual Error dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) override;
|
||||
void emit_dialog_callback(int p_button_index);
|
||||
|
||||
virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override;
|
||||
void emit_input_dialog_callback(String p_text);
|
||||
|
||||
virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, const FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;
|
||||
void emit_file_picker_callback(bool p_ok, const Vector<String> &p_selected_paths);
|
||||
|
||||
virtual Color get_accent_color() const override;
|
||||
virtual Color get_base_color() const override;
|
||||
|
||||
virtual TypedArray<Rect2> get_display_cutouts() const override;
|
||||
virtual Rect2i get_display_safe_area() const override;
|
||||
|
||||
|
|
@ -138,6 +159,7 @@ public:
|
|||
virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), VirtualKeyboardType p_type = KEYBOARD_TYPE_DEFAULT, int p_max_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override;
|
||||
virtual void virtual_keyboard_hide() override;
|
||||
virtual int virtual_keyboard_get_height() const override;
|
||||
virtual bool has_hardware_keyboard() const override;
|
||||
|
||||
virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
|
||||
virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
|
||||
|
|
@ -210,8 +232,12 @@ public:
|
|||
|
||||
virtual void mouse_set_mode(MouseMode p_mode) override;
|
||||
virtual MouseMode mouse_get_mode() const override;
|
||||
virtual void mouse_set_mode_override(MouseMode p_mode) override;
|
||||
virtual MouseMode mouse_get_mode_override() const override;
|
||||
virtual void mouse_set_mode_override_enabled(bool p_override_enabled) override;
|
||||
virtual bool mouse_is_mode_override_enabled() const override;
|
||||
|
||||
static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error);
|
||||
static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error);
|
||||
static Vector<String> get_rendering_drivers_func();
|
||||
static void register_android_driver();
|
||||
|
||||
|
|
@ -228,7 +254,7 @@ public:
|
|||
virtual void set_native_icon(const String &p_filename) override;
|
||||
virtual void set_icon(const Ref<Image> &p_icon) override;
|
||||
|
||||
DisplayServerAndroid(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error);
|
||||
DisplayServerAndroid(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error);
|
||||
~DisplayServerAndroid();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@
|
|||
Path to an APK file to use as a custom export template for release exports. If left empty, default template is used.
|
||||
[b]Note:[/b] This is only used if [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is disabled.
|
||||
</member>
|
||||
<member name="gesture/swipe_to_dismiss" type="bool" setter="" getter="">
|
||||
If [code]true[/code], [url=https://developer.android.com/design/ui/wear/guides/components/swipe-to-dismiss]Swipe to dismiss[/url] will be enabled.
|
||||
This functionality is intended for smartwatches and is generally ignored on standard Android devices. However, some devices may not ignore it. Therefore, it is recommended to keep this feature disabled for standard Android apps to avoid unexpected behavior.
|
||||
[b]Note:[/b] This is [code]false[/code] by default. To enable this behavior, [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is required.
|
||||
</member>
|
||||
<member name="gradle_build/android_source_template" type="String" setter="" getter="">
|
||||
Path to a ZIP file holding the source for the export template used in a Gradle build. If left empty, the default template is used.
|
||||
</member>
|
||||
|
|
@ -102,6 +107,9 @@
|
|||
<member name="launcher_icons/adaptive_foreground_432x432" type="String" setter="" getter="">
|
||||
Foreground layer of the application adaptive icon file. See [url=https://developer.android.com/develop/ui/views/launch/icon_design_adaptive#design-adaptive-icons]Design adaptive icons[/url].
|
||||
</member>
|
||||
<member name="launcher_icons/adaptive_monochrome_432x432" type="String" setter="" getter="">
|
||||
Monochrome layer of the application adaptive icon file. See [url=https://developer.android.com/develop/ui/views/launch/icon_design_adaptive#design-adaptive-icons]Design adaptive icons[/url].
|
||||
</member>
|
||||
<member name="launcher_icons/main_192x192" type="String" setter="" getter="">
|
||||
Application icon file. If left empty, it will fallback to [member ProjectSettings.application/config/icon].
|
||||
</member>
|
||||
|
|
@ -148,6 +156,9 @@
|
|||
<member name="permissions/access_location_extra_commands" type="bool" setter="" getter="">
|
||||
Allows access to the extra location provider commands. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_LOCATION_EXTRA_COMMANDS]ACCESS_LOCATION_EXTRA_COMMANDS[/url].
|
||||
</member>
|
||||
<member name="permissions/access_media_location" type="bool" setter="" getter="">
|
||||
Allows an application to access any geographic locations persisted in the user's shared collection. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_MEDIA_LOCATION]ACCESS_MEDIA_LOCATION[/url].
|
||||
</member>
|
||||
<member name="permissions/access_mock_location" type="bool" setter="" getter="">
|
||||
Allows an application to create mock location providers for testing.
|
||||
</member>
|
||||
|
|
@ -409,6 +420,18 @@
|
|||
<member name="permissions/read_logs" type="bool" setter="" getter="">
|
||||
Allows an application to read the low-level system log files. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_LOGS]READ_LOGS[/url].
|
||||
</member>
|
||||
<member name="permissions/read_media_audio" type="bool" setter="" getter="">
|
||||
Allows an application to read audio files from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_AUDIO]READ_MEDIA_AUDIO[/url].
|
||||
</member>
|
||||
<member name="permissions/read_media_images" type="bool" setter="" getter="">
|
||||
Allows an application to read image files from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_IMAGES]READ_MEDIA_IMAGES[/url].
|
||||
</member>
|
||||
<member name="permissions/read_media_video" type="bool" setter="" getter="">
|
||||
Allows an application to read video files from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_VIDEO]READ_MEDIA_VIDEO[/url].
|
||||
</member>
|
||||
<member name="permissions/read_media_visual_user_selected" type="bool" setter="" getter="">
|
||||
Allows an application to read image or video files from external storage that a user has selected via the permission prompt photo picker. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_VISUAL_USER_SELECTED]READ_MEDIA_VISUAL_USER_SELECTED[/url].
|
||||
</member>
|
||||
<member name="permissions/read_phone_state" type="bool" setter="" getter="">
|
||||
Allows read only access to phone state. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE]READ_PHONE_STATE[/url].
|
||||
</member>
|
||||
|
|
@ -574,7 +597,7 @@
|
|||
Allows an application to write to the user dictionary.
|
||||
</member>
|
||||
<member name="screen/immersive_mode" type="bool" setter="" getter="">
|
||||
If [code]true[/code], hides navigation and status bar.
|
||||
If [code]true[/code], hides navigation and status bar. See [method DisplayServer.window_set_mode] to toggle it at runtime.
|
||||
</member>
|
||||
<member name="screen/support_large" type="bool" setter="" getter="">
|
||||
Indicates whether the application supports larger screen form-factors.
|
||||
|
|
|
|||
|
|
@ -37,21 +37,29 @@
|
|||
#include "editor/editor_settings.h"
|
||||
#include "editor/export/editor_export.h"
|
||||
|
||||
String get_default_android_sdk_path();
|
||||
|
||||
void register_android_exporter_types() {
|
||||
GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformAndroid);
|
||||
}
|
||||
|
||||
void register_android_exporter() {
|
||||
#ifndef ANDROID_ENABLED
|
||||
EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
|
||||
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
|
||||
EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
|
||||
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
|
||||
EDITOR_DEF("export/android/debug_keystore", EditorPaths::get_singleton()->get_debug_keystore_path());
|
||||
// TODO: Move to editor_settings.cpp
|
||||
EDITOR_DEF_BASIC("export/android/debug_keystore", EditorPaths::get_singleton()->get_debug_keystore_path());
|
||||
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"));
|
||||
EDITOR_DEF("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
|
||||
EDITOR_DEF("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
|
||||
EDITOR_DEF_BASIC("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
|
||||
EDITOR_DEF_BASIC("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
|
||||
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore_pass", PROPERTY_HINT_PASSWORD));
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
EDITOR_DEF_BASIC("export/android/install_exported_apk", !OS::get_singleton()->has_feature("horizonos"));
|
||||
#else
|
||||
EDITOR_DEF_BASIC("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
|
||||
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
|
||||
|
||||
EDITOR_DEF_BASIC("export/android/android_sdk_path", OS::get_singleton()->has_environment("ANDROID_HOME") ? OS::get_singleton()->get_environment("ANDROID_HOME") : get_default_android_sdk_path());
|
||||
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
|
||||
|
||||
EDITOR_DEF("export/android/force_system_user", false);
|
||||
|
||||
EDITOR_DEF("export/android/shutdown_adb_on_exit", true);
|
||||
|
|
@ -65,3 +73,15 @@ void register_android_exporter() {
|
|||
Ref<EditorExportPlatformAndroid> exporter = Ref<EditorExportPlatformAndroid>(memnew(EditorExportPlatformAndroid));
|
||||
EditorExport::get_singleton()->add_export_platform(exporter);
|
||||
}
|
||||
|
||||
inline String get_default_android_sdk_path() {
|
||||
#ifdef WINDOWS_ENABLED
|
||||
return OS::get_singleton()->get_environment("LOCALAPPDATA").path_join("Android/Sdk");
|
||||
#elif LINUXBSD_ENABLED
|
||||
return OS::get_singleton()->get_environment("HOME").path_join("Android/Sdk");
|
||||
#elif MACOS_ENABLED
|
||||
return OS::get_singleton()->get_environment("HOME").path_join("Library/Android/sdk");
|
||||
#else
|
||||
return String();
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -35,6 +35,7 @@
|
|||
#include "godot_plugin_config.h"
|
||||
#endif // DISABLE_DEPRECATED
|
||||
|
||||
#include "core/io/image.h"
|
||||
#include "core/io/zip_io.h"
|
||||
#include "core/os/os.h"
|
||||
#include "editor/export/editor_export_platform.h"
|
||||
|
|
@ -141,9 +142,9 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
|
|||
|
||||
static Error save_apk_so(void *p_userdata, const SharedObject &p_so);
|
||||
|
||||
static Error save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
|
||||
static Error save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed);
|
||||
|
||||
static Error ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
|
||||
static Error ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed);
|
||||
|
||||
static Error copy_gradle_so(void *p_userdata, const SharedObject &p_so);
|
||||
|
||||
|
|
@ -155,6 +156,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
|
|||
|
||||
void _write_tmp_manifest(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, bool p_debug);
|
||||
|
||||
void _fix_themes_xml(const Ref<EditorExportPreset> &p_preset);
|
||||
|
||||
void _fix_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_manifest, bool p_give_internet);
|
||||
|
||||
static String _get_keystore_path(const Ref<EditorExportPreset> &p_preset, bool p_debug);
|
||||
|
|
@ -167,12 +170,13 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
|
|||
|
||||
void _process_launcher_icons(const String &p_file_name, const Ref<Image> &p_source_image, int dimension, Vector<uint8_t> &p_data);
|
||||
|
||||
void load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background);
|
||||
void load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background, Ref<Image> &monochrome);
|
||||
|
||||
void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,
|
||||
const Ref<Image> &p_main_image,
|
||||
const Ref<Image> &p_foreground,
|
||||
const Ref<Image> &p_background);
|
||||
const Ref<Image> &p_background,
|
||||
const Ref<Image> &p_monochrome);
|
||||
|
||||
static void _create_editor_debug_keystore_if_needed();
|
||||
|
||||
|
|
@ -184,7 +188,7 @@ protected:
|
|||
void _notification(int p_what);
|
||||
|
||||
public:
|
||||
typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
|
||||
typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed);
|
||||
|
||||
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
|
||||
|
||||
|
|
@ -214,7 +218,7 @@ public:
|
|||
|
||||
virtual String get_device_architecture(int p_index) const override;
|
||||
|
||||
virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override;
|
||||
virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) override;
|
||||
|
||||
virtual Ref<Texture2D> get_run_icon() const override;
|
||||
|
||||
|
|
@ -242,7 +246,7 @@ public:
|
|||
|
||||
Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path);
|
||||
|
||||
void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags);
|
||||
void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, Vector<uint8_t> &r_command_line_flags);
|
||||
|
||||
Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep);
|
||||
|
||||
|
|
@ -253,9 +257,9 @@ public:
|
|||
static String join_list(const List<String> &p_parts, const String &p_separator);
|
||||
static String join_abis(const Vector<ABI> &p_parts, const String &p_separator, bool p_use_arch);
|
||||
|
||||
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;
|
||||
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override;
|
||||
|
||||
Error export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, int p_flags);
|
||||
Error export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, BitField<EditorExportPlatform::DebugFlags> p_flags);
|
||||
|
||||
virtual void get_platform_features(List<String> *r_features) const override;
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ int _get_app_category_value(int category_index) {
|
|||
return 7;
|
||||
case APP_CATEGORY_SOCIAL:
|
||||
return 4;
|
||||
case APP_CATEGORY_UNDEFINED:
|
||||
return -1;
|
||||
case APP_CATEGORY_VIDEO:
|
||||
return 2;
|
||||
case APP_CATEGORY_GAME:
|
||||
|
|
@ -167,10 +169,11 @@ Error store_string_at_path(const String &p_path, const String &p_data) {
|
|||
// It is used by the export_project_files method to save all the asset files into the gradle project.
|
||||
// It's functionality mirrors that of the method save_apk_file.
|
||||
// This method will be called ONLY when gradle build is enabled.
|
||||
Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) {
|
||||
Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed) {
|
||||
CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata);
|
||||
String dst_path = p_path.replace_first("res://", export_data->assets_directory + "/");
|
||||
print_verbose("Saving project files from " + p_path + " into " + dst_path);
|
||||
const String path = ResourceUID::ensure_path(p_path);
|
||||
const String dst_path = path.replace_first("res://", export_data->assets_directory + "/");
|
||||
print_verbose("Saving project files from " + path + " into " + dst_path);
|
||||
Error err = store_file_at_path(dst_path, p_data);
|
||||
return err;
|
||||
}
|
||||
|
|
@ -194,7 +197,7 @@ String _android_xml_escape(const String &p_string) {
|
|||
Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name, const String &p_gradle_build_dir) {
|
||||
print_verbose("Creating strings resources for supported locales for project " + project_name);
|
||||
// Stores the string into the default values directory.
|
||||
String processed_default_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(project_name));
|
||||
String processed_default_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(project_name));
|
||||
store_string_at_path(p_gradle_build_dir.path_join("res/values/godot_project_name_string.xml"), processed_default_xml_string);
|
||||
|
||||
// Searches the Gradle project res/ directory to find all supported locales
|
||||
|
|
@ -220,7 +223,7 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset
|
|||
String locale_directory = p_gradle_build_dir.path_join("res/" + file + "/godot_project_name_string.xml");
|
||||
if (appnames.has(locale)) {
|
||||
String locale_project_name = appnames[locale];
|
||||
String processed_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(locale_project_name));
|
||||
String processed_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(locale_project_name));
|
||||
print_verbose("Storing project name for locale " + locale + " under " + locale_directory);
|
||||
store_string_at_path(locale_directory, processed_xml_string);
|
||||
} else {
|
||||
|
|
@ -257,7 +260,7 @@ String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) {
|
|||
String _get_activity_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug) {
|
||||
String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation"))));
|
||||
String manifest_activity_text = vformat(
|
||||
" <activity android:name=\"com.godot.game.GodotApp\" "
|
||||
" <activity android:name=\".GodotApp\" "
|
||||
"tools:replace=\"android:screenOrientation,android:excludeFromRecents,android:resizeableActivity\" "
|
||||
"tools:node=\"mergeOnlyAttributes\" "
|
||||
"android:excludeFromRecents=\"%s\" "
|
||||
|
|
@ -311,17 +314,21 @@ String _get_application_tag(const Ref<EditorExportPlatform> &p_export_platform,
|
|||
" <application android:label=\"@string/godot_project_name_string\"\n"
|
||||
" android:allowBackup=\"%s\"\n"
|
||||
" android:icon=\"@mipmap/icon\"\n"
|
||||
" android:appCategory=\"%s\"\n"
|
||||
" android:isGame=\"%s\"\n"
|
||||
" android:hasFragileUserData=\"%s\"\n"
|
||||
" android:requestLegacyExternalStorage=\"%s\"\n"
|
||||
" tools:replace=\"android:allowBackup,android:appCategory,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n"
|
||||
" tools:ignore=\"GoogleAppIndexingWarning\">\n\n",
|
||||
" android:requestLegacyExternalStorage=\"%s\"\n",
|
||||
bool_to_string(p_preset->get("user_data_backup/allow")),
|
||||
_get_app_category_label(app_category_index),
|
||||
bool_to_string(is_game),
|
||||
bool_to_string(p_preset->get("package/retain_data_on_uninstall")),
|
||||
bool_to_string(p_has_read_write_storage_permission));
|
||||
if (app_category_index != APP_CATEGORY_UNDEFINED) {
|
||||
manifest_application_text += vformat(" android:appCategory=\"%s\"\n", _get_app_category_label(app_category_index));
|
||||
manifest_application_text += " tools:replace=\"android:allowBackup,android:appCategory,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n";
|
||||
} else {
|
||||
manifest_application_text += " tools:remove=\"android:appCategory\"\n";
|
||||
manifest_application_text += " tools:replace=\"android:allowBackup,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n";
|
||||
}
|
||||
manifest_application_text += " tools:ignore=\"GoogleAppIndexingWarning\">\n\n";
|
||||
|
||||
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
|
||||
for (int i = 0; i < export_plugins.size(); i++) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
#include "core/os/os.h"
|
||||
#include "editor/export/editor_export.h"
|
||||
|
||||
const String godot_project_name_xml_string = R"(<?xml version="1.0" encoding="utf-8"?>
|
||||
const String GODOT_PROJECT_NAME_XML_STRING = R"(<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">%s</string>
|
||||
|
|
@ -55,6 +55,7 @@ static const int APP_CATEGORY_NEWS = 5;
|
|||
static const int APP_CATEGORY_PRODUCTIVITY = 6;
|
||||
static const int APP_CATEGORY_SOCIAL = 7;
|
||||
static const int APP_CATEGORY_VIDEO = 8;
|
||||
static const int APP_CATEGORY_UNDEFINED = 9;
|
||||
|
||||
// Supported XR modes.
|
||||
// This should match the entries in 'platform/android/java/lib/src/org/godotengine/godot/xr/XRMode.java'
|
||||
|
|
@ -92,7 +93,7 @@ Error store_string_at_path(const String &p_path, const String &p_data);
|
|||
// It is used by the export_project_files method to save all the asset files into the gradle project.
|
||||
// It's functionality mirrors that of the method save_apk_file.
|
||||
// This method will be called ONLY when gradle build is enabled.
|
||||
Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
|
||||
Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed);
|
||||
|
||||
// Creates strings.xml files inside the gradle project for different locales.
|
||||
Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name, const String &p_gradle_build_dir);
|
||||
|
|
|
|||
|
|
@ -113,87 +113,6 @@ bool FileAccessAndroid::eof_reached() const {
|
|||
return eof;
|
||||
}
|
||||
|
||||
uint8_t FileAccessAndroid::get_8() const {
|
||||
if (pos >= len) {
|
||||
eof = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t byte;
|
||||
AAsset_read(asset, &byte, 1);
|
||||
pos++;
|
||||
return byte;
|
||||
}
|
||||
|
||||
uint16_t FileAccessAndroid::get_16() const {
|
||||
if (pos >= len) {
|
||||
eof = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t bytes = 0;
|
||||
int r = AAsset_read(asset, &bytes, 2);
|
||||
|
||||
if (r >= 0) {
|
||||
pos += r;
|
||||
if (pos >= len) {
|
||||
eof = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (big_endian) {
|
||||
bytes = BSWAP16(bytes);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
uint32_t FileAccessAndroid::get_32() const {
|
||||
if (pos >= len) {
|
||||
eof = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t bytes = 0;
|
||||
int r = AAsset_read(asset, &bytes, 4);
|
||||
|
||||
if (r >= 0) {
|
||||
pos += r;
|
||||
if (pos >= len) {
|
||||
eof = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (big_endian) {
|
||||
bytes = BSWAP32(bytes);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
uint64_t FileAccessAndroid::get_64() const {
|
||||
if (pos >= len) {
|
||||
eof = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t bytes = 0;
|
||||
int r = AAsset_read(asset, &bytes, 8);
|
||||
|
||||
if (r >= 0) {
|
||||
pos += r;
|
||||
if (pos >= len) {
|
||||
eof = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (big_endian) {
|
||||
bytes = BSWAP64(bytes);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const {
|
||||
ERR_FAIL_COND_V(!p_dst && p_length > 0, -1);
|
||||
|
||||
|
|
@ -209,6 +128,7 @@ uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const
|
|||
pos = len;
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
|
|
@ -220,20 +140,8 @@ void FileAccessAndroid::flush() {
|
|||
ERR_FAIL();
|
||||
}
|
||||
|
||||
void FileAccessAndroid::store_8(uint8_t p_dest) {
|
||||
ERR_FAIL();
|
||||
}
|
||||
|
||||
void FileAccessAndroid::store_16(uint16_t p_dest) {
|
||||
ERR_FAIL();
|
||||
}
|
||||
|
||||
void FileAccessAndroid::store_32(uint32_t p_dest) {
|
||||
ERR_FAIL();
|
||||
}
|
||||
|
||||
void FileAccessAndroid::store_64(uint64_t p_dest) {
|
||||
ERR_FAIL();
|
||||
bool FileAccessAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) {
|
||||
ERR_FAIL_V(false);
|
||||
}
|
||||
|
||||
bool FileAccessAndroid::file_exists(const String &p_path) {
|
||||
|
|
|
|||
|
|
@ -68,25 +68,18 @@ public:
|
|||
virtual bool eof_reached() const override; // reading passed EOF
|
||||
|
||||
virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; }
|
||||
virtual uint8_t get_8() const override; // get a byte
|
||||
virtual uint16_t get_16() const override;
|
||||
virtual uint32_t get_32() const override;
|
||||
virtual uint64_t get_64() const override;
|
||||
virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override;
|
||||
|
||||
virtual Error get_error() const override; // get last error
|
||||
|
||||
virtual void flush() override;
|
||||
virtual void store_8(uint8_t p_dest) override; // store a byte
|
||||
virtual void store_16(uint16_t p_dest) override;
|
||||
virtual void store_32(uint32_t p_dest) override;
|
||||
virtual void store_64(uint64_t p_dest) override;
|
||||
virtual bool store_buffer(const uint8_t *p_src, uint64_t p_length) override;
|
||||
|
||||
virtual bool file_exists(const String &p_path) override; // return true if a file exists
|
||||
|
||||
virtual uint64_t _get_modified_time(const String &p_file) override { return 0; }
|
||||
virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; }
|
||||
virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; }
|
||||
virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return ERR_UNAVAILABLE; }
|
||||
|
||||
virtual bool _get_hidden_attribute(const String &p_file) override { return false; }
|
||||
virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; }
|
||||
|
|
|
|||
|
|
@ -77,15 +77,9 @@ Error FileAccessFilesystemJAndroid::open_internal(const String &p_path, int p_mo
|
|||
int res = env->CallIntMethod(file_access_handler, _file_open, js, p_mode_flags);
|
||||
env->DeleteLocalRef(js);
|
||||
|
||||
if (res <= 0) {
|
||||
switch (res) {
|
||||
case 0:
|
||||
default:
|
||||
return ERR_FILE_CANT_OPEN;
|
||||
|
||||
case -2:
|
||||
return ERR_FILE_NOT_FOUND;
|
||||
}
|
||||
if (res < 0) {
|
||||
// Errors are passed back as their negative value to differentiate from the positive file id.
|
||||
return static_cast<Error>(-res);
|
||||
}
|
||||
|
||||
id = res;
|
||||
|
|
@ -175,43 +169,6 @@ void FileAccessFilesystemJAndroid::_set_eof(bool eof) {
|
|||
}
|
||||
}
|
||||
|
||||
uint8_t FileAccessFilesystemJAndroid::get_8() const {
|
||||
ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
|
||||
uint8_t byte;
|
||||
get_buffer(&byte, 1);
|
||||
return byte;
|
||||
}
|
||||
|
||||
uint16_t FileAccessFilesystemJAndroid::get_16() const {
|
||||
ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
|
||||
uint16_t bytes = 0;
|
||||
get_buffer(reinterpret_cast<uint8_t *>(&bytes), 2);
|
||||
if (big_endian) {
|
||||
bytes = BSWAP16(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
uint32_t FileAccessFilesystemJAndroid::get_32() const {
|
||||
ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
|
||||
uint32_t bytes = 0;
|
||||
get_buffer(reinterpret_cast<uint8_t *>(&bytes), 4);
|
||||
if (big_endian) {
|
||||
bytes = BSWAP32(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
uint64_t FileAccessFilesystemJAndroid::get_64() const {
|
||||
ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
|
||||
uint64_t bytes = 0;
|
||||
get_buffer(reinterpret_cast<uint8_t *>(&bytes), 8);
|
||||
if (big_endian) {
|
||||
bytes = BSWAP64(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
String FileAccessFilesystemJAndroid::get_line() const {
|
||||
ERR_FAIL_COND_V_MSG(!is_open(), String(), "File must be opened before use.");
|
||||
|
||||
|
|
@ -277,44 +234,23 @@ uint64_t FileAccessFilesystemJAndroid::get_buffer(uint8_t *p_dst, uint64_t p_len
|
|||
}
|
||||
}
|
||||
|
||||
void FileAccessFilesystemJAndroid::store_8(uint8_t p_dest) {
|
||||
store_buffer(&p_dest, 1);
|
||||
}
|
||||
|
||||
void FileAccessFilesystemJAndroid::store_16(uint16_t p_dest) {
|
||||
if (big_endian) {
|
||||
p_dest = BSWAP16(p_dest);
|
||||
}
|
||||
store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 2);
|
||||
}
|
||||
|
||||
void FileAccessFilesystemJAndroid::store_32(uint32_t p_dest) {
|
||||
if (big_endian) {
|
||||
p_dest = BSWAP32(p_dest);
|
||||
}
|
||||
store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 4);
|
||||
}
|
||||
|
||||
void FileAccessFilesystemJAndroid::store_64(uint64_t p_dest) {
|
||||
if (big_endian) {
|
||||
p_dest = BSWAP64(p_dest);
|
||||
}
|
||||
store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 8);
|
||||
}
|
||||
|
||||
void FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) {
|
||||
bool FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) {
|
||||
if (_file_write) {
|
||||
ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
|
||||
ERR_FAIL_COND_V_MSG(!is_open(), false, "File must be opened before use.");
|
||||
ERR_FAIL_COND_V(!p_src && p_length > 0, false);
|
||||
if (p_length == 0) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL(env);
|
||||
ERR_FAIL_NULL_V(env, false);
|
||||
|
||||
jobject j_buffer = env->NewDirectByteBuffer((void *)p_src, p_length);
|
||||
env->CallVoidMethod(file_access_handler, _file_write, id, j_buffer);
|
||||
bool ok = env->CallBooleanMethod(file_access_handler, _file_write, id, j_buffer);
|
||||
env->DeleteLocalRef(j_buffer);
|
||||
return ok;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -331,19 +267,7 @@ Error FileAccessFilesystemJAndroid::resize(int64_t p_length) {
|
|||
ERR_FAIL_NULL_V(env, FAILED);
|
||||
ERR_FAIL_COND_V_MSG(!is_open(), FAILED, "File must be opened before use.");
|
||||
int res = env->CallIntMethod(file_access_handler, _file_resize, id, p_length);
|
||||
switch (res) {
|
||||
case 0:
|
||||
return OK;
|
||||
case -4:
|
||||
return ERR_INVALID_PARAMETER;
|
||||
case -3:
|
||||
return ERR_FILE_CANT_OPEN;
|
||||
case -2:
|
||||
return ERR_FILE_NOT_FOUND;
|
||||
case -1:
|
||||
default:
|
||||
return FAILED;
|
||||
}
|
||||
return static_cast<Error>(res);
|
||||
} else {
|
||||
return ERR_UNAVAILABLE;
|
||||
}
|
||||
|
|
@ -404,7 +328,7 @@ void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) {
|
|||
_file_seek_end = env->GetMethodID(cls, "fileSeekFromEnd", "(IJ)V");
|
||||
_file_read = env->GetMethodID(cls, "fileRead", "(ILjava/nio/ByteBuffer;)I");
|
||||
_file_close = env->GetMethodID(cls, "fileClose", "(I)V");
|
||||
_file_write = env->GetMethodID(cls, "fileWrite", "(ILjava/nio/ByteBuffer;)V");
|
||||
_file_write = env->GetMethodID(cls, "fileWrite", "(ILjava/nio/ByteBuffer;)Z");
|
||||
_file_flush = env->GetMethodID(cls, "fileFlush", "(I)V");
|
||||
_file_exists = env->GetMethodID(cls, "fileExists", "(Ljava/lang/String;)Z");
|
||||
_file_last_modified = env->GetMethodID(cls, "fileLastModified", "(Ljava/lang/String;)J");
|
||||
|
|
|
|||
|
|
@ -78,21 +78,13 @@ public:
|
|||
virtual bool eof_reached() const override; ///< reading passed EOF
|
||||
|
||||
virtual Error resize(int64_t p_length) override;
|
||||
virtual uint8_t get_8() const override; ///< get a byte
|
||||
virtual uint16_t get_16() const override;
|
||||
virtual uint32_t get_32() const override;
|
||||
virtual uint64_t get_64() const override;
|
||||
virtual String get_line() const override; ///< get a line
|
||||
virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override;
|
||||
|
||||
virtual Error get_error() const override; ///< get last error
|
||||
|
||||
virtual void flush() override;
|
||||
virtual void store_8(uint8_t p_dest) override; ///< store a byte
|
||||
virtual void store_16(uint16_t p_dest) override;
|
||||
virtual void store_32(uint32_t p_dest) override;
|
||||
virtual void store_64(uint64_t p_dest) override;
|
||||
virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override;
|
||||
virtual bool store_buffer(const uint8_t *p_src, uint64_t p_length) override;
|
||||
|
||||
virtual bool file_exists(const String &p_path) override; ///< return true if a file exists
|
||||
|
||||
|
|
@ -101,7 +93,7 @@ public:
|
|||
|
||||
virtual uint64_t _get_modified_time(const String &p_file) override;
|
||||
virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; }
|
||||
virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; }
|
||||
virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return ERR_UNAVAILABLE; }
|
||||
|
||||
virtual bool _get_hidden_attribute(const String &p_file) override { return false; }
|
||||
virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; }
|
||||
|
|
|
|||
127
engine/platform/android/game_menu_utils_jni.cpp
Normal file
127
engine/platform/android/game_menu_utils_jni.cpp
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/**************************************************************************/
|
||||
/* game_menu_utils_jni.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "game_menu_utils_jni.h"
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
#include "editor/editor_interface.h"
|
||||
#include "editor/editor_node.h"
|
||||
#include "editor/plugins/game_view_plugin.h"
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_suspend(enabled);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->next_frame();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_node_type(type);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_select_mode(mode);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_selection_visible(visible);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_camera_override(enabled);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_camera_manipulate_mode(static_cast<EditorDebuggerNode::CameraOverride>(mode));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->reset_camera_2d_position();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->reset_camera_3d_position();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
EditorInterface::get_singleton()->play_main_scene();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
49
engine/platform/android/game_menu_utils_jni.h
Normal file
49
engine/platform/android/game_menu_utils_jni.h
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**************************************************************************/
|
||||
/* game_menu_utils_jni.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#ifndef GAME_MENU_UTILS_JNI_H
|
||||
#define GAME_MENU_UTILS_JNI_H
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
extern "C" {
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz);
|
||||
}
|
||||
|
||||
#endif // GAME_MENU_UTILS_JNI_H
|
||||
|
|
@ -3,14 +3,6 @@
|
|||
This file list third-party libraries used in the Android source folder,
|
||||
with their provenance and, when relevant, modifications made to those files.
|
||||
|
||||
## com.android.vending.billing
|
||||
|
||||
- Upstream: https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive/app/src/main
|
||||
- Version: git (7a94c69, 2019)
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`.
|
||||
|
||||
## com.google.android.vending.expansion.downloader
|
||||
|
||||
- Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library
|
||||
|
|
@ -19,10 +11,10 @@ Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`.
|
|||
|
||||
Overwrite all files under:
|
||||
|
||||
- `src/com/google/android/vending/expansion/downloader`
|
||||
- `lib/src/com/google/android/vending/expansion/downloader`
|
||||
|
||||
Some files have been modified for yet unclear reasons.
|
||||
See the `patches/com.google.android.vending.expansion.downloader.patch` file.
|
||||
See the `lib/patches/com.google.android.vending.expansion.downloader.patch` file.
|
||||
|
||||
## com.google.android.vending.licensing
|
||||
|
||||
|
|
@ -32,8 +24,18 @@ See the `patches/com.google.android.vending.expansion.downloader.patch` file.
|
|||
|
||||
Overwrite all files under:
|
||||
|
||||
- `aidl/com/android/vending/licensing`
|
||||
- `src/com/google/android/vending/licensing`
|
||||
- `lib/aidl/com/android/vending/licensing`
|
||||
- `lib/src/com/google/android/vending/licensing`
|
||||
|
||||
Some files have been modified to silence linter errors or fix downstream issues.
|
||||
See the `patches/com.google.android.vending.licensing.patch` file.
|
||||
See the `lib/patches/com.google.android.vending.licensing.patch` file.
|
||||
|
||||
## com.android.apksig
|
||||
|
||||
- Upstream: https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888
|
||||
- Version: git (ac5cbb07d87cc342fcf07715857a812305d69888, 2024)
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite all files under:
|
||||
|
||||
- `editor/src/main/java/com/android/apksig`
|
||||
|
|
@ -33,6 +33,10 @@
|
|||
<meta-data
|
||||
android:name="org.godotengine.editor.version"
|
||||
android:value="${godotEditorVersion}" />
|
||||
<!-- Records the rendering method used by the Godot engine -->
|
||||
<meta-data
|
||||
android:name="org.godotengine.rendering.method"
|
||||
android:value="${godotRenderingMethod}"/>
|
||||
|
||||
<activity
|
||||
android:name=".GodotApp"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ allprojects {
|
|||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"}
|
||||
|
||||
// Godot user plugins custom maven repos
|
||||
String[] mavenRepos = getGodotPluginsMavenRepos()
|
||||
|
|
@ -28,6 +29,8 @@ allprojects {
|
|||
configurations {
|
||||
// Initializes a placeholder for the devImplementation dependency configuration.
|
||||
devImplementation {}
|
||||
// Initializes a placeholder for the monoImplementation dependency configuration.
|
||||
monoImplementation {}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -41,9 +44,9 @@ dependencies {
|
|||
} else {
|
||||
// Godot gradle build mode. In this scenario this project is the only one around and the Godot
|
||||
// library is available through the pre-generated godot-lib.*.aar android archive files.
|
||||
debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar'])
|
||||
devImplementation fileTree(dir: 'libs/dev', include: ['*.jar', '*.aar'])
|
||||
releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar'])
|
||||
debugImplementation fileTree(dir: 'libs/debug', include: ['**/*.jar', '*.aar'])
|
||||
devImplementation fileTree(dir: 'libs/dev', include: ['**/*.jar', '*.aar'])
|
||||
releaseImplementation fileTree(dir: 'libs/release', include: ['**/*.jar', '*.aar'])
|
||||
}
|
||||
|
||||
// Godot user plugins remote dependencies
|
||||
|
|
@ -59,6 +62,18 @@ dependencies {
|
|||
if (pluginsBinaries != null && pluginsBinaries.size() > 0) {
|
||||
implementation files(pluginsBinaries)
|
||||
}
|
||||
|
||||
// Automatically pick up local dependencies in res://addons
|
||||
String addonsDirectory = getAddonsDirectory()
|
||||
if (addonsDirectory != null && !addonsDirectory.isBlank()) {
|
||||
implementation fileTree(dir: "$addonsDirectory", include: ['*.jar', '*.aar'])
|
||||
}
|
||||
|
||||
// .NET dependencies
|
||||
String jar = '../../../../modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar'
|
||||
if (file(jar).exists()) {
|
||||
monoImplementation files(jar)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -90,7 +105,10 @@ android {
|
|||
abiFilters export_abi_list
|
||||
}
|
||||
|
||||
manifestPlaceholders = [godotEditorVersion: getGodotEditorVersion()]
|
||||
manifestPlaceholders = [
|
||||
godotEditorVersion: getGodotEditorVersion(),
|
||||
godotRenderingMethod: getGodotRenderingMethod()
|
||||
]
|
||||
|
||||
// Feel free to modify the application id to your own.
|
||||
applicationId getExportPackageName()
|
||||
|
|
@ -154,6 +172,10 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
debug {
|
||||
|
|
@ -191,6 +213,13 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
flavorDimensions 'edition'
|
||||
|
||||
productFlavors {
|
||||
standard {}
|
||||
mono {}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
|
|
@ -206,75 +235,38 @@ android {
|
|||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all { output ->
|
||||
output.outputFileName = "android_${variant.name}.apk"
|
||||
String filenameSuffix = variant.flavorName == "mono" ? variant.name : variant.buildType.name
|
||||
output.outputFileName = "android_${filenameSuffix}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task copyAndRenameDebugApk(type: Copy) {
|
||||
task copyAndRenameBinary(type: Copy) {
|
||||
// The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
|
||||
// and directories. Otherwise this check may cause permissions access failures on Windows
|
||||
// machines.
|
||||
doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
|
||||
|
||||
from "$buildDir/outputs/apk/debug/android_debug.apk"
|
||||
into getExportPath()
|
||||
rename "android_debug.apk", getExportFilename()
|
||||
}
|
||||
String exportPath = getExportPath()
|
||||
String exportFilename = getExportFilename()
|
||||
String exportEdition = getExportEdition()
|
||||
String exportBuildType = getExportBuildType()
|
||||
String exportBuildTypeCapitalized = exportBuildType.capitalize()
|
||||
String exportFormat = getExportFormat()
|
||||
|
||||
task copyAndRenameDevApk(type: Copy) {
|
||||
// The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
|
||||
// and directories. Otherwise this check may cause permissions access failures on Windows
|
||||
// machines.
|
||||
doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
|
||||
boolean isAab = exportFormat == "aab"
|
||||
boolean isMono = exportEdition == "mono"
|
||||
String filenameSuffix = isAab ? "${exportEdition}-${exportBuildType}" : exportBuildType
|
||||
if (isMono) {
|
||||
filenameSuffix = isAab ? "${exportEdition}-${exportBuildType}" : "${exportEdition}${exportBuildTypeCapitalized}"
|
||||
}
|
||||
|
||||
from "$buildDir/outputs/apk/dev/android_dev.apk"
|
||||
into getExportPath()
|
||||
rename "android_dev.apk", getExportFilename()
|
||||
}
|
||||
String sourceFilename = isAab ? "build-${filenameSuffix}.aab" : "android_${filenameSuffix}.apk"
|
||||
String sourceFilepath = isAab ? "$buildDir/outputs/bundle/${exportEdition}${exportBuildTypeCapitalized}/$sourceFilename" : "$buildDir/outputs/apk/$exportEdition/$exportBuildType/$sourceFilename"
|
||||
|
||||
task copyAndRenameReleaseApk(type: Copy) {
|
||||
// The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
|
||||
// and directories. Otherwise this check may cause permissions access failures on Windows
|
||||
// machines.
|
||||
doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
|
||||
|
||||
from "$buildDir/outputs/apk/release/android_release.apk"
|
||||
into getExportPath()
|
||||
rename "android_release.apk", getExportFilename()
|
||||
}
|
||||
|
||||
task copyAndRenameDebugAab(type: Copy) {
|
||||
// The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
|
||||
// and directories. Otherwise this check may cause permissions access failures on Windows
|
||||
// machines.
|
||||
doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
|
||||
|
||||
from "$buildDir/outputs/bundle/debug/build-debug.aab"
|
||||
into getExportPath()
|
||||
rename "build-debug.aab", getExportFilename()
|
||||
}
|
||||
|
||||
task copyAndRenameDevAab(type: Copy) {
|
||||
// The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
|
||||
// and directories. Otherwise this check may cause permissions access failures on Windows
|
||||
// machines.
|
||||
doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
|
||||
|
||||
from "$buildDir/outputs/bundle/dev/build-dev.aab"
|
||||
into getExportPath()
|
||||
rename "build-dev.aab", getExportFilename()
|
||||
}
|
||||
|
||||
task copyAndRenameReleaseAab(type: Copy) {
|
||||
// The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
|
||||
// and directories. Otherwise this check may cause permissions access failures on Windows
|
||||
// machines.
|
||||
doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
|
||||
|
||||
from "$buildDir/outputs/bundle/release/build-release.aab"
|
||||
into getExportPath()
|
||||
rename "build-release.aab", getExportFilename()
|
||||
from sourceFilepath
|
||||
into exportPath
|
||||
rename sourceFilename, exportFilename
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ ext.versions = [
|
|||
javaVersion : JavaVersion.VERSION_17,
|
||||
// Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
|
||||
ndkVersion : '23.2.8568313',
|
||||
splashscreenVersion: '1.0.1'
|
||||
splashscreenVersion: '1.0.1',
|
||||
openxrVendorsVersion: '3.1.2-stable'
|
||||
|
||||
]
|
||||
|
||||
|
|
@ -70,6 +71,11 @@ ext.getExportTargetSdkVersion = { ->
|
|||
}
|
||||
}
|
||||
|
||||
ext.getGodotRenderingMethod = { ->
|
||||
String renderingMethod = project.hasProperty("godot_rendering_method") ? project.property("godot_rendering_method") : ""
|
||||
return renderingMethod
|
||||
}
|
||||
|
||||
ext.getGodotEditorVersion = { ->
|
||||
String editorVersion = project.hasProperty("godot_editor_version") ? project.property("godot_editor_version") : ""
|
||||
if (editorVersion == null || editorVersion.isEmpty()) {
|
||||
|
|
@ -224,6 +230,30 @@ ext.getExportFilename = {
|
|||
return exportFilename
|
||||
}
|
||||
|
||||
ext.getExportEdition = {
|
||||
String exportEdition = project.hasProperty("export_edition") ? project.property("export_edition") : ""
|
||||
if (exportEdition == null || exportEdition.isEmpty()) {
|
||||
exportEdition = "standard"
|
||||
}
|
||||
return exportEdition
|
||||
}
|
||||
|
||||
ext.getExportBuildType = {
|
||||
String exportBuildType = project.hasProperty("export_build_type") ? project.property("export_build_type") : ""
|
||||
if (exportBuildType == null || exportBuildType.isEmpty()) {
|
||||
exportBuildType = "debug"
|
||||
}
|
||||
return exportBuildType
|
||||
}
|
||||
|
||||
ext.getExportFormat = {
|
||||
String exportFormat = project.hasProperty("export_format") ? project.property("export_format") : ""
|
||||
if (exportFormat == null || exportFormat.isEmpty()) {
|
||||
exportFormat = "apk"
|
||||
}
|
||||
return exportFormat
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the project properties for the 'plugins_maven_repos' property and return the list
|
||||
* of maven repos.
|
||||
|
|
@ -384,3 +414,8 @@ ext.shouldUseLegacyPackaging = { ->
|
|||
// Default behavior for minSdk >= 23
|
||||
return false
|
||||
}
|
||||
|
||||
ext.getAddonsDirectory = { ->
|
||||
String addonsDirectory = project.hasProperty("addons_directory") ? project.property("addons_directory") : ""
|
||||
return addonsDirectory
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar"/>
|
||||
<style name="GodotAppMainTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowSwipeToDismiss">false</item>
|
||||
</style>
|
||||
|
||||
<style name="GodotAppSplashTheme" parent="Theme.SplashScreen">
|
||||
<!-- Set the splash screen background, animated icon, and animation
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pluginManagement {
|
|||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,14 +33,29 @@ package com.godot.game;
|
|||
import org.godotengine.godot.GodotActivity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
|
||||
import com.godot.game.BuildConfig;
|
||||
|
||||
/**
|
||||
* Template activity for Godot Android builds.
|
||||
* Feel free to extend and modify this class for your custom logic.
|
||||
*/
|
||||
public class GodotApp extends GodotActivity {
|
||||
static {
|
||||
// .NET libraries.
|
||||
if (BuildConfig.FLAVOR.equals("mono")) {
|
||||
try {
|
||||
Log.v("GODOT", "Loading System.Security.Cryptography.Native.Android library");
|
||||
System.loadLibrary("System.Security.Cryptography.Native.Android");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
Log.e("GODOT", "Unable to load System.Security.Cryptography.Native.Android library");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
SplashScreen.installSplashScreen(this);
|
||||
|
|
|
|||
|
|
@ -18,16 +18,19 @@ allprojects {
|
|||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"}
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"]
|
||||
supportedFlavors = ["editor", "template"]
|
||||
supportedAndroidDistributions = ["android", "horizonos", "picoos"]
|
||||
supportedFlavorsBuildTypes = [
|
||||
"editor": ["dev", "debug", "release"],
|
||||
"template": ["dev", "debug", "release"]
|
||||
]
|
||||
supportedEditions = ["standard", "mono"]
|
||||
|
||||
// Used by gradle to specify which architecture to build for by default when running
|
||||
// `./gradlew build` (this command is usually used by Android Studio).
|
||||
|
|
@ -35,122 +38,23 @@ ext {
|
|||
// `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s).
|
||||
// The {selectedAbis} values must be from the {supportedAbis} values.
|
||||
selectedAbis = ["arm64"]
|
||||
}
|
||||
|
||||
def rootDir = "../../.."
|
||||
def binDir = "$rootDir/bin/"
|
||||
def androidEditorBuildsDir = "$binDir/android_editor_builds/"
|
||||
rootDir = "../../.."
|
||||
binDir = "$rootDir/bin/"
|
||||
androidEditorBuildsDir = "$binDir/android_editor_builds/"
|
||||
}
|
||||
|
||||
def getSconsTaskName(String flavor, String buildType, String abi) {
|
||||
return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the generated 'android_debug.apk' binary template into the Godot bin directory.
|
||||
* Depends on the app build task to ensure the binary is generated prior to copying.
|
||||
*/
|
||||
task copyDebugBinaryToBin(type: Copy) {
|
||||
dependsOn ':app:assembleDebug'
|
||||
from('app/build/outputs/apk/debug')
|
||||
into(binDir)
|
||||
include('android_debug.apk')
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the generated 'android_dev.apk' binary template into the Godot bin directory.
|
||||
* Depends on the app build task to ensure the binary is generated prior to copying.
|
||||
*/
|
||||
task copyDevBinaryToBin(type: Copy) {
|
||||
dependsOn ':app:assembleDev'
|
||||
from('app/build/outputs/apk/dev')
|
||||
into(binDir)
|
||||
include('android_dev.apk')
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the generated 'android_release.apk' binary template into the Godot bin directory.
|
||||
* Depends on the app build task to ensure the binary is generated prior to copying.
|
||||
*/
|
||||
task copyReleaseBinaryToBin(type: Copy) {
|
||||
dependsOn ':app:assembleRelease'
|
||||
from('app/build/outputs/apk/release')
|
||||
into(binDir)
|
||||
include('android_release.apk')
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the Godot android library archive debug file into the app module debug libs directory.
|
||||
* Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
*/
|
||||
task copyDebugAARToAppModule(type: Copy) {
|
||||
dependsOn ':lib:assembleTemplateDebug'
|
||||
from('lib/build/outputs/aar')
|
||||
into('app/libs/debug')
|
||||
include('godot-lib.template_debug.aar')
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the Godot android library archive debug file into the root bin directory.
|
||||
* Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
*/
|
||||
task copyDebugAARToBin(type: Copy) {
|
||||
dependsOn ':lib:assembleTemplateDebug'
|
||||
from('lib/build/outputs/aar')
|
||||
into(binDir)
|
||||
include('godot-lib.template_debug.aar')
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the Godot android library archive dev file into the app module dev libs directory.
|
||||
* Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
*/
|
||||
task copyDevAARToAppModule(type: Copy) {
|
||||
dependsOn ':lib:assembleTemplateDev'
|
||||
from('lib/build/outputs/aar')
|
||||
into('app/libs/dev')
|
||||
include('godot-lib.template_debug.dev.aar')
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the Godot android library archive dev file into the root bin directory.
|
||||
* Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
*/
|
||||
task copyDevAARToBin(type: Copy) {
|
||||
dependsOn ':lib:assembleTemplateDev'
|
||||
from('lib/build/outputs/aar')
|
||||
into(binDir)
|
||||
include('godot-lib.template_debug.dev.aar')
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the Godot android library archive release file into the app module release libs directory.
|
||||
* Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
*/
|
||||
task copyReleaseAARToAppModule(type: Copy) {
|
||||
dependsOn ':lib:assembleTemplateRelease'
|
||||
from('lib/build/outputs/aar')
|
||||
into('app/libs/release')
|
||||
include('godot-lib.template_release.aar')
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the Godot android library archive release file into the root bin directory.
|
||||
* Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
*/
|
||||
task copyReleaseAARToBin(type: Copy) {
|
||||
dependsOn ':lib:assembleTemplateRelease'
|
||||
from('lib/build/outputs/aar')
|
||||
into(binDir)
|
||||
include('godot-lib.template_release.aar')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Godot gradle build template by zipping the source files from the app directory, as well
|
||||
* as the AAR files generated by 'copyDebugAAR', 'copyDevAAR' and 'copyReleaseAAR'.
|
||||
* The zip file also includes some gradle tools to enable gradle builds from the Godot Editor.
|
||||
*/
|
||||
task zipGradleBuild(type: Zip) {
|
||||
onlyIf { generateGodotTemplates.state.executed || generateDevTemplate.state.executed }
|
||||
onlyIf { generateGodotTemplates.state.executed || generateGodotMonoTemplates.state.executed || generateDevTemplate.state.executed }
|
||||
doFirst {
|
||||
logger.lifecycle("Generating Godot gradle build template")
|
||||
}
|
||||
|
|
@ -191,99 +95,158 @@ def templateExcludedBuildTask() {
|
|||
/**
|
||||
* Generates the build tasks for the given flavor
|
||||
* @param flavor Must be one of the supported flavors ('template' / 'editor')
|
||||
* @param edition Must be one of the supported editions ('standard' / 'mono')
|
||||
* @param androidDistro Must be one of the supported Android distributions ('android' / 'horizonos' / 'picoos')
|
||||
*/
|
||||
def generateBuildTasks(String flavor = "template") {
|
||||
def generateBuildTasks(String flavor = "template", String edition = "standard", String androidDistro = "android") {
|
||||
if (!supportedFlavors.contains(flavor)) {
|
||||
throw new GradleException("Invalid build flavor: $flavor")
|
||||
}
|
||||
if (!supportedAndroidDistributions.contains(androidDistro)) {
|
||||
throw new GradleException("Invalid Android distribution: $androidDistro")
|
||||
}
|
||||
if (!supportedEditions.contains(edition)) {
|
||||
throw new GradleException("Invalid build edition: $edition")
|
||||
}
|
||||
if (edition == "mono" && flavor != "template") {
|
||||
throw new GradleException("'mono' edition only supports the 'template' flavor.")
|
||||
}
|
||||
|
||||
def tasks = []
|
||||
String capitalizedAndroidDistro = androidDistro.capitalize()
|
||||
def buildTasks = []
|
||||
|
||||
// Only build the apks and aar files for which we have native shared libraries unless we intend
|
||||
// Only build the binary files for which we have native shared libraries unless we intend
|
||||
// to run the scons build tasks.
|
||||
boolean excludeSconsBuildTasks = excludeSconsBuildTasks()
|
||||
boolean isTemplate = flavor == "template"
|
||||
String libsDir = isTemplate ? "lib/libs/" : "lib/libs/tools/"
|
||||
for (String target : supportedFlavorsBuildTypes[flavor]) {
|
||||
File targetLibs = new File(libsDir + target)
|
||||
|
||||
String targetSuffix = target
|
||||
if (target == "dev") {
|
||||
targetSuffix = "debug.dev"
|
||||
}
|
||||
|
||||
if (!excludeSconsBuildTasks || (targetLibs != null
|
||||
&& targetLibs.isDirectory()
|
||||
&& targetLibs.listFiles() != null
|
||||
&& targetLibs.listFiles().length > 0)) {
|
||||
|
||||
String capitalizedTarget = target.capitalize()
|
||||
String capitalizedEdition = edition.capitalize()
|
||||
if (isTemplate) {
|
||||
// Copy the generated aar library files to the build directory.
|
||||
tasks += "copy${capitalizedTarget}AARToAppModule"
|
||||
// Copy the generated aar library files to the bin directory.
|
||||
tasks += "copy${capitalizedTarget}AARToBin"
|
||||
// Copy the prebuilt binary templates to the bin directory.
|
||||
tasks += "copy${capitalizedTarget}BinaryToBin"
|
||||
// Copy the Godot android library archive file into the app module libs directory.
|
||||
// Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
String copyAARTaskName = "copy${capitalizedTarget}AARToAppModule"
|
||||
if (tasks.findByName(copyAARTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyAARTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyAARTaskName, type: Copy) {
|
||||
dependsOn ":lib:assembleTemplate${capitalizedTarget}"
|
||||
from('lib/build/outputs/aar')
|
||||
include("godot-lib.template_${targetSuffix}.aar")
|
||||
into("app/libs/${target}")
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the Godot android library archive file into the root bin directory.
|
||||
// Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
String copyAARToBinTaskName = "copy${capitalizedTarget}AARToBin"
|
||||
if (tasks.findByName(copyAARToBinTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyAARToBinTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyAARToBinTaskName, type: Copy) {
|
||||
dependsOn ":lib:assembleTemplate${capitalizedTarget}"
|
||||
from('lib/build/outputs/aar')
|
||||
include("godot-lib.template_${targetSuffix}.aar")
|
||||
into(binDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the generated binary template into the Godot bin directory.
|
||||
// Depends on the app build task to ensure the binary is generated prior to copying.
|
||||
String copyBinaryTaskName = "copy${capitalizedEdition}${capitalizedTarget}BinaryToBin"
|
||||
if (tasks.findByName(copyBinaryTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyBinaryTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyBinaryTaskName, type: Copy) {
|
||||
String filenameSuffix = edition == "mono" ? "${edition}${capitalizedTarget}" : target
|
||||
dependsOn ":app:assemble${capitalizedEdition}${capitalizedTarget}"
|
||||
from("app/build/outputs/apk/${edition}/${target}")
|
||||
into(binDir)
|
||||
include("android_${filenameSuffix}.apk")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Copy the generated editor apk to the bin directory.
|
||||
tasks += "copyEditor${capitalizedTarget}ApkToBin"
|
||||
String copyEditorApkTaskName = "copyEditor${capitalizedAndroidDistro}${capitalizedTarget}ApkToBin"
|
||||
if (tasks.findByName(copyEditorApkTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyEditorApkTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyEditorApkTaskName, type: Copy) {
|
||||
dependsOn ":editor:assemble${capitalizedAndroidDistro}${capitalizedTarget}"
|
||||
from("editor/build/outputs/apk/${androidDistro}/${target}")
|
||||
into(androidEditorBuildsDir)
|
||||
include("android_editor-${androidDistro}-${target}*.apk")
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the generated editor aab to the bin directory.
|
||||
tasks += "copyEditor${capitalizedTarget}AabToBin"
|
||||
String copyEditorAabTaskName = "copyEditor${capitalizedAndroidDistro}${capitalizedTarget}AabToBin"
|
||||
if (tasks.findByName(copyEditorAabTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyEditorAabTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyEditorAabTaskName, type: Copy) {
|
||||
dependsOn ":editor:bundle${capitalizedAndroidDistro}${capitalizedTarget}"
|
||||
from("editor/build/outputs/bundle/${androidDistro}${capitalizedTarget}")
|
||||
into(androidEditorBuildsDir)
|
||||
include("android_editor-${androidDistro}-${target}*.aab")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.lifecycle("No native shared libs for target $target. Skipping build.")
|
||||
}
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
task copyEditorReleaseApkToBin(type: Copy) {
|
||||
dependsOn ':editor:assembleRelease'
|
||||
from('editor/build/outputs/apk/release')
|
||||
into(androidEditorBuildsDir)
|
||||
include('android_editor-release*.apk')
|
||||
}
|
||||
|
||||
task copyEditorReleaseAabToBin(type: Copy) {
|
||||
dependsOn ':editor:bundleRelease'
|
||||
from('editor/build/outputs/bundle/release')
|
||||
into(androidEditorBuildsDir)
|
||||
include('android_editor-release*.aab')
|
||||
}
|
||||
|
||||
task copyEditorDebugApkToBin(type: Copy) {
|
||||
dependsOn ':editor:assembleDebug'
|
||||
from('editor/build/outputs/apk/debug')
|
||||
into(androidEditorBuildsDir)
|
||||
include('android_editor-debug.apk')
|
||||
}
|
||||
|
||||
task copyEditorDebugAabToBin(type: Copy) {
|
||||
dependsOn ':editor:bundleDebug'
|
||||
from('editor/build/outputs/bundle/debug')
|
||||
into(androidEditorBuildsDir)
|
||||
include('android_editor-debug.aab')
|
||||
}
|
||||
|
||||
task copyEditorDevApkToBin(type: Copy) {
|
||||
dependsOn ':editor:assembleDev'
|
||||
from('editor/build/outputs/apk/dev')
|
||||
into(androidEditorBuildsDir)
|
||||
include('android_editor-dev.apk')
|
||||
}
|
||||
|
||||
task copyEditorDevAabToBin(type: Copy) {
|
||||
dependsOn ':editor:bundleDev'
|
||||
from('editor/build/outputs/bundle/dev')
|
||||
into(androidEditorBuildsDir)
|
||||
include('android_editor-dev.aab')
|
||||
return buildTasks
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Godot Editor Android apk.
|
||||
* Generate the Godot Editor binaries for Android devices.
|
||||
*
|
||||
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
|
||||
* must have been generated (via scons) prior to running this gradle task.
|
||||
* The task will only build the apk(s) for which the shared libraries is available.
|
||||
* The task will only build the binaries for which the shared libraries is available.
|
||||
*/
|
||||
task generateGodotEditor {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("editor")
|
||||
dependsOn = generateBuildTasks("editor", "standard", "android")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Godot Editor binaries for HorizonOS devices.
|
||||
*
|
||||
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
|
||||
* must have been generated (via scons) prior to running this gradle task.
|
||||
* The task will only build the binaries for which the shared libraries is available.
|
||||
*/
|
||||
task generateGodotHorizonOSEditor {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("editor", "standard", "horizonos")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Godot Editor binaries for PicoOS devices.
|
||||
*
|
||||
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
|
||||
* must have been generated (via scons) prior to running this gradle task.
|
||||
* The task will only build the binaries for which the shared libraries is available.
|
||||
*/
|
||||
task generateGodotPicoOSEditor {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("editor", "standard", "picoos")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -296,6 +259,17 @@ task generateGodotTemplates {
|
|||
finalizedBy 'zipGradleBuild'
|
||||
}
|
||||
|
||||
/**
|
||||
* Master task used to coordinate the tasks defined above to generate the set of Godot templates
|
||||
* for the 'mono' edition of the engine.
|
||||
*/
|
||||
task generateGodotMonoTemplates {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("template", "mono")
|
||||
|
||||
finalizedBy 'zipGradleBuild'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the same output as generateGodotTemplates but with dev symbols
|
||||
*/
|
||||
|
|
@ -354,6 +328,9 @@ task cleanGodotTemplates(type: Delete) {
|
|||
delete("$binDir/android_debug.apk")
|
||||
delete("$binDir/android_dev.apk")
|
||||
delete("$binDir/android_release.apk")
|
||||
delete("$binDir/android_monoDebug.apk")
|
||||
delete("$binDir/android_monoDev.apk")
|
||||
delete("$binDir/android_monoRelease.apk")
|
||||
delete("$binDir/android_source.zip")
|
||||
delete("$binDir/godot-lib.template_debug.aar")
|
||||
delete("$binDir/godot-lib.template_debug.dev.aar")
|
||||
|
|
|
|||
|
|
@ -5,15 +5,6 @@ plugins {
|
|||
id 'base'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
|
||||
implementation project(":lib")
|
||||
|
||||
implementation "androidx.window:window:1.3.0"
|
||||
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
}
|
||||
|
||||
ext {
|
||||
// Retrieve the build number from the environment variable; default to 0 if none is specified.
|
||||
// The build number is added as a suffix to the version code for upload to the Google Play store.
|
||||
|
|
@ -36,7 +27,7 @@ ext {
|
|||
// Return the keystore file used for signing the release build.
|
||||
getGodotKeystoreFile = { ->
|
||||
def keyStore = System.getenv("GODOT_ANDROID_SIGN_KEYSTORE")
|
||||
if (keyStore == null) {
|
||||
if (keyStore == null || keyStore.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return file(keyStore)
|
||||
|
|
@ -95,7 +86,7 @@ android {
|
|||
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
manifestPlaceholders += [
|
||||
editorAppName: "Godot Editor 4",
|
||||
editorAppName: "Godot Engine 4",
|
||||
editorBuildSuffix: ""
|
||||
]
|
||||
}
|
||||
|
|
@ -153,4 +144,51 @@ android {
|
|||
doNotStrip '**/*.so'
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions = ["android_distribution"]
|
||||
productFlavors {
|
||||
android {
|
||||
dimension "android_distribution"
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
}
|
||||
horizonos {
|
||||
dimension "android_distribution"
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters "arm64-v8a"
|
||||
}
|
||||
applicationIdSuffix ".meta"
|
||||
versionNameSuffix "-meta"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 32
|
||||
}
|
||||
picoos {
|
||||
dimension "android_distribution"
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters "arm64-v8a"
|
||||
}
|
||||
applicationIdSuffix ".pico"
|
||||
versionNameSuffix "-pico"
|
||||
minSdkVersion 29
|
||||
targetSdkVersion 32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
|
||||
implementation project(":lib")
|
||||
|
||||
implementation "androidx.window:window:1.3.0"
|
||||
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
implementation "org.bouncycastle:bcprov-jdk15to18:1.78"
|
||||
|
||||
// Meta dependencies
|
||||
horizonosImplementation "org.godotengine:godot-openxr-vendors-meta:$versions.openxrVendorsVersion"
|
||||
// Pico dependencies
|
||||
picoosImplementation "org.godotengine:godot-openxr-vendors-pico:$versions.openxrVendorsVersion"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**************************************************************************/
|
||||
/* FileErrors.kt */
|
||||
/* GodotEditor.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
|
|
@ -28,26 +28,11 @@
|
|||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.file
|
||||
package org.godotengine.editor
|
||||
|
||||
/**
|
||||
* Set of errors that may occur when performing data access.
|
||||
* Primary window of the Godot Editor.
|
||||
*
|
||||
* This is the implementation of the editor used when running on regular Android devices.
|
||||
*/
|
||||
internal enum class FileErrors(val nativeValue: Int) {
|
||||
OK(0),
|
||||
FAILED(-1),
|
||||
FILE_NOT_FOUND(-2),
|
||||
FILE_CANT_OPEN(-3),
|
||||
INVALID_PARAMETER(-4);
|
||||
|
||||
companion object {
|
||||
fun fromNativeError(error: Int): FileErrors? {
|
||||
for (fileError in entries) {
|
||||
if (fileError.nativeValue == error) {
|
||||
return fileError
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
open class GodotEditor : BaseGodotEditor()
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:horizonos="http://schemas.horizonos/sdk">
|
||||
|
||||
<horizonos:uses-horizonos-sdk
|
||||
horizonos:minSdkVersion="69"
|
||||
horizonos:targetSdkVersion="69" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.vr.headtracking"
|
||||
android:required="true"
|
||||
android:version="1"/>
|
||||
|
||||
<!-- Oculus Quest hand tracking -->
|
||||
<uses-permission android:name="com.oculus.permission.HAND_TRACKING" />
|
||||
<uses-feature
|
||||
android:name="oculus.software.handtracking"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Passthrough feature flag -->
|
||||
<uses-feature android:name="com.oculus.feature.PASSTHROUGH"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Overlay keyboard support -->
|
||||
<uses-feature android:name="oculus.software.overlay_keyboard" android:required="false"/>
|
||||
|
||||
<!-- Render model -->
|
||||
<uses-permission android:name="com.oculus.permission.RENDER_MODEL" />
|
||||
<uses-feature android:name="com.oculus.feature.RENDER_MODEL" android:required="false" />
|
||||
|
||||
<!-- Anchor api -->
|
||||
<uses-permission android:name="com.oculus.permission.USE_ANCHOR_API" />
|
||||
|
||||
<!-- Scene api -->
|
||||
<uses-permission android:name="com.oculus.permission.USE_SCENE" />
|
||||
|
||||
<!-- Temp removal of the 'REQUEST_INSTALL_PACKAGES' permission as it's currently forbidden by the Horizon OS store -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove" />
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".GodotEditor"
|
||||
android:exported="true"
|
||||
android:screenOrientation="landscape"
|
||||
tools:node="merge"
|
||||
tools:replace="android:screenOrientation">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="com.oculus.intent.category.2D" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.oculus.vrshell.free_resizing_lock_aspect_ratio" android:value="true"/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".GodotXRGame"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Supported Meta devices -->
|
||||
<meta-data
|
||||
android:name="com.oculus.supportedDevices"
|
||||
android:value="quest3|questpro"
|
||||
tools:replace="android:value" />
|
||||
|
||||
<!--
|
||||
We remove this meta-data originating from the vendors plugin as we only need the loader for
|
||||
now since the project being edited provides its own version of the vendors plugin.
|
||||
|
||||
This needs to be removed once we start implementing the immersive version of the project
|
||||
manager and editor windows.
|
||||
-->
|
||||
<meta-data
|
||||
android:name="org.godotengine.plugin.v2.GodotOpenXRMeta"
|
||||
android:value="org.godotengine.openxr.vendors.meta.GodotOpenXRMeta"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- Enable system splash screen -->
|
||||
<meta-data android:name="com.oculus.ossplash" android:value="true"/>
|
||||
<!-- Enable passthrough background during the splash screen -->
|
||||
<meta-data android:name="com.oculus.ossplash.background" android:value="passthrough-contextual"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,46 @@
|
|||
/**************************************************************************/
|
||||
/* GodotEditor.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.editor
|
||||
|
||||
/**
|
||||
* Primary window of the Godot Editor.
|
||||
*
|
||||
* This is the implementation of the editor used when running on HorizonOS devices.
|
||||
*/
|
||||
open class GodotEditor : BaseGodotEditor() {
|
||||
|
||||
override fun getXRRuntimePermissions(): MutableSet<String> {
|
||||
val xrRuntimePermissions = super.getXRRuntimePermissions()
|
||||
xrRuntimePermissions.add("com.oculus.permission.USE_SCENE")
|
||||
xrRuntimePermissions.add("horizonos.permission.USE_SCENE")
|
||||
return xrRuntimePermissions
|
||||
}
|
||||
}
|
||||
|
|
@ -25,10 +25,11 @@
|
|||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/icon"
|
||||
android:icon="@mipmap/themed_icon"
|
||||
android:label="${editorAppName}${editorBuildSuffix}"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/GodotEditorSplashScreenTheme"
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
android:name=".GodotEditor"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/themed_icon"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="userLandscape">
|
||||
<layout
|
||||
|
|
@ -59,14 +61,43 @@
|
|||
android:name=".GodotGame"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="false"
|
||||
android:label="@string/godot_project_name_string"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:launchMode="singleTask"
|
||||
android:process=":GodotGame"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="userLandscape">
|
||||
<layout
|
||||
android:defaultWidth="@dimen/editor_default_window_width"
|
||||
android:defaultHeight="@dimen/editor_default_window_height" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".embed.EmbeddedGodotGame"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="false"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:theme="@style/GodotEmbeddedGameTheme"
|
||||
android:taskAffinity=":embed"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:process=":EmbeddedGodotGame"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="userLandscape" />
|
||||
<activity
|
||||
android:name=".GodotXRGame"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:process=":GodotXRGame"
|
||||
android:launchMode="singleTask"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:exported="false"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:resizeableActivity="false"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,550 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.util.DataSink;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.RunnablesExecutor;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* APK signing logic which is independent of how input and output APKs are stored, parsed, and
|
||||
* generated.
|
||||
*
|
||||
* <p><h3>Operating Model</h3>
|
||||
*
|
||||
* The abstract operating model is that there is an input APK which is being signed, thus producing
|
||||
* an output APK. In reality, there may be just an output APK being built from scratch, or the input
|
||||
* APK and the output APK may be the same file. Because this engine does not deal with reading and
|
||||
* writing files, it can handle all of these scenarios.
|
||||
*
|
||||
* <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once
|
||||
* the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
|
||||
* This may be more efficient than signing the APK using a new instance of the engine. See
|
||||
* <a href="#incremental">Incremental Operation</a>.
|
||||
*
|
||||
* <p>In the engine's operating model, a signed APK is produced as follows.
|
||||
* <ol>
|
||||
* <li>JAR entries to be signed are output,</li>
|
||||
* <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
|
||||
* output,</li>
|
||||
* <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
|
||||
* to the output.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or
|
||||
* may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
|
||||
* engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
|
||||
* which tells the client whether the input JAR entry needs to be output. This avoids the need for
|
||||
* the client to hard-code the aspects of APK signing which determine which parts of input must be
|
||||
* ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
|
||||
* client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
|
||||
* APK.
|
||||
*
|
||||
* <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these
|
||||
* steps:
|
||||
* <ol>
|
||||
* <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
|
||||
* for signing multiple APKs.</li>
|
||||
* <li>Locate the input APK's APK Signing Block and provide it to
|
||||
* {@link #inputApkSigningBlock(DataSource)}.</li>
|
||||
* <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
|
||||
* whether this entry should be output. The engine may request to inspect the entry.</li>
|
||||
* <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
|
||||
* inspect the entry.</li>
|
||||
* <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
|
||||
* that additional JAR entries are output. These entries comprise the output APK's JAR
|
||||
* signature.</li>
|
||||
* <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
|
||||
* invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
|
||||
* an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
|
||||
* output APK's APK Signature Scheme v2 signature.</li>
|
||||
* <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
|
||||
* confirm that the output APK is signed.</li>
|
||||
* <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
|
||||
* engine free any resources it no longer needs.
|
||||
* </ol>
|
||||
*
|
||||
* <p>Some invocations of the engine may provide the client with a task to perform. The client is
|
||||
* expected to perform all requested tasks before proceeding to the next stage of signing. See
|
||||
* documentation of each method about the deadlines for performing the tasks requested by the
|
||||
* method.
|
||||
*
|
||||
* <p><h3 id="incremental">Incremental Operation</h3></a>
|
||||
*
|
||||
* The engine supports incremental operation where a signed APK is produced, then modified and
|
||||
* re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
|
||||
* by the developer. Re-signing may be more efficient than signing from scratch.
|
||||
*
|
||||
* <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through
|
||||
* {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
|
||||
* {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
|
||||
* and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
|
||||
* these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
|
||||
* APK.
|
||||
*
|
||||
* <p><h3>Output-only Operation</h3>
|
||||
*
|
||||
* The engine's abstract operating model consists of an input APK and an output APK. However, it is
|
||||
* possible to use the engine in output-only mode where the engine's {@code input...} methods are
|
||||
* not invoked. In this mode, the engine has less control over output because it cannot request that
|
||||
* some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
|
||||
* signed and will report an error if cannot do so.
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
|
||||
*/
|
||||
public interface ApkSignerEngine extends Closeable {
|
||||
|
||||
default void setExecutor(RunnablesExecutor executor) {
|
||||
throw new UnsupportedOperationException("setExecutor method is not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the signer engine with the data already present in the apk (if any). There
|
||||
* might already be data that can be reused if the entries has not been changed.
|
||||
*
|
||||
* @param manifestBytes
|
||||
* @param entryNames
|
||||
* @return set of entry names which were processed by the engine during the initialization, a
|
||||
* subset of entryNames
|
||||
*/
|
||||
default Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
|
||||
throw new UnsupportedOperationException("initWith method is not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the input APK contains the provided APK Signing Block. The
|
||||
* block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
|
||||
*
|
||||
* @param apkSigningBlock APK signing block of the input APK. The provided data source is
|
||||
* guaranteed to not be used by the engine after this method terminates.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the APK Signing Block
|
||||
* @throws ApkFormatException if the APK Signing Block is malformed
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
void inputApkSigningBlock(DataSource apkSigningBlock)
|
||||
throws IOException, ApkFormatException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was encountered in the input APK.
|
||||
*
|
||||
* <p>When an input entry is updated/changed, it's OK to not invoke
|
||||
* {@link #inputJarEntryRemoved(String)} before invoking this method.
|
||||
*
|
||||
* @return instructions about how to proceed with this entry
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was output.
|
||||
*
|
||||
* <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
|
||||
* requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
|
||||
* data requested by the engine.
|
||||
*
|
||||
* <p>When an already output entry is updated/changed, it's OK to not invoke
|
||||
* {@link #outputJarEntryRemoved(String)} before invoking this method.
|
||||
*
|
||||
* @return request to inspect the entry or {@code null} if the engine does not need to inspect
|
||||
* the entry. The request must be fulfilled before {@link #outputJarEntries()} is
|
||||
* invoked.
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was removed from the input. It's safe
|
||||
* to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
|
||||
*
|
||||
* @return output policy of this JAR entry. The policy indicates how this input entry affects
|
||||
* the output APK. The client of this engine should use this information to determine
|
||||
* how the removal of this input APK's JAR entry affects the output APK.
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
|
||||
throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was removed from the output. It's safe
|
||||
* to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
void outputJarEntryRemoved(String entryName) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that all JAR entries have been output.
|
||||
*
|
||||
* @return request to add JAR signature to the output or {@code null} if there is no need to add
|
||||
* a JAR signature. The request will contain additional JAR entries to be output. The
|
||||
* request must be fulfilled before
|
||||
* {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
|
||||
*
|
||||
* @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
|
||||
* from producing a valid signature. For example, if the engine uses the provided
|
||||
* {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries, or if the engine is closed
|
||||
*/
|
||||
OutputJarSignatureRequest outputJarEntries()
|
||||
throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
|
||||
SignatureException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
|
||||
*
|
||||
* <p>The provided data sources are guaranteed to not be used by the engine after this method
|
||||
* terminates.
|
||||
*
|
||||
* @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
|
||||
* DataSource)}.
|
||||
*
|
||||
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
|
||||
* the ZIP entries. In a well-formed archive, this section starts at the start of the
|
||||
* archive and extends all the way to the ZIP Central Directory.
|
||||
* @param zipCentralDirectory ZIP Central Directory section
|
||||
* @param zipEocd ZIP End of Central Directory (EoCD) record
|
||||
*
|
||||
* @return request to add an APK Signing Block to the output or {@code null} if the output must
|
||||
* not contain an APK Signing Block. The request must be fulfilled before
|
||||
* {@link #outputDone()} is invoked.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
|
||||
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
|
||||
* engine from producing a valid signature. For example, if the APK Signing Block
|
||||
* provided to the engine is malformed.
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries or to output JAR signature, or if the engine is closed
|
||||
*/
|
||||
@Deprecated
|
||||
OutputApkSigningBlockRequest outputZipSections(
|
||||
DataSource zipEntries,
|
||||
DataSource zipCentralDirectory,
|
||||
DataSource zipEocd)
|
||||
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, SignatureException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
|
||||
*
|
||||
* <p>The provided data sources are guaranteed to not be used by the engine after this method
|
||||
* terminates.
|
||||
*
|
||||
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
|
||||
* the ZIP entries. In a well-formed archive, this section starts at the start of the
|
||||
* archive and extends all the way to the ZIP Central Directory.
|
||||
* @param zipCentralDirectory ZIP Central Directory section
|
||||
* @param zipEocd ZIP End of Central Directory (EoCD) record
|
||||
*
|
||||
* @return request to add an APK Signing Block to the output or {@code null} if the output must
|
||||
* not contain an APK Signing Block. The request must be fulfilled before
|
||||
* {@link #outputDone()} is invoked.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
|
||||
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
|
||||
* engine from producing a valid signature. For example, if the APK Signing Block
|
||||
* provided to the engine is malformed.
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries or to output JAR signature, or if the engine is closed
|
||||
*/
|
||||
OutputApkSigningBlockRequest2 outputZipSections2(
|
||||
DataSource zipEntries,
|
||||
DataSource zipCentralDirectory,
|
||||
DataSource zipEocd)
|
||||
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, SignatureException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the signed APK was output.
|
||||
*
|
||||
* <p>This does not change the output APK. The method helps the client confirm that the current
|
||||
* output is signed.
|
||||
*
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries or to output signatures, or if the engine is closed
|
||||
*/
|
||||
void outputDone() throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Generates a V4 signature proto and write to output file.
|
||||
*
|
||||
* @param data Input data to calculate a verity hash tree and hash root
|
||||
* @param outputFile To store the serialized V4 Signature.
|
||||
* @param ignoreFailures Whether any failures will be silently ignored.
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IOException if protobuf fails to be serialized and written to file
|
||||
*/
|
||||
void signV4(DataSource data, File outputFile, boolean ignoreFailures)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
|
||||
|
||||
/**
|
||||
* Checks if the signing configuration provided to the engine is capable of creating a
|
||||
* SourceStamp.
|
||||
*/
|
||||
default boolean isEligibleForSourceStamp() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Generates the digest of the certificate used to sign the source stamp. */
|
||||
default byte[] generateSourceStampCertificateDigest() throws SignatureException {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates to this engine that it will no longer be used. Invoking this on an already closed
|
||||
* engine is OK.
|
||||
*
|
||||
* <p>This does not change the output APK. For example, if the output APK is not yet fully
|
||||
* signed, it will remain so after this method terminates.
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Instructions about how to handle an input APK's JAR entry.
|
||||
*
|
||||
* <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
|
||||
* may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
|
||||
* which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
|
||||
* invoked.
|
||||
*/
|
||||
public static class InputJarEntryInstructions {
|
||||
private final OutputPolicy mOutputPolicy;
|
||||
private final InspectJarEntryRequest mInspectJarEntryRequest;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
||||
* output policy and without a request to inspect the entry.
|
||||
*/
|
||||
public InputJarEntryInstructions(OutputPolicy outputPolicy) {
|
||||
this(outputPolicy, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
||||
* output mode and with the provided request to inspect the entry.
|
||||
*
|
||||
* @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
|
||||
* need to inspect the entry.
|
||||
*/
|
||||
public InputJarEntryInstructions(
|
||||
OutputPolicy outputPolicy,
|
||||
InspectJarEntryRequest inspectJarEntryRequest) {
|
||||
mOutputPolicy = outputPolicy;
|
||||
mInspectJarEntryRequest = inspectJarEntryRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output policy for this entry.
|
||||
*/
|
||||
public OutputPolicy getOutputPolicy() {
|
||||
return mOutputPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request to inspect the JAR entry or {@code null} if there is no need to
|
||||
* inspect the entry.
|
||||
*/
|
||||
public InspectJarEntryRequest getInspectJarEntryRequest() {
|
||||
return mInspectJarEntryRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output policy for an input APK's JAR entry.
|
||||
*/
|
||||
public static enum OutputPolicy {
|
||||
/** Entry must not be output. */
|
||||
SKIP,
|
||||
|
||||
/** Entry should be output. */
|
||||
OUTPUT,
|
||||
|
||||
/** Entry will be output by the engine. The client can thus ignore this input entry. */
|
||||
OUTPUT_BY_ENGINE,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to inspect the specified JAR entry.
|
||||
*
|
||||
* <p>The entry's uncompressed data must be provided to the data sink returned by
|
||||
* {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
|
||||
* must be invoked.
|
||||
*/
|
||||
interface InspectJarEntryRequest {
|
||||
|
||||
/**
|
||||
* Returns the data sink into which the entry's uncompressed data should be sent.
|
||||
*/
|
||||
DataSink getDataSink();
|
||||
|
||||
/**
|
||||
* Indicates that entry's data has been provided in full.
|
||||
*/
|
||||
void done();
|
||||
|
||||
/**
|
||||
* Returns the name of the JAR entry.
|
||||
*/
|
||||
String getEntryName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add JAR signature (aka v1 signature) to the output APK.
|
||||
*
|
||||
* <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
|
||||
* which {@link #done()} must be invoked.
|
||||
*/
|
||||
interface OutputJarSignatureRequest {
|
||||
|
||||
/**
|
||||
* Returns JAR entries that must be added to the output APK.
|
||||
*/
|
||||
List<JarEntry> getAdditionalJarEntries();
|
||||
|
||||
/**
|
||||
* Indicates that the JAR entries contained in this request were added to the output APK.
|
||||
*/
|
||||
void done();
|
||||
|
||||
/**
|
||||
* JAR entry.
|
||||
*/
|
||||
public static class JarEntry {
|
||||
private final String mName;
|
||||
private final byte[] mData;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code JarEntry} with the provided name and data.
|
||||
*
|
||||
* @param data uncompressed data of the entry. Changes to this array will not be
|
||||
* reflected in {@link #getData()}.
|
||||
*/
|
||||
public JarEntry(String name, byte[] data) {
|
||||
mName = name;
|
||||
mData = data.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this ZIP entry.
|
||||
*/
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uncompressed data of this JAR entry.
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return mData.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
|
||||
* signature(s) of the APK are contained in this block.
|
||||
*
|
||||
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
|
||||
* output APK such that the block is immediately before the ZIP Central Directory, the offset of
|
||||
* ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
|
||||
* accordingly, and then {@link #done()} must be invoked.
|
||||
*
|
||||
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
|
||||
* contained in this request.
|
||||
*
|
||||
* @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
|
||||
*/
|
||||
@Deprecated
|
||||
interface OutputApkSigningBlockRequest {
|
||||
|
||||
/**
|
||||
* Returns the APK Signing Block.
|
||||
*/
|
||||
byte[] getApkSigningBlock();
|
||||
|
||||
/**
|
||||
* Indicates that the APK Signing Block was output as requested.
|
||||
*/
|
||||
void done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
|
||||
* signature(s) of the APK are contained in this block.
|
||||
*
|
||||
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
|
||||
* output APK such that the block is immediately before the ZIP Central Directory. Immediately
|
||||
* before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
|
||||
* {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
|
||||
* ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
|
||||
* must be invoked.
|
||||
*
|
||||
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
|
||||
* contained in this request.
|
||||
*/
|
||||
interface OutputApkSigningBlockRequest2 {
|
||||
/**
|
||||
* Returns the APK Signing Block.
|
||||
*/
|
||||
byte[] getApkSigningBlock();
|
||||
|
||||
/**
|
||||
* Indicates that the APK Signing Block was output as requested.
|
||||
*/
|
||||
void done();
|
||||
|
||||
/**
|
||||
* Returns the number of 0x00 bytes the caller must place immediately before APK Signing
|
||||
* Block.
|
||||
*/
|
||||
int getPaddingSizeBeforeApkSigningBlock();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
/**
|
||||
* This class is intended as a lightweight representation of an APK signature verification issue
|
||||
* where the client does not require the additional textual details provided by a subclass.
|
||||
*/
|
||||
public class ApkVerificationIssue {
|
||||
/* The V2 signer(s) could not be read from the V2 signature block */
|
||||
public static final int V2_SIG_MALFORMED_SIGNERS = 1;
|
||||
/* A V2 signature block exists without any V2 signers */
|
||||
public static final int V2_SIG_NO_SIGNERS = 2;
|
||||
/* Failed to parse a signer's block in the V2 signature block */
|
||||
public static final int V2_SIG_MALFORMED_SIGNER = 3;
|
||||
/* Failed to parse the signer's signature record in the V2 signature block */
|
||||
public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
|
||||
/* The V2 signer contained no signatures */
|
||||
public static final int V2_SIG_NO_SIGNATURES = 5;
|
||||
/* The V2 signer's certificate could not be parsed */
|
||||
public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
|
||||
/* No signing certificates exist for the V2 signer */
|
||||
public static final int V2_SIG_NO_CERTIFICATES = 7;
|
||||
/* Failed to parse the V2 signer's digest record */
|
||||
public static final int V2_SIG_MALFORMED_DIGEST = 8;
|
||||
/* The V3 signer(s) could not be read from the V3 signature block */
|
||||
public static final int V3_SIG_MALFORMED_SIGNERS = 9;
|
||||
/* A V3 signature block exists without any V3 signers */
|
||||
public static final int V3_SIG_NO_SIGNERS = 10;
|
||||
/* Failed to parse a signer's block in the V3 signature block */
|
||||
public static final int V3_SIG_MALFORMED_SIGNER = 11;
|
||||
/* Failed to parse the signer's signature record in the V3 signature block */
|
||||
public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
|
||||
/* The V3 signer contained no signatures */
|
||||
public static final int V3_SIG_NO_SIGNATURES = 13;
|
||||
/* The V3 signer's certificate could not be parsed */
|
||||
public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
|
||||
/* No signing certificates exist for the V3 signer */
|
||||
public static final int V3_SIG_NO_CERTIFICATES = 15;
|
||||
/* Failed to parse the V3 signer's digest record */
|
||||
public static final int V3_SIG_MALFORMED_DIGEST = 16;
|
||||
/* The source stamp signer contained no signatures */
|
||||
public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
|
||||
/* The source stamp signer's certificate could not be parsed */
|
||||
public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
|
||||
/* The source stamp contains a signature produced using an unknown algorithm */
|
||||
public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
|
||||
/* Failed to parse the signer's signature in the source stamp signature block */
|
||||
public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
|
||||
/* The source stamp's signature block failed verification */
|
||||
public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
|
||||
/* An exception was encountered when verifying the source stamp */
|
||||
public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
|
||||
/* The certificate digest in the APK does not match the expected digest */
|
||||
public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
|
||||
/*
|
||||
* The APK contains a source stamp signature block without a corresponding stamp certificate
|
||||
* digest in the APK contents.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
|
||||
/*
|
||||
* The APK does not contain the source stamp certificate digest file nor the source stamp
|
||||
* signature block.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
|
||||
/*
|
||||
* None of the signatures provided by the source stamp were produced with a known signature
|
||||
* algorithm.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
|
||||
/*
|
||||
* The source stamp signer's certificate in the signing block does not match the certificate in
|
||||
* the APK.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
|
||||
/* The APK could not be properly parsed due to a ZIP or APK format exception */
|
||||
public static final int MALFORMED_APK = 28;
|
||||
/* An unexpected exception was caught when attempting to verify the APK's signatures */
|
||||
public static final int UNEXPECTED_EXCEPTION = 29;
|
||||
/* The APK contains the certificate digest file but does not contain a stamp signature block */
|
||||
public static final int SOURCE_STAMP_SIG_MISSING = 30;
|
||||
/* Source stamp block contains a malformed attribute. */
|
||||
public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
|
||||
/* Source stamp block contains an unknown attribute. */
|
||||
public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
|
||||
/**
|
||||
* Failed to parse the SigningCertificateLineage structure in the source stamp
|
||||
* attributes section.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
|
||||
/**
|
||||
* The source stamp certificate does not match the terminal node in the provided
|
||||
* proof-of-rotation structure describing the stamp certificate history.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
|
||||
/**
|
||||
* The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
|
||||
* with signature(s) that did not verify.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
|
||||
/** No V1 / jar signing signature blocks were found in the APK. */
|
||||
public static final int JAR_SIG_NO_SIGNATURES = 36;
|
||||
/** An exception was encountered when parsing the V1 / jar signer in the signature block. */
|
||||
public static final int JAR_SIG_PARSE_EXCEPTION = 37;
|
||||
/** The source stamp timestamp attribute has an invalid value. */
|
||||
public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38;
|
||||
|
||||
private final int mIssueId;
|
||||
private final String mFormat;
|
||||
private final Object[] mParams;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
|
||||
* {@code params}.
|
||||
*/
|
||||
public ApkVerificationIssue(String format, Object... params) {
|
||||
mIssueId = -1;
|
||||
mFormat = format;
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
|
||||
* params}.
|
||||
*/
|
||||
public ApkVerificationIssue(int issueId, Object... params) {
|
||||
mIssueId = issueId;
|
||||
mFormat = null;
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the numeric ID for this issue.
|
||||
*/
|
||||
public int getIssueId() {
|
||||
return mIssueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional parameters for this issue.
|
||||
*/
|
||||
public Object[] getParams() {
|
||||
return mParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// If this instance was created by a subclass with a format string then return the same
|
||||
// formatted String as the subclass.
|
||||
if (mFormat != null) {
|
||||
return String.format(mFormat, mParams);
|
||||
}
|
||||
StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
|
||||
for (Object param : mParams) {
|
||||
result.append(", ").append(param.toString());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||
import com.android.apksig.internal.apk.v1.V1SchemeConstants;
|
||||
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
|
||||
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
|
||||
|
||||
/**
|
||||
* Exports internally defined constants to allow clients to reference these values without relying
|
||||
* on internal code.
|
||||
*/
|
||||
public class Constants {
|
||||
private Constants() {}
|
||||
|
||||
public static final int VERSION_SOURCE_STAMP = 0;
|
||||
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
|
||||
|
||||
/**
|
||||
* The maximum number of signers supported by the v1 and v2 APK Signature Schemes.
|
||||
*/
|
||||
public static final int MAX_APK_SIGNERS = 10;
|
||||
|
||||
/**
|
||||
* The default page alignment for native library files in bytes.
|
||||
*/
|
||||
public static final short LIBRARY_PAGE_ALIGNMENT_BYTES = 16384;
|
||||
|
||||
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
|
||||
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||
public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
|
||||
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
|
||||
|
||||
public static final int V1_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||
public static final int V2_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.apksig;
|
||||
import java.io.IOException;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class Hints {
|
||||
/**
|
||||
* Name of hint pattern asset file in APK.
|
||||
*/
|
||||
public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
|
||||
|
||||
/**
|
||||
* Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
|
||||
*/
|
||||
public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
|
||||
|
||||
private static int clampToInt(long value) {
|
||||
return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
public static final class ByteRange {
|
||||
final long start;
|
||||
final long end;
|
||||
|
||||
public ByteRange(long start, long end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PatternWithRange {
|
||||
final Pattern pattern;
|
||||
final long offset;
|
||||
final long size;
|
||||
|
||||
public PatternWithRange(String pattern) {
|
||||
this.pattern = Pattern.compile(pattern);
|
||||
this.offset= 0;
|
||||
this.size = Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
public PatternWithRange(String pattern, long offset, long size) {
|
||||
this.pattern = Pattern.compile(pattern);
|
||||
this.offset = offset;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public Matcher matcher(CharSequence input) {
|
||||
return this.pattern.matcher(input);
|
||||
}
|
||||
|
||||
public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
|
||||
if (rangeIn.end - rangeIn.start < this.offset) {
|
||||
return null;
|
||||
}
|
||||
long rangeOutStart = rangeIn.start + this.offset;
|
||||
long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
|
||||
this.size);
|
||||
return new ByteRange(rangeOutStart,
|
||||
rangeOutStart + rangeOutSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a blob of bytes that PinnerService understands as a
|
||||
* sequence of byte ranges to pin.
|
||||
*/
|
||||
public static byte[] encodeByteRangeList(List<ByteRange> pinByteRanges) {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
|
||||
DataOutputStream out = new DataOutputStream(bos);
|
||||
try {
|
||||
for (ByteRange pinByteRange : pinByteRanges) {
|
||||
out.writeInt(clampToInt(pinByteRange.start));
|
||||
out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new AssertionError("impossible", ex);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) {
|
||||
ArrayList<PatternWithRange> pinPatterns = new ArrayList<>();
|
||||
try {
|
||||
for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
|
||||
String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
|
||||
String[] fields = line.split(" ");
|
||||
if (fields.length == 1) {
|
||||
pinPatterns.add(new PatternWithRange(fields[0]));
|
||||
} else if (fields.length == 3) {
|
||||
long start = Long.parseLong(fields[1]);
|
||||
long end = Long.parseLong(fields[2]);
|
||||
pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
|
||||
} else {
|
||||
throw new AssertionError("bad pin pattern line " + line);
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
throw new RuntimeException("UTF-8 must be supported", ex);
|
||||
}
|
||||
return pinPatterns;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# apksig ([commit ac5cbb07d87cc342fcf07715857a812305d69888](https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888))
|
||||
|
||||
apksig is a project which aims to simplify APK signing and checking whether APK signatures are
|
||||
expected to verify on Android. apksig supports
|
||||
[JAR signing](https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File)
|
||||
(used by Android since day one) and
|
||||
[APK Signature Scheme v2](https://source.android.com/security/apksigning/v2.html) (supported since
|
||||
Android Nougat, API Level 24). apksig is meant to be used outside of Android devices.
|
||||
|
||||
The key feature of apksig is that it knows about differences in APK signature verification logic
|
||||
between different versions of the Android platform. apksig thus thoroughly checks whether an APK's
|
||||
signature is expected to verify on all Android platform versions supported by the APK. When signing
|
||||
an APK, apksig chooses the most appropriate cryptographic algorithms based on the Android platform
|
||||
versions supported by the APK being signed.
|
||||
|
||||
## apksig library
|
||||
|
||||
apksig library offers three primitives:
|
||||
|
||||
* `ApkSigner` which signs the provided APK so that it verifies on all Android platform versions
|
||||
supported by the APK. The range of platform versions can be customized.
|
||||
* `ApkVerifier` which checks whether the provided APK is expected to verify on all Android
|
||||
platform versions supported by the APK. The range of platform versions can be customized.
|
||||
* `(Default)ApkSignerEngine` which abstracts away signing APKs from parsing and building APKs.
|
||||
This is useful in optimized APK building pipelines, such as in Android Plugin for Gradle,
|
||||
which need to perform signing while building an APK, instead of after. For simpler use cases
|
||||
where the APK to be signed is available upfront, the `ApkSigner` above is easier to use.
|
||||
|
||||
_NOTE: Some public classes of the library are in packages having the word "internal" in their name.
|
||||
These are not public API of the library. Do not use \*.internal.\* classes directly because these
|
||||
classes may change any time without regard to existing clients outside of `apksig` and `apksigner`._
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,911 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||
import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
|
||||
import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
|
||||
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
|
||||
import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkUtilsLite;
|
||||
import com.android.apksig.internal.apk.ApkSigResult;
|
||||
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.apk.SignatureNotFoundException;
|
||||
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||
import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
|
||||
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
|
||||
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
|
||||
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
||||
import com.android.apksig.internal.zip.LocalFileRecord;
|
||||
import com.android.apksig.internal.zip.ZipUtils;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.DataSources;
|
||||
import com.android.apksig.zip.ZipFormatException;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* APK source stamp verifier intended only to verify the validity of the stamp signature.
|
||||
*
|
||||
* <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
|
||||
* when obtaining the digests for verification. This verifier should only be used in cases where
|
||||
* another mechanism has already been used to verify the APK signatures.
|
||||
*/
|
||||
public class SourceStampVerifier {
|
||||
private final File mApkFile;
|
||||
private final DataSource mApkDataSource;
|
||||
|
||||
private final int mMinSdkVersion;
|
||||
private final int mMaxSdkVersion;
|
||||
|
||||
private SourceStampVerifier(
|
||||
File apkFile,
|
||||
DataSource apkDataSource,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion) {
|
||||
mApkFile = apkFile;
|
||||
mApkDataSource = apkDataSource;
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
mMaxSdkVersion = maxSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the APK's source stamp signature and returns the result of the verification.
|
||||
*
|
||||
* <p>The APK's source stamp can be considered verified if the result's {@link
|
||||
* Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
|
||||
* resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
|
||||
* can be obtained as follows:
|
||||
* <ul>
|
||||
* <li>Obtain the generic errors via {@link Result#getErrors()}
|
||||
* <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
|
||||
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
||||
* <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
|
||||
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
||||
* <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
|
||||
* for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
|
||||
* </ul>
|
||||
*/
|
||||
public SourceStampVerifier.Result verifySourceStamp() {
|
||||
return verifySourceStamp(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
|
||||
* the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
|
||||
* of the verification.
|
||||
*
|
||||
* <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
|
||||
* if present, without verifying the actual source stamp certificate used to sign the source
|
||||
* stamp. This can be used to verify an APK contains a properly signed source stamp without
|
||||
* verifying a particular signer.
|
||||
*
|
||||
* @see #verifySourceStamp()
|
||||
*/
|
||||
public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
|
||||
Closeable in = null;
|
||||
try {
|
||||
DataSource apk;
|
||||
if (mApkDataSource != null) {
|
||||
apk = mApkDataSource;
|
||||
} else if (mApkFile != null) {
|
||||
RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
|
||||
in = f;
|
||||
apk = DataSources.asDataSource(f, 0, f.length());
|
||||
} else {
|
||||
throw new IllegalStateException("APK not provided");
|
||||
}
|
||||
return verifySourceStamp(apk, expectedCertDigest);
|
||||
} catch (IOException e) {
|
||||
Result result = new Result();
|
||||
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
||||
return result;
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided {@code apk}'s source stamp signature, including verification of the
|
||||
* SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
|
||||
* returns the result of the verification.
|
||||
*
|
||||
* @see #verifySourceStamp(String)
|
||||
*/
|
||||
private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
|
||||
String expectedCertDigest) {
|
||||
Result result = new Result();
|
||||
try {
|
||||
ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
|
||||
// Attempt to obtain the source stamp's certificate digest from the APK.
|
||||
List<CentralDirectoryRecord> cdRecords =
|
||||
ZipUtils.parseZipCentralDirectory(apk, zipSections);
|
||||
CentralDirectoryRecord sourceStampCdRecord = null;
|
||||
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||
if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
|
||||
sourceStampCdRecord = cdRecord;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the source stamp's certificate digest is not available within the APK then the
|
||||
// source stamp cannot be verified; check if a source stamp signing block is in the
|
||||
// APK's signature block to determine the appropriate status to return.
|
||||
if (sourceStampCdRecord == null) {
|
||||
boolean stampSigningBlockFound;
|
||||
try {
|
||||
ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
|
||||
stampSigningBlockFound = true;
|
||||
} catch (SignatureNotFoundException e) {
|
||||
stampSigningBlockFound = false;
|
||||
}
|
||||
result.addVerificationError(stampSigningBlockFound
|
||||
? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
|
||||
: ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Verify that the contents of the source stamp certificate digest match the expected
|
||||
// value, if provided.
|
||||
byte[] sourceStampCertificateDigest =
|
||||
LocalFileRecord.getUncompressedData(
|
||||
apk,
|
||||
sourceStampCdRecord,
|
||||
zipSections.getZipCentralDirectoryOffset());
|
||||
if (expectedCertDigest != null) {
|
||||
String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
|
||||
sourceStampCertificateDigest);
|
||||
if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
|
||||
result.addVerificationError(
|
||||
ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
|
||||
actualCertDigest, expectedCertDigest);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
|
||||
new HashMap<>();
|
||||
if (mMaxSdkVersion >= AndroidSdkVersion.P) {
|
||||
SignatureInfo signatureInfo;
|
||||
try {
|
||||
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
signatureInfo = null;
|
||||
}
|
||||
if (signatureInfo != null) {
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
||||
ContentDigestAlgorithm.class);
|
||||
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
|
||||
apkContentDigests, result);
|
||||
signatureSchemeApkContentDigests.put(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
|
||||
}
|
||||
}
|
||||
|
||||
if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
|
||||
signatureSchemeApkContentDigests.isEmpty())) {
|
||||
SignatureInfo signatureInfo;
|
||||
try {
|
||||
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
signatureInfo = null;
|
||||
}
|
||||
if (signatureInfo != null) {
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
||||
ContentDigestAlgorithm.class);
|
||||
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
|
||||
apkContentDigests, result);
|
||||
signatureSchemeApkContentDigests.put(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
|
||||
}
|
||||
}
|
||||
|
||||
if (mMinSdkVersion < AndroidSdkVersion.N
|
||||
|| signatureSchemeApkContentDigests.isEmpty()) {
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
|
||||
getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
|
||||
signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
|
||||
apkContentDigests);
|
||||
}
|
||||
|
||||
ApkSigResult sourceStampResult =
|
||||
V2SourceStampVerifier.verify(
|
||||
apk,
|
||||
zipSections,
|
||||
sourceStampCertificateDigest,
|
||||
signatureSchemeApkContentDigests,
|
||||
mMinSdkVersion,
|
||||
mMaxSdkVersion);
|
||||
result.mergeFrom(sourceStampResult);
|
||||
return result;
|
||||
} catch (ApkFormatException | IOException | ZipFormatException e) {
|
||||
result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
|
||||
* {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
public static void parseSigners(
|
||||
ByteBuffer apkSignatureSchemeBlock,
|
||||
int apkSigSchemeVersion,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
Result result) {
|
||||
boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
// Both the V2 and V3 signature blocks contain the following:
|
||||
// * length-prefixed sequence of length-prefixed signers
|
||||
ByteBuffer signers;
|
||||
try {
|
||||
signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
|
||||
} catch (ApkFormatException e) {
|
||||
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
|
||||
return;
|
||||
}
|
||||
if (!signers.hasRemaining()) {
|
||||
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
|
||||
: ApkVerificationIssue.V3_SIG_NO_SIGNERS);
|
||||
return;
|
||||
}
|
||||
|
||||
CertificateFactory certFactory;
|
||||
try {
|
||||
certFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||
}
|
||||
while (signers.hasRemaining()) {
|
||||
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
||||
if (isV2Block) {
|
||||
result.addV2Signer(signerInfo);
|
||||
} else {
|
||||
result.addV3Signer(signerInfo);
|
||||
}
|
||||
try {
|
||||
ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
|
||||
parseSigner(
|
||||
signer,
|
||||
apkSigSchemeVersion,
|
||||
certFactory,
|
||||
apkContentDigests,
|
||||
signerInfo);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided signer block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
||||
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
|
||||
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
|
||||
* integrity of the APK.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
private static void parseSigner(
|
||||
ByteBuffer signerBlock,
|
||||
int apkSigSchemeVersion,
|
||||
CertificateFactory certFactory,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
Result.SignerInfo signerInfo)
|
||||
throws ApkFormatException {
|
||||
boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
// Both the V2 and V3 signer blocks contain the following:
|
||||
// * length-prefixed signed data
|
||||
// * length-prefixed sequence of length-prefixed digests:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: digest of contents
|
||||
// * length-prefixed sequence of certificates:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||
ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
|
||||
ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
||||
ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
||||
|
||||
// Parse the digests block
|
||||
while (digests.hasRemaining()) {
|
||||
try {
|
||||
ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
|
||||
int sigAlgorithmId = digest.getInt();
|
||||
byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||
if (signatureAlgorithm == null) {
|
||||
continue;
|
||||
}
|
||||
apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the certificates block
|
||||
if (certificates.hasRemaining()) {
|
||||
byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
|
||||
X509Certificate certificate;
|
||||
try {
|
||||
certificate = (X509Certificate) certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(encodedCert));
|
||||
} catch (CertificateException e) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
|
||||
return;
|
||||
}
|
||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||
// form. Without this, getEncoded may return a different form from what was stored in
|
||||
// the signature. This is because some X509Certificate(Factory) implementations
|
||||
// re-encode certificates.
|
||||
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||
signerInfo.setSigningCertificate(certificate);
|
||||
}
|
||||
|
||||
if (signerInfo.getSigningCertificate() == null) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
|
||||
: ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
|
||||
* V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
|
||||
* returned.
|
||||
*
|
||||
* <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
|
||||
* will be updated to include a warning, but the source stamp verification can still proceed.
|
||||
*/
|
||||
private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
|
||||
List<CentralDirectoryRecord> cdRecords,
|
||||
DataSource apk,
|
||||
ZipSections zipSections,
|
||||
Result result)
|
||||
throws IOException, ApkFormatException {
|
||||
CentralDirectoryRecord manifestCdRecord = null;
|
||||
List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
|
||||
Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
|
||||
ContentDigestAlgorithm.class);
|
||||
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||
String cdRecordName = cdRecord.getName();
|
||||
if (cdRecordName == null) {
|
||||
continue;
|
||||
}
|
||||
if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
|
||||
manifestCdRecord = cdRecord;
|
||||
continue;
|
||||
}
|
||||
if (cdRecordName.startsWith("META-INF/")
|
||||
&& (cdRecordName.endsWith(".RSA")
|
||||
|| cdRecordName.endsWith(".DSA")
|
||||
|| cdRecordName.endsWith(".EC"))) {
|
||||
signatureBlockRecords.add(cdRecord);
|
||||
}
|
||||
}
|
||||
if (manifestCdRecord == null) {
|
||||
// No JAR signing manifest file found. For SourceStamp verification, returning an empty
|
||||
// digest is enough since this would affect the final digest signed by the stamp, and
|
||||
// thus an empty digest will invalidate that signature.
|
||||
return v1ContentDigest;
|
||||
}
|
||||
if (signatureBlockRecords.isEmpty()) {
|
||||
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
|
||||
} else {
|
||||
for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
|
||||
signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
|
||||
for (Certificate certificate : certFactory.generateCertificates(
|
||||
new ByteArrayInputStream(signatureBlockBytes))) {
|
||||
// If multiple certificates are found within the signature block only the
|
||||
// first is used as the signer of this block.
|
||||
if (certificate instanceof X509Certificate) {
|
||||
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
||||
signerInfo.setSigningCertificate((X509Certificate) certificate);
|
||||
result.addV1Signer(signerInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
// Log a warning for the parsing exception but still proceed with the stamp
|
||||
// verification.
|
||||
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
|
||||
signatureBlockRecord.getName(), e);
|
||||
break;
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Failed to read APK", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
byte[] manifestBytes =
|
||||
LocalFileRecord.getUncompressedData(
|
||||
apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
|
||||
v1ContentDigest.put(
|
||||
ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
|
||||
return v1ContentDigest;
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Failed to read APK", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of verifying the APK's source stamp signature; this signature can only be considered
|
||||
* verified if {@link #isVerified()} returns true.
|
||||
*/
|
||||
public static class Result {
|
||||
private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
|
||||
private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
|
||||
private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
|
||||
private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
|
||||
mV2SchemeSigners, mV3SchemeSigners);
|
||||
private SourceStampInfo mSourceStampInfo;
|
||||
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
|
||||
private boolean mVerified;
|
||||
|
||||
void addVerificationError(int errorId, Object... params) {
|
||||
mErrors.add(new ApkVerificationIssue(errorId, params));
|
||||
}
|
||||
|
||||
void addVerificationWarning(int warningId, Object... params) {
|
||||
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
||||
}
|
||||
|
||||
private void addV1Signer(SignerInfo signerInfo) {
|
||||
mV1SchemeSigners.add(signerInfo);
|
||||
}
|
||||
|
||||
private void addV2Signer(SignerInfo signerInfo) {
|
||||
mV2SchemeSigners.add(signerInfo);
|
||||
}
|
||||
|
||||
private void addV3Signer(SignerInfo signerInfo) {
|
||||
mV3SchemeSigners.add(signerInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the APK's source stamp signature
|
||||
*/
|
||||
public boolean isVerified() {
|
||||
return mVerified;
|
||||
}
|
||||
|
||||
private void mergeFrom(ApkSigResult source) {
|
||||
switch (source.signatureSchemeVersion) {
|
||||
case Constants.VERSION_SOURCE_STAMP:
|
||||
mVerified = source.verified;
|
||||
if (!source.mSigners.isEmpty()) {
|
||||
mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown ApkSigResult Signing Block Scheme Id "
|
||||
+ source.signatureSchemeVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
|
||||
* provided APK.
|
||||
*/
|
||||
public List<SignerInfo> getV1SchemeSigners() {
|
||||
return mV1SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
|
||||
* provided APK.
|
||||
*/
|
||||
public List<SignerInfo> getV2SchemeSigners() {
|
||||
return mV2SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
|
||||
* provided APK.
|
||||
*/
|
||||
public List<SignerInfo> getV3SchemeSigners() {
|
||||
return mV3SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
|
||||
* APK, or null if the source stamp signature verification failed before the stamp signature
|
||||
* block could be fully parsed.
|
||||
*/
|
||||
public SourceStampInfo getSourceStampInfo() {
|
||||
return mSourceStampInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if an error was encountered while verifying the APK.
|
||||
*
|
||||
* <p>Any error prevents the APK from being considered verified.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
if (!mErrors.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||
for (SignerInfo signer : signers) {
|
||||
if (signer.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mSourceStampInfo != null) {
|
||||
if (mSourceStampInfo.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errors encountered while verifying the APK's source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warnings encountered while verifying the APK's source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all errors for this result, including any errors from signature scheme signers
|
||||
* and the source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getAllErrors() {
|
||||
List<ApkVerificationIssue> errors = new ArrayList<>();
|
||||
errors.addAll(mErrors);
|
||||
|
||||
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||
for (SignerInfo signer : signers) {
|
||||
errors.addAll(signer.getErrors());
|
||||
}
|
||||
}
|
||||
if (mSourceStampInfo != null) {
|
||||
errors.addAll(mSourceStampInfo.getErrors());
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all warnings for this result, including any warnings from signature scheme
|
||||
* signers and the source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getAllWarnings() {
|
||||
List<ApkVerificationIssue> warnings = new ArrayList<>();
|
||||
warnings.addAll(mWarnings);
|
||||
|
||||
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||
for (SignerInfo signer : signers) {
|
||||
warnings.addAll(signer.getWarnings());
|
||||
}
|
||||
}
|
||||
if (mSourceStampInfo != null) {
|
||||
warnings.addAll(mSourceStampInfo.getWarnings());
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains information about an APK's signer and any errors encountered while parsing the
|
||||
* corresponding signature block.
|
||||
*/
|
||||
public static class SignerInfo {
|
||||
private X509Certificate mSigningCertificate;
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
|
||||
void setSigningCertificate(X509Certificate signingCertificate) {
|
||||
mSigningCertificate = signingCertificate;
|
||||
}
|
||||
|
||||
void addVerificationError(int errorId, Object... params) {
|
||||
mErrors.add(new ApkVerificationIssue(errorId, params));
|
||||
}
|
||||
|
||||
void addVerificationWarning(int warningId, Object... params) {
|
||||
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current signing certificate used by this signer.
|
||||
*/
|
||||
public X509Certificate getSigningCertificate() {
|
||||
return mSigningCertificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
|
||||
* encountered during processing of this signer's signature block.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
|
||||
* encountered during processing of this signer's signature block.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any errors were encountered while parsing this signer's
|
||||
* signature block.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains information about an APK's source stamp and any errors encountered while
|
||||
* parsing the stamp signature block.
|
||||
*/
|
||||
public static class SourceStampInfo {
|
||||
private final List<X509Certificate> mCertificates;
|
||||
private final List<X509Certificate> mCertificateLineage;
|
||||
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
|
||||
|
||||
private final long mTimestamp;
|
||||
|
||||
/*
|
||||
* Since this utility is intended just to verify the source stamp, and the source stamp
|
||||
* currently only logs warnings to prevent failing the APK signature verification, treat
|
||||
* all warnings as errors. If the stamp verification is updated to log errors this
|
||||
* should be set to false to ensure only errors trigger a failure verifying the source
|
||||
* stamp.
|
||||
*/
|
||||
private static final boolean mWarningsAsErrors = true;
|
||||
|
||||
private SourceStampInfo(ApkSignerInfo result) {
|
||||
mCertificates = result.certs;
|
||||
mCertificateLineage = result.certificateLineage;
|
||||
mErrors.addAll(result.getErrors());
|
||||
mWarnings.addAll(result.getWarnings());
|
||||
mInfoMessages.addAll(result.getInfoMessages());
|
||||
mTimestamp = result.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SourceStamp's signing certificate or {@code null} if not available. The
|
||||
* certificate is guaranteed to be available if no errors were encountered during
|
||||
* verification (see {@link #containsErrors()}.
|
||||
*
|
||||
* <p>This certificate contains the SourceStamp's public key.
|
||||
*/
|
||||
public X509Certificate getCertificate() {
|
||||
return mCertificates.isEmpty() ? null : mCertificates.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link X509Certificate} instances representing the source
|
||||
* stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
|
||||
* if the stamp's signing certificate has not been rotated.
|
||||
*/
|
||||
public List<X509Certificate> getCertificatesInLineage() {
|
||||
return mCertificateLineage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any errors were encountered during the source stamp verification.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any info messages were encountered during verification of
|
||||
* this source stamp.
|
||||
*/
|
||||
public boolean containsInfoMessages() {
|
||||
return !mInfoMessages.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
|
||||
* encountered during source stamp verification.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getErrors() {
|
||||
if (!mWarningsAsErrors) {
|
||||
return mErrors;
|
||||
}
|
||||
List<ApkVerificationIssue> result = new ArrayList<>();
|
||||
result.addAll(mErrors);
|
||||
result.addAll(mWarnings);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
|
||||
* were encountered during source stamp verification.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link ApkVerificationIssue} representing info messages
|
||||
* that were encountered during source stamp verification.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getInfoMessages() {
|
||||
return mInfoMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the epoch timestamp in seconds representing the time this source stamp block
|
||||
* was signed, or 0 if the timestamp is not available.
|
||||
*/
|
||||
public long getTimestampEpochSeconds() {
|
||||
return mTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder of {@link SourceStampVerifier} instances.
|
||||
*
|
||||
* <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
|
||||
* verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
|
||||
* queried to determine the APK's minimum supported level, so the caller should specify a lower
|
||||
* bound with {@link #setMinCheckedPlatformVersion(int)}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final File mApkFile;
|
||||
private final DataSource mApkDataSource;
|
||||
|
||||
private int mMinSdkVersion = 1;
|
||||
private int mMaxSdkVersion = Integer.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
|
||||
* apk}.
|
||||
*/
|
||||
public Builder(File apk) {
|
||||
if (apk == null) {
|
||||
throw new NullPointerException("apk == null");
|
||||
}
|
||||
mApkFile = apk;
|
||||
mApkDataSource = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
|
||||
* apk}.
|
||||
*/
|
||||
public Builder(DataSource apk) {
|
||||
if (apk == null) {
|
||||
throw new NullPointerException("apk == null");
|
||||
}
|
||||
mApkDataSource = apk;
|
||||
mApkFile = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the oldest Android platform version for which the APK's source stamp is verified.
|
||||
*
|
||||
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
||||
* on all Android platforms starting from the platform version with the provided {@code
|
||||
* minSdkVersion}. The upper end of the platform versions range can be modified via
|
||||
* {@link #setMaxCheckedPlatformVersion(int)}.
|
||||
*
|
||||
* @param minSdkVersion API Level of the oldest platform for which to verify the APK
|
||||
*/
|
||||
public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the newest Android platform version for which the APK's source stamp is verified.
|
||||
*
|
||||
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
||||
* on all platform versions up to and including the proviced {@code maxSdkVersion}. The
|
||||
* lower end of the platform versions range can be modified via {@link
|
||||
* #setMinCheckedPlatformVersion(int)}.
|
||||
*
|
||||
* @param maxSdkVersion API Level of the newest platform for which to verify the APK
|
||||
* @see #setMinCheckedPlatformVersion(int)
|
||||
*/
|
||||
public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
|
||||
mMaxSdkVersion = maxSdkVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link SourceStampVerifier} initialized according to the configuration of this
|
||||
* builder.
|
||||
*/
|
||||
public SourceStampVerifier build() {
|
||||
return new SourceStampVerifier(
|
||||
mApkFile,
|
||||
mApkDataSource,
|
||||
mMinSdkVersion,
|
||||
mMaxSdkVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a
|
||||
* well-formed ZIP archive, in which case {@link #getCause()} will return a
|
||||
* {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains
|
||||
* multiple ZIP entries with the same name.
|
||||
*/
|
||||
public class ApkFormatException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ApkFormatException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ApkFormatException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that no APK Signing Block was found in an APK.
|
||||
*/
|
||||
public class ApkSigningBlockNotFoundException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ApkSigningBlockNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ApkSigningBlockNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,670 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
import com.android.apksig.internal.apk.AndroidBinXmlParser;
|
||||
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
||||
import com.android.apksig.internal.zip.LocalFileRecord;
|
||||
import com.android.apksig.internal.zip.ZipUtils;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipFormatException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* APK utilities.
|
||||
*/
|
||||
public abstract class ApkUtils {
|
||||
|
||||
/**
|
||||
* Name of the Android manifest ZIP entry in APKs.
|
||||
*/
|
||||
public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
|
||||
|
||||
/** Name of the SourceStamp certificate hash ZIP entry in APKs. */
|
||||
public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME =
|
||||
SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
|
||||
|
||||
private ApkUtils() {}
|
||||
|
||||
/**
|
||||
* Finds the main ZIP sections of the provided APK.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred while reading the APK
|
||||
* @throws ZipFormatException if the APK is malformed
|
||||
*/
|
||||
public static ZipSections findZipSections(DataSource apk)
|
||||
throws IOException, ZipFormatException {
|
||||
com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
|
||||
return new ZipSections(
|
||||
zipSections.getZipCentralDirectoryOffset(),
|
||||
zipSections.getZipCentralDirectorySizeBytes(),
|
||||
zipSections.getZipCentralDirectoryRecordCount(),
|
||||
zipSections.getZipEndOfCentralDirectoryOffset(),
|
||||
zipSections.getZipEndOfCentralDirectory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the ZIP sections of an APK.
|
||||
*/
|
||||
public static class ZipSections extends com.android.apksig.zip.ZipSections {
|
||||
public ZipSections(
|
||||
long centralDirectoryOffset,
|
||||
long centralDirectorySizeBytes,
|
||||
int centralDirectoryRecordCount,
|
||||
long eocdOffset,
|
||||
ByteBuffer eocd) {
|
||||
super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
|
||||
eocdOffset, eocd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
|
||||
* Directory record.
|
||||
*
|
||||
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
|
||||
* @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
|
||||
* be between {@code 0} and {@code 2^32 - 1} inclusive.
|
||||
*/
|
||||
public static void setZipEocdCentralDirectoryOffset(
|
||||
ByteBuffer zipEndOfCentralDirectory, long offset) {
|
||||
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
|
||||
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the length of EOCD comment.
|
||||
*
|
||||
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
|
||||
*/
|
||||
public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
|
||||
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
|
||||
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||
ZipUtils.updateZipEocdCommentLen(eocd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the APK Signing Block of the provided {@code apk}.
|
||||
*
|
||||
* @throws ApkFormatException if the APK is not a valid ZIP archive
|
||||
* @throws IOException if an I/O error occurs
|
||||
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
|
||||
* </a>
|
||||
*/
|
||||
public static ApkSigningBlock findApkSigningBlock(DataSource apk)
|
||||
throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
|
||||
ApkUtils.ZipSections inputZipSections;
|
||||
try {
|
||||
inputZipSections = ApkUtils.findZipSections(apk);
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
|
||||
}
|
||||
return findApkSigningBlock(apk, inputZipSections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the APK Signing Block of the provided APK.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
|
||||
* </a>
|
||||
*/
|
||||
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
|
||||
throws IOException, ApkSigningBlockNotFoundException {
|
||||
ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
|
||||
zipSections);
|
||||
return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the location of the APK Signing Block inside an APK.
|
||||
*/
|
||||
public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
|
||||
/**
|
||||
* Constructs a new {@code ApkSigningBlock}.
|
||||
*
|
||||
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
|
||||
* Signing Block inside the APK file
|
||||
* @param contents contents of the APK Signing Block
|
||||
*/
|
||||
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
|
||||
super(startOffsetInApk, contents);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the APK's {@code AndroidManifest.xml}.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the APK
|
||||
* @throws ApkFormatException if the APK is malformed
|
||||
*/
|
||||
public static ByteBuffer getAndroidManifest(DataSource apk)
|
||||
throws IOException, ApkFormatException {
|
||||
ZipSections zipSections;
|
||||
try {
|
||||
zipSections = findZipSections(apk);
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Not a valid ZIP archive", e);
|
||||
}
|
||||
List<CentralDirectoryRecord> cdRecords =
|
||||
V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
|
||||
CentralDirectoryRecord androidManifestCdRecord = null;
|
||||
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||
if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
|
||||
androidManifestCdRecord = cdRecord;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (androidManifestCdRecord == null) {
|
||||
throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
|
||||
}
|
||||
DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset());
|
||||
|
||||
try {
|
||||
return ByteBuffer.wrap(
|
||||
LocalFileRecord.getUncompressedData(
|
||||
lfhSection, androidManifestCdRecord, lfhSection.size()));
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml.
|
||||
*/
|
||||
private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml.
|
||||
*/
|
||||
private static final int DEBUGGABLE_ATTR_ID = 0x0101000f;
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:targetSandboxVersion} attribute in
|
||||
* AndroidManifest.xml.
|
||||
*/
|
||||
private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:targetSdkVersion} attribute in
|
||||
* AndroidManifest.xml.
|
||||
*/
|
||||
private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
|
||||
private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
|
||||
*/
|
||||
private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
|
||||
private static final String MANIFEST_ELEMENT_TAG = "manifest";
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
|
||||
*/
|
||||
private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;
|
||||
|
||||
/**
|
||||
* Returns the lowest Android platform version (API Level) supported by an APK with the
|
||||
* provided {@code AndroidManifest.xml}.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*
|
||||
* @throws MinSdkVersionException if an error occurred while determining the API Level
|
||||
*/
|
||||
public static int getMinSdkVersionFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) throws MinSdkVersionException {
|
||||
// IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using
|
||||
// uses-sdk elements which are children of the top-level manifest element. uses-sdk element
|
||||
// declares the minimum supported platform version using the android:minSdkVersion attribute
|
||||
// whose default value is 1.
|
||||
// For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion
|
||||
// is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the
|
||||
// effective minSdkVersion value is the maximum over the encountered minSdkVersion values.
|
||||
|
||||
try {
|
||||
// If no uses-sdk elements are encountered, Android accepts the APK. We treat this
|
||||
// scenario as though the minimum supported API Level is 1.
|
||||
int result = 1;
|
||||
|
||||
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||
&& (parser.getDepth() == 2)
|
||||
&& ("uses-sdk".equals(parser.getName()))
|
||||
&& (parser.getNamespace().isEmpty())) {
|
||||
// In each uses-sdk element, minSdkVersion defaults to 1
|
||||
int minSdkVersion = 1;
|
||||
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||
if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) {
|
||||
int valueType = parser.getAttributeValueType(i);
|
||||
switch (valueType) {
|
||||
case AndroidBinXmlParser.VALUE_TYPE_INT:
|
||||
minSdkVersion = parser.getAttributeIntValue(i);
|
||||
break;
|
||||
case AndroidBinXmlParser.VALUE_TYPE_STRING:
|
||||
minSdkVersion =
|
||||
getMinSdkVersionForCodename(
|
||||
parser.getAttributeStringValue(i));
|
||||
break;
|
||||
default:
|
||||
throw new MinSdkVersionException(
|
||||
"Unable to determine APK's minimum supported Android"
|
||||
+ ": unsupported value type in "
|
||||
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
|
||||
+ " minSdkVersion"
|
||||
+ ". Only integer values supported.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
result = Math.max(result, minSdkVersion);
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||
throw new MinSdkVersionException(
|
||||
"Unable to determine APK's minimum supported Android platform version"
|
||||
+ ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CodenamesLazyInitializer {
|
||||
|
||||
/**
|
||||
* List of platform codename (first letter of) to API Level mappings. The list must be
|
||||
* sorted by the first letter. For codenames not in the list, the assumption is that the API
|
||||
* Level is incremented by one for every increase in the codename's first letter.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL =
|
||||
new Pair[] {
|
||||
Pair.of('C', 2),
|
||||
Pair.of('D', 3),
|
||||
Pair.of('E', 4),
|
||||
Pair.of('F', 7),
|
||||
Pair.of('G', 8),
|
||||
Pair.of('H', 10),
|
||||
Pair.of('I', 13),
|
||||
Pair.of('J', 15),
|
||||
Pair.of('K', 18),
|
||||
Pair.of('L', 20),
|
||||
Pair.of('M', 22),
|
||||
Pair.of('N', 23),
|
||||
Pair.of('O', 25),
|
||||
};
|
||||
|
||||
private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR =
|
||||
new ByFirstComparator();
|
||||
|
||||
private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> {
|
||||
@Override
|
||||
public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) {
|
||||
char c1 = o1.getFirst();
|
||||
char c2 = o2.getFirst();
|
||||
return c1 - c2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the API Level corresponding to the provided platform codename.
|
||||
*
|
||||
* <p>This method is pessimistic. It returns a value one lower than the API Level with which the
|
||||
* platform is actually released (e.g., 23 for N which was released as API Level 24). This is
|
||||
* because new features which first appear in an API Level are not available in the early days
|
||||
* of that platform version's existence, when the platform only has a codename. Moreover, this
|
||||
* method currently doesn't differentiate between initial and MR releases, meaning API Level
|
||||
* returned for MR releases may be more than one lower than the API Level with which the
|
||||
* platform version is actually released.
|
||||
*
|
||||
* @throws CodenameMinSdkVersionException if the {@code codename} is not supported
|
||||
*/
|
||||
static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException {
|
||||
char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0);
|
||||
// Codenames are case-sensitive. Only codenames starting with A-Z are supported for now.
|
||||
// We only look at the first letter of the codename as this is the most important letter.
|
||||
if ((firstChar >= 'A') && (firstChar <= 'Z')) {
|
||||
Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel =
|
||||
CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL;
|
||||
int searchResult =
|
||||
Arrays.binarySearch(
|
||||
sortedCodenamesFirstCharToApiLevel,
|
||||
Pair.of(firstChar, null), // second element of the pair is ignored here
|
||||
CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR);
|
||||
if (searchResult >= 0) {
|
||||
// Exact match -- searchResult is the index of the matching element
|
||||
return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond();
|
||||
}
|
||||
// Not an exact match -- searchResult is negative and is -(insertion index) - 1.
|
||||
// The element at insertionIndex - 1 (if present) is smaller than firstChar and the
|
||||
// element at insertionIndex (if present) is greater than firstChar.
|
||||
int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length]
|
||||
if (insertionIndex == 0) {
|
||||
// 'A' or 'B' -- never released to public
|
||||
return 1;
|
||||
} else {
|
||||
// The element at insertionIndex - 1 is the newest older codename.
|
||||
// API Level bumped by at least 1 for every change in the first letter of codename
|
||||
Pair<Character, Integer> newestOlderCodenameMapping =
|
||||
sortedCodenamesFirstCharToApiLevel[insertionIndex - 1];
|
||||
char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst();
|
||||
int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond();
|
||||
return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar);
|
||||
}
|
||||
}
|
||||
|
||||
throw new CodenameMinSdkVersionException(
|
||||
"Unable to determine APK's minimum supported Android platform version"
|
||||
+ " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME
|
||||
+ "'s minSdkVersion: \"" + codename + "\"",
|
||||
codename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}.
|
||||
* See the {@code android:debuggable} attribute of the {@code application} element.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*
|
||||
* @throws ApkFormatException if the manifest is malformed
|
||||
*/
|
||||
public static boolean getDebuggableFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) throws ApkFormatException {
|
||||
// IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first
|
||||
// "application" element which is a child of the top-level manifest element. The debuggable
|
||||
// attribute of this application element is coerced to a boolean value. If there is no
|
||||
// application element or if it doesn't declare the debuggable attribute, the package is
|
||||
// considered not debuggable.
|
||||
|
||||
try {
|
||||
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||
&& (parser.getDepth() == 2)
|
||||
&& ("application".equals(parser.getName()))
|
||||
&& (parser.getNamespace().isEmpty())) {
|
||||
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||
if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) {
|
||||
int valueType = parser.getAttributeValueType(i);
|
||||
switch (valueType) {
|
||||
case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN:
|
||||
case AndroidBinXmlParser.VALUE_TYPE_STRING:
|
||||
case AndroidBinXmlParser.VALUE_TYPE_INT:
|
||||
String value = parser.getAttributeStringValue(i);
|
||||
return ("true".equals(value))
|
||||
|| ("TRUE".equals(value))
|
||||
|| ("1".equals(value));
|
||||
case AndroidBinXmlParser.VALUE_TYPE_REFERENCE:
|
||||
// References to resources are not supported on purpose. The
|
||||
// reason is that the resolved value depends on the resource
|
||||
// configuration (e.g, MNC/MCC, locale, screen density) used
|
||||
// at resolution time. As a result, the same APK may appear as
|
||||
// debuggable in one situation and as non-debuggable in another
|
||||
// situation. Such APKs may put users at risk.
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine whether APK is debuggable"
|
||||
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
|
||||
+ " android:debuggable attribute references a"
|
||||
+ " resource. References are not supported for"
|
||||
+ " security reasons. Only constant boolean,"
|
||||
+ " string and int values are supported.");
|
||||
default:
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine whether APK is debuggable"
|
||||
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
|
||||
+ " android:debuggable attribute uses"
|
||||
+ " unsupported value type. Only boolean,"
|
||||
+ " string and int values are supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
// This application element does not declare the debuggable attribute
|
||||
return false;
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
|
||||
// No application element found
|
||||
return false;
|
||||
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine whether APK is debuggable: malformed binary resource: "
|
||||
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the package name of the APK according to its {@code AndroidManifest.xml} or
|
||||
* {@code null} if package name is not declared. See the {@code package} attribute of the
|
||||
* {@code manifest} element.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*
|
||||
* @throws ApkFormatException if the manifest is malformed
|
||||
*/
|
||||
public static String getPackageNameFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) throws ApkFormatException {
|
||||
// IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level
|
||||
// manifest element. Interestingly, as opposed to most other attributes, Android Package
|
||||
// Manager looks up this attribute by its name rather than by its resource ID.
|
||||
|
||||
try {
|
||||
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||
&& (parser.getDepth() == 1)
|
||||
&& ("manifest".equals(parser.getName()))
|
||||
&& (parser.getNamespace().isEmpty())) {
|
||||
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||
if ("package".equals(parser.getAttributeName(i))
|
||||
&& (parser.getNamespace().isEmpty())) {
|
||||
return parser.getAttributeStringValue(i);
|
||||
}
|
||||
}
|
||||
// No "package" attribute found
|
||||
return null;
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
|
||||
// No manifest element found
|
||||
return null;
|
||||
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine APK package name: malformed binary resource: "
|
||||
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the security sandbox version targeted by an APK with the provided
|
||||
* {@code AndroidManifest.xml}.
|
||||
*
|
||||
* <p>If the security sandbox version is not specified in the manifest a default value of 1 is
|
||||
* returned.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*/
|
||||
public static int getTargetSandboxVersionFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) {
|
||||
try {
|
||||
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||
MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
|
||||
} catch (ApkFormatException e) {
|
||||
// An ApkFormatException indicates the target sandbox is not specified in the manifest;
|
||||
// return a default value of 1.
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
|
||||
*
|
||||
* <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
|
||||
* value is specified then a value of 1 is returned.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*/
|
||||
public static int getTargetSdkVersionFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) {
|
||||
// If the targetSdkVersion is not specified then the platform will use the value of the
|
||||
// minSdkVersion; if neither is specified then the platform will use a value of 1.
|
||||
int minSdkVersion = 1;
|
||||
try {
|
||||
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||
USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
|
||||
} catch (ApkFormatException e) {
|
||||
// Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
|
||||
// element is not specified at all.
|
||||
}
|
||||
androidManifestContents.rewind();
|
||||
try {
|
||||
minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
|
||||
} catch (ApkFormatException e) {
|
||||
// Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
|
||||
// the uses-sdk element is not specified at all.
|
||||
}
|
||||
return minSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
|
||||
*
|
||||
* <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
|
||||
* integer an ApkFormatException is thrown.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
* @throws ApkFormatException if an error occurred while determining the versionCode, or if the
|
||||
* versionCode attribute value is not available.
|
||||
*/
|
||||
public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
|
||||
throws ApkFormatException {
|
||||
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||
MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the versionCode and versionCodeMajor of the APK according to its {@code
|
||||
* AndroidManifest.xml} combined together as a single long value.
|
||||
*
|
||||
* <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
|
||||
* 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
* @throws ApkFormatException if an error occurred while determining the version, or if the
|
||||
* versionCode attribute value is not available.
|
||||
*/
|
||||
public static long getLongVersionCodeFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) throws ApkFormatException {
|
||||
// If the versionCode is not found then allow the ApkFormatException to be thrown to notify
|
||||
// the caller that the versionCode is not available.
|
||||
int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
|
||||
long versionCodeMajor = 0;
|
||||
try {
|
||||
androidManifestContents.rewind();
|
||||
versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||
MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
|
||||
} catch (ApkFormatException e) {
|
||||
// This is expected if the versionCodeMajor has not been defined for the APK; in this
|
||||
// case the return value is just the versionCode.
|
||||
}
|
||||
return (versionCodeMajor << 32) | versionCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the requested {@code attributeId} in the specified {@code
|
||||
* elementName} from the provided {@code androidManifestContents} in binary Android resource
|
||||
* format.
|
||||
*
|
||||
* @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
|
||||
* if the requested attribute is not found.
|
||||
*/
|
||||
private static int getAttributeValueFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents, String elementName, int attributeId)
|
||||
throws ApkFormatException {
|
||||
if (elementName == null) {
|
||||
throw new NullPointerException("elementName cannot be null");
|
||||
}
|
||||
try {
|
||||
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||
&& (elementName.equals(parser.getName()))) {
|
||||
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||
if (parser.getAttributeNameResourceId(i) == attributeId) {
|
||||
int valueType = parser.getAttributeValueType(i);
|
||||
switch (valueType) {
|
||||
case AndroidBinXmlParser.VALUE_TYPE_INT:
|
||||
case AndroidBinXmlParser.VALUE_TYPE_STRING:
|
||||
return parser.getAttributeIntValue(i);
|
||||
default:
|
||||
throw new ApkFormatException(
|
||||
"Unsupported value type, " + valueType
|
||||
+ ", for attribute " + String.format("0x%08X",
|
||||
attributeId) + " under element " + elementName);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
throw new ApkFormatException(
|
||||
"Failed to determine APK's " + elementName + " attribute "
|
||||
+ String.format("0x%08X", attributeId) + " value");
|
||||
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine value for attribute " + String.format("0x%08X",
|
||||
attributeId) + " under element " + elementName
|
||||
+ "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] computeSha256DigestBytes(byte[] data) {
|
||||
return ApkUtilsLite.computeSha256DigestBytes(data);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.internal.zip.ZipUtils;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipFormatException;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Lightweight version of the ApkUtils for clients that only require a subset of the utility
|
||||
* functionality.
|
||||
*/
|
||||
public class ApkUtilsLite {
|
||||
private ApkUtilsLite() {}
|
||||
|
||||
/**
|
||||
* Finds the main ZIP sections of the provided APK.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred while reading the APK
|
||||
* @throws ZipFormatException if the APK is malformed
|
||||
*/
|
||||
public static ZipSections findZipSections(DataSource apk)
|
||||
throws IOException, ZipFormatException {
|
||||
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
|
||||
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
|
||||
if (eocdAndOffsetInFile == null) {
|
||||
throw new ZipFormatException("ZIP End of Central Directory record not found");
|
||||
}
|
||||
|
||||
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
|
||||
long eocdOffset = eocdAndOffsetInFile.getSecond();
|
||||
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
|
||||
if (cdStartOffset > eocdOffset) {
|
||||
throw new ZipFormatException(
|
||||
"ZIP Central Directory start offset out of range: " + cdStartOffset
|
||||
+ ". ZIP End of Central Directory offset: " + eocdOffset);
|
||||
}
|
||||
|
||||
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
|
||||
long cdEndOffset = cdStartOffset + cdSizeBytes;
|
||||
if (cdEndOffset > eocdOffset) {
|
||||
throw new ZipFormatException(
|
||||
"ZIP Central Directory overlaps with End of Central Directory"
|
||||
+ ". CD end: " + cdEndOffset
|
||||
+ ", EoCD start: " + eocdOffset);
|
||||
}
|
||||
|
||||
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
|
||||
|
||||
return new ZipSections(
|
||||
cdStartOffset,
|
||||
cdSizeBytes,
|
||||
cdRecordCount,
|
||||
eocdOffset,
|
||||
eocdBuf);
|
||||
}
|
||||
|
||||
// See https://source.android.com/security/apksigning/v2.html
|
||||
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
|
||||
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
|
||||
private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
|
||||
|
||||
/**
|
||||
* Returns the APK Signing Block of the provided APK.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
|
||||
* </a>
|
||||
*/
|
||||
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
|
||||
throws IOException, ApkSigningBlockNotFoundException {
|
||||
// FORMAT (see https://source.android.com/security/apksigning/v2.html):
|
||||
// OFFSET DATA TYPE DESCRIPTION
|
||||
// * @+0 bytes uint64: size in bytes (excluding this field)
|
||||
// * @+8 bytes payload
|
||||
// * @-24 bytes uint64: size in bytes (same as the one above)
|
||||
// * @-16 bytes uint128: magic
|
||||
|
||||
long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
|
||||
long centralDirEndOffset =
|
||||
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
|
||||
long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
|
||||
if (centralDirEndOffset != eocdStartOffset) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"ZIP Central Directory is not immediately followed by End of Central Directory"
|
||||
+ ". CD end: " + centralDirEndOffset
|
||||
+ ", EoCD start: " + eocdStartOffset);
|
||||
}
|
||||
|
||||
if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"APK too small for APK Signing Block. ZIP Central Directory offset: "
|
||||
+ centralDirStartOffset);
|
||||
}
|
||||
// Read the magic and offset in file from the footer section of the block:
|
||||
// * uint64: size of block
|
||||
// * 16 bytes: magic
|
||||
ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
|
||||
footer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|
||||
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"No APK Signing Block before ZIP Central Directory");
|
||||
}
|
||||
// Read and compare size fields
|
||||
long apkSigBlockSizeInFooter = footer.getLong(0);
|
||||
if ((apkSigBlockSizeInFooter < footer.capacity())
|
||||
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
|
||||
}
|
||||
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
|
||||
long apkSigBlockOffset = centralDirStartOffset - totalSize;
|
||||
if (apkSigBlockOffset < 0) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"APK Signing Block offset out of range: " + apkSigBlockOffset);
|
||||
}
|
||||
ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
|
||||
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
|
||||
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"APK Signing Block sizes in header and footer do not match: "
|
||||
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
|
||||
}
|
||||
return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the location of the APK Signing Block inside an APK.
|
||||
*/
|
||||
public static class ApkSigningBlock {
|
||||
private final long mStartOffsetInApk;
|
||||
private final DataSource mContents;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ApkSigningBlock}.
|
||||
*
|
||||
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
|
||||
* Signing Block inside the APK file
|
||||
* @param contents contents of the APK Signing Block
|
||||
*/
|
||||
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
|
||||
mStartOffsetInApk = startOffsetInApk;
|
||||
mContents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
|
||||
*/
|
||||
public long getStartOffset() {
|
||||
return mStartOffsetInApk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data source which provides the full contents of the APK Signing Block,
|
||||
* including its footer.
|
||||
*/
|
||||
public DataSource getContents() {
|
||||
return mContents;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] computeSha256DigestBytes(byte[] data) {
|
||||
MessageDigest messageDigest;
|
||||
try {
|
||||
messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 is not found", e);
|
||||
}
|
||||
messageDigest.update(data);
|
||||
return messageDigest.digest();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||
* an APK because the version is specified as a codename, rather than as API Level number, and the
|
||||
* codename is in an unexpected format.
|
||||
*/
|
||||
public class CodenameMinSdkVersionException extends MinSdkVersionException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** Encountered codename. */
|
||||
private final String mCodename;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code MinSdkVersionCodenameException} with the provided message and
|
||||
* codename.
|
||||
*/
|
||||
public CodenameMinSdkVersionException(String message, String codename) {
|
||||
super(message);
|
||||
mCodename = codename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the codename.
|
||||
*/
|
||||
public String getCodename() {
|
||||
return mCodename;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||
* an APK.
|
||||
*/
|
||||
public class MinSdkVersionException extends ApkFormatException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code MinSdkVersionException} with the provided message.
|
||||
*/
|
||||
public MinSdkVersionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code MinSdkVersionException} with the provided message and cause.
|
||||
*/
|
||||
public MinSdkVersionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,869 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
|
||||
*
|
||||
* <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
|
||||
* {@link #getEventType()} and {@link #next()} methods. Additional information about the current
|
||||
* event can be obtained via an assortment of getters, for example, {@link #getName()} or
|
||||
* {@link #getAttributeNameResourceId(int)}.
|
||||
*/
|
||||
public class AndroidBinXmlParser {
|
||||
|
||||
/** Event: start of document. */
|
||||
public static final int EVENT_START_DOCUMENT = 1;
|
||||
|
||||
/** Event: end of document. */
|
||||
public static final int EVENT_END_DOCUMENT = 2;
|
||||
|
||||
/** Event: start of an element. */
|
||||
public static final int EVENT_START_ELEMENT = 3;
|
||||
|
||||
/** Event: end of an document. */
|
||||
public static final int EVENT_END_ELEMENT = 4;
|
||||
|
||||
/** Attribute value type is not supported by this parser. */
|
||||
public static final int VALUE_TYPE_UNSUPPORTED = 0;
|
||||
|
||||
/** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
|
||||
public static final int VALUE_TYPE_STRING = 1;
|
||||
|
||||
/** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
|
||||
public static final int VALUE_TYPE_INT = 2;
|
||||
|
||||
/**
|
||||
* Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
|
||||
*/
|
||||
public static final int VALUE_TYPE_REFERENCE = 3;
|
||||
|
||||
/** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
|
||||
public static final int VALUE_TYPE_BOOLEAN = 4;
|
||||
|
||||
private static final long NO_NAMESPACE = 0xffffffffL;
|
||||
|
||||
private final ByteBuffer mXml;
|
||||
|
||||
private StringPool mStringPool;
|
||||
private ResourceMap mResourceMap;
|
||||
private int mDepth;
|
||||
private int mCurrentEvent = EVENT_START_DOCUMENT;
|
||||
|
||||
private String mCurrentElementName;
|
||||
private String mCurrentElementNamespace;
|
||||
private int mCurrentElementAttributeCount;
|
||||
private List<Attribute> mCurrentElementAttributes;
|
||||
private ByteBuffer mCurrentElementAttributesContents;
|
||||
private int mCurrentElementAttrSizeBytes;
|
||||
|
||||
/**
|
||||
* Constructs a new parser for the provided document.
|
||||
*/
|
||||
public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
|
||||
xml.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
Chunk resXmlChunk = null;
|
||||
while (xml.hasRemaining()) {
|
||||
Chunk chunk = Chunk.get(xml);
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
if (chunk.getType() == Chunk.TYPE_RES_XML) {
|
||||
resXmlChunk = chunk;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (resXmlChunk == null) {
|
||||
throw new XmlParserException("No XML chunk in file");
|
||||
}
|
||||
mXml = resXmlChunk.getContents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the depth of the current element. Outside of the root of the document the depth is
|
||||
* {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
|
||||
* is decremented by {@code 1} after each {@code end element} event.
|
||||
*/
|
||||
public int getDepth() {
|
||||
return mDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the current event. See {@code EVENT_...} constants.
|
||||
*/
|
||||
public int getEventType() {
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local name of the current element or {@code null} if the current event does not
|
||||
* pertain to an element.
|
||||
*/
|
||||
public String getName() {
|
||||
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||
return null;
|
||||
}
|
||||
return mCurrentElementName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace of the current element or {@code null} if the current event does not
|
||||
* pertain to an element. Returns an empty string if the element is not associated with a
|
||||
* namespace.
|
||||
*/
|
||||
public String getNamespace() {
|
||||
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||
return null;
|
||||
}
|
||||
return mCurrentElementNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of attributes of the element associated with the current event or
|
||||
* {@code -1} if no element is associated with the current event.
|
||||
*/
|
||||
public int getAttributeCount() {
|
||||
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return mCurrentElementAttributeCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource ID corresponding to the name of the specified attribute of the current
|
||||
* element or {@code 0} if the name is not associated with a resource ID.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public int getAttributeNameResourceId(int index) throws XmlParserException {
|
||||
return getAttribute(index).getNameResourceId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the specified attribute of the current element.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public String getAttributeName(int index) throws XmlParserException {
|
||||
return getAttribute(index).getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the specified attribute of the current element or an empty string if
|
||||
* the attribute is not associated with a namespace.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public String getAttributeNamespace(int index) throws XmlParserException {
|
||||
return getAttribute(index).getNamespace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value type of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public int getAttributeValueType(int index) throws XmlParserException {
|
||||
int type = getAttribute(index).getValueType();
|
||||
switch (type) {
|
||||
case Attribute.TYPE_STRING:
|
||||
return VALUE_TYPE_STRING;
|
||||
case Attribute.TYPE_INT_DEC:
|
||||
case Attribute.TYPE_INT_HEX:
|
||||
return VALUE_TYPE_INT;
|
||||
case Attribute.TYPE_REFERENCE:
|
||||
return VALUE_TYPE_REFERENCE;
|
||||
case Attribute.TYPE_INT_BOOLEAN:
|
||||
return VALUE_TYPE_BOOLEAN;
|
||||
default:
|
||||
return VALUE_TYPE_UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event.
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public int getAttributeIntValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getIntValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the boolean value of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event.
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public boolean getAttributeBooleanValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getBooleanValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event.
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public String getAttributeStringValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getStringValue();
|
||||
}
|
||||
|
||||
private Attribute getAttribute(int index) {
|
||||
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||
throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
|
||||
}
|
||||
if (index < 0) {
|
||||
throw new IndexOutOfBoundsException("index must be >= 0");
|
||||
}
|
||||
if (index >= mCurrentElementAttributeCount) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"index must be <= attr count (" + mCurrentElementAttributeCount + ")");
|
||||
}
|
||||
parseCurrentElementAttributesIfNotParsed();
|
||||
return mCurrentElementAttributes.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
|
||||
*/
|
||||
public int next() throws XmlParserException {
|
||||
// Decrement depth if the previous event was "end element".
|
||||
if (mCurrentEvent == EVENT_END_ELEMENT) {
|
||||
mDepth--;
|
||||
}
|
||||
|
||||
// Read events from document, ignoring events that we don't report to caller. Stop at the
|
||||
// earliest event which we report to caller.
|
||||
while (mXml.hasRemaining()) {
|
||||
Chunk chunk = Chunk.get(mXml);
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
switch (chunk.getType()) {
|
||||
case Chunk.TYPE_STRING_POOL:
|
||||
if (mStringPool != null) {
|
||||
throw new XmlParserException("Multiple string pools not supported");
|
||||
}
|
||||
mStringPool = new StringPool(chunk);
|
||||
break;
|
||||
|
||||
case Chunk.RES_XML_TYPE_START_ELEMENT:
|
||||
{
|
||||
if (mStringPool == null) {
|
||||
throw new XmlParserException(
|
||||
"Named element encountered before string pool");
|
||||
}
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (contents.remaining() < 20) {
|
||||
throw new XmlParserException(
|
||||
"Start element chunk too short. Need at least 20 bytes. Available: "
|
||||
+ contents.remaining() + " bytes");
|
||||
}
|
||||
long nsId = getUnsignedInt32(contents);
|
||||
long nameId = getUnsignedInt32(contents);
|
||||
int attrStartOffset = getUnsignedInt16(contents);
|
||||
int attrSizeBytes = getUnsignedInt16(contents);
|
||||
int attrCount = getUnsignedInt16(contents);
|
||||
long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
|
||||
contents.position(0);
|
||||
if (attrStartOffset > contents.remaining()) {
|
||||
throw new XmlParserException(
|
||||
"Attributes start offset out of bounds: " + attrStartOffset
|
||||
+ ", max: " + contents.remaining());
|
||||
}
|
||||
if (attrEndOffset > contents.remaining()) {
|
||||
throw new XmlParserException(
|
||||
"Attributes end offset out of bounds: " + attrEndOffset
|
||||
+ ", max: " + contents.remaining());
|
||||
}
|
||||
|
||||
mCurrentElementName = mStringPool.getString(nameId);
|
||||
mCurrentElementNamespace =
|
||||
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||
mCurrentElementAttributeCount = attrCount;
|
||||
mCurrentElementAttributes = null;
|
||||
mCurrentElementAttrSizeBytes = attrSizeBytes;
|
||||
mCurrentElementAttributesContents =
|
||||
sliceFromTo(contents, attrStartOffset, attrEndOffset);
|
||||
|
||||
mDepth++;
|
||||
mCurrentEvent = EVENT_START_ELEMENT;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
case Chunk.RES_XML_TYPE_END_ELEMENT:
|
||||
{
|
||||
if (mStringPool == null) {
|
||||
throw new XmlParserException(
|
||||
"Named element encountered before string pool");
|
||||
}
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (contents.remaining() < 8) {
|
||||
throw new XmlParserException(
|
||||
"End element chunk too short. Need at least 8 bytes. Available: "
|
||||
+ contents.remaining() + " bytes");
|
||||
}
|
||||
long nsId = getUnsignedInt32(contents);
|
||||
long nameId = getUnsignedInt32(contents);
|
||||
mCurrentElementName = mStringPool.getString(nameId);
|
||||
mCurrentElementNamespace =
|
||||
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||
mCurrentEvent = EVENT_END_ELEMENT;
|
||||
mCurrentElementAttributes = null;
|
||||
mCurrentElementAttributesContents = null;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
case Chunk.RES_XML_TYPE_RESOURCE_MAP:
|
||||
if (mResourceMap != null) {
|
||||
throw new XmlParserException("Multiple resource maps not supported");
|
||||
}
|
||||
mResourceMap = new ResourceMap(chunk);
|
||||
break;
|
||||
default:
|
||||
// Unknown chunk type -- ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mCurrentEvent = EVENT_END_DOCUMENT;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
private void parseCurrentElementAttributesIfNotParsed() {
|
||||
if (mCurrentElementAttributes != null) {
|
||||
return;
|
||||
}
|
||||
mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
|
||||
for (int i = 0; i < mCurrentElementAttributeCount; i++) {
|
||||
int startPosition = i * mCurrentElementAttrSizeBytes;
|
||||
ByteBuffer attr =
|
||||
sliceFromTo(
|
||||
mCurrentElementAttributesContents,
|
||||
startPosition,
|
||||
startPosition + mCurrentElementAttrSizeBytes);
|
||||
long nsId = getUnsignedInt32(attr);
|
||||
long nameId = getUnsignedInt32(attr);
|
||||
attr.position(attr.position() + 7); // skip ignored fields
|
||||
int valueType = getUnsignedInt8(attr);
|
||||
long valueData = getUnsignedInt32(attr);
|
||||
mCurrentElementAttributes.add(
|
||||
new Attribute(
|
||||
nsId,
|
||||
nameId,
|
||||
valueType,
|
||||
(int) valueData,
|
||||
mStringPool,
|
||||
mResourceMap));
|
||||
}
|
||||
}
|
||||
|
||||
private static class Attribute {
|
||||
private static final int TYPE_REFERENCE = 1;
|
||||
private static final int TYPE_STRING = 3;
|
||||
private static final int TYPE_INT_DEC = 0x10;
|
||||
private static final int TYPE_INT_HEX = 0x11;
|
||||
private static final int TYPE_INT_BOOLEAN = 0x12;
|
||||
|
||||
private final long mNsId;
|
||||
private final long mNameId;
|
||||
private final int mValueType;
|
||||
private final int mValueData;
|
||||
private final StringPool mStringPool;
|
||||
private final ResourceMap mResourceMap;
|
||||
|
||||
private Attribute(
|
||||
long nsId,
|
||||
long nameId,
|
||||
int valueType,
|
||||
int valueData,
|
||||
StringPool stringPool,
|
||||
ResourceMap resourceMap) {
|
||||
mNsId = nsId;
|
||||
mNameId = nameId;
|
||||
mValueType = valueType;
|
||||
mValueData = valueData;
|
||||
mStringPool = stringPool;
|
||||
mResourceMap = resourceMap;
|
||||
}
|
||||
|
||||
public int getNameResourceId() {
|
||||
return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
|
||||
}
|
||||
|
||||
public String getName() throws XmlParserException {
|
||||
return mStringPool.getString(mNameId);
|
||||
}
|
||||
|
||||
public String getNamespace() throws XmlParserException {
|
||||
return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
|
||||
}
|
||||
|
||||
public int getValueType() {
|
||||
return mValueType;
|
||||
}
|
||||
|
||||
public int getIntValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_REFERENCE:
|
||||
case TYPE_INT_DEC:
|
||||
case TYPE_INT_HEX:
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return mValueData;
|
||||
default:
|
||||
throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getBooleanValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return mValueData != 0;
|
||||
default:
|
||||
throw new XmlParserException(
|
||||
"Cannot coerce to boolean: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
|
||||
public String getStringValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_STRING:
|
||||
return mStringPool.getString(mValueData & 0xffffffffL);
|
||||
case TYPE_INT_DEC:
|
||||
return Integer.toString(mValueData);
|
||||
case TYPE_INT_HEX:
|
||||
return "0x" + Integer.toHexString(mValueData);
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return Boolean.toString(mValueData != 0);
|
||||
case TYPE_REFERENCE:
|
||||
return "@" + Integer.toHexString(mValueData);
|
||||
default:
|
||||
throw new XmlParserException(
|
||||
"Cannot coerce to string: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
|
||||
* contents.
|
||||
*/
|
||||
private static class Chunk {
|
||||
public static final int TYPE_STRING_POOL = 1;
|
||||
public static final int TYPE_RES_XML = 3;
|
||||
public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
|
||||
public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
|
||||
public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
|
||||
|
||||
static final int HEADER_MIN_SIZE_BYTES = 8;
|
||||
|
||||
private final int mType;
|
||||
private final ByteBuffer mHeader;
|
||||
private final ByteBuffer mContents;
|
||||
|
||||
public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
|
||||
mType = type;
|
||||
mHeader = header;
|
||||
mContents = contents;
|
||||
}
|
||||
|
||||
public ByteBuffer getContents() {
|
||||
ByteBuffer result = mContents.slice();
|
||||
result.order(mContents.order());
|
||||
return result;
|
||||
}
|
||||
|
||||
public ByteBuffer getHeader() {
|
||||
ByteBuffer result = mHeader.slice();
|
||||
result.order(mHeader.order());
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes the chunk located at the current position of the input and returns the chunk
|
||||
* or {@code null} if there is no chunk left in the input.
|
||||
*
|
||||
* @throws XmlParserException if the chunk is malformed
|
||||
*/
|
||||
public static Chunk get(ByteBuffer input) throws XmlParserException {
|
||||
if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
|
||||
// Android ignores the last chunk if its header is too big to fit into the file
|
||||
input.position(input.limit());
|
||||
return null;
|
||||
}
|
||||
|
||||
int originalPosition = input.position();
|
||||
int type = getUnsignedInt16(input);
|
||||
int headerSize = getUnsignedInt16(input);
|
||||
long chunkSize = getUnsignedInt32(input);
|
||||
long chunkRemaining = chunkSize - 8;
|
||||
if (chunkRemaining > input.remaining()) {
|
||||
// Android ignores the last chunk if it's too big to fit into the file
|
||||
input.position(input.limit());
|
||||
return null;
|
||||
}
|
||||
if (headerSize < HEADER_MIN_SIZE_BYTES) {
|
||||
throw new XmlParserException(
|
||||
"Malformed chunk: header too short: " + headerSize + " bytes");
|
||||
} else if (headerSize > chunkSize) {
|
||||
throw new XmlParserException(
|
||||
"Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
|
||||
+ chunkSize + " bytes");
|
||||
}
|
||||
int contentStartPosition = originalPosition + headerSize;
|
||||
long chunkEndPosition = originalPosition + chunkSize;
|
||||
Chunk chunk =
|
||||
new Chunk(
|
||||
type,
|
||||
sliceFromTo(input, originalPosition, contentStartPosition),
|
||||
sliceFromTo(input, contentStartPosition, chunkEndPosition));
|
||||
input.position((int) chunkEndPosition);
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
|
||||
*/
|
||||
private static class StringPool {
|
||||
private static final int FLAG_UTF8 = 1 << 8;
|
||||
|
||||
private final ByteBuffer mChunkContents;
|
||||
private final ByteBuffer mStringsSection;
|
||||
private final int mStringCount;
|
||||
private final boolean mUtf8Encoded;
|
||||
private final Map<Integer, String> mCachedStrings = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs a new string pool from the provided chunk.
|
||||
*
|
||||
* @throws XmlParserException if a parsing error occurred
|
||||
*/
|
||||
public StringPool(Chunk chunk) throws XmlParserException {
|
||||
ByteBuffer header = chunk.getHeader();
|
||||
int headerSizeBytes = header.remaining();
|
||||
header.position(Chunk.HEADER_MIN_SIZE_BYTES);
|
||||
if (header.remaining() < 20) {
|
||||
throw new XmlParserException(
|
||||
"XML chunk's header too short. Required at least 20 bytes. Available: "
|
||||
+ header.remaining() + " bytes");
|
||||
}
|
||||
long stringCount = getUnsignedInt32(header);
|
||||
if (stringCount > Integer.MAX_VALUE) {
|
||||
throw new XmlParserException("Too many strings: " + stringCount);
|
||||
}
|
||||
mStringCount = (int) stringCount;
|
||||
long styleCount = getUnsignedInt32(header);
|
||||
if (styleCount > Integer.MAX_VALUE) {
|
||||
throw new XmlParserException("Too many styles: " + styleCount);
|
||||
}
|
||||
long flags = getUnsignedInt32(header);
|
||||
long stringsStartOffset = getUnsignedInt32(header);
|
||||
long stylesStartOffset = getUnsignedInt32(header);
|
||||
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (mStringCount > 0) {
|
||||
int stringsSectionStartOffsetInContents =
|
||||
(int) (stringsStartOffset - headerSizeBytes);
|
||||
int stringsSectionEndOffsetInContents;
|
||||
if (styleCount > 0) {
|
||||
// Styles section follows the strings section
|
||||
if (stylesStartOffset < stringsStartOffset) {
|
||||
throw new XmlParserException(
|
||||
"Styles offset (" + stylesStartOffset + ") < strings offset ("
|
||||
+ stringsStartOffset + ")");
|
||||
}
|
||||
stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
|
||||
} else {
|
||||
stringsSectionEndOffsetInContents = contents.remaining();
|
||||
}
|
||||
mStringsSection =
|
||||
sliceFromTo(
|
||||
contents,
|
||||
stringsSectionStartOffsetInContents,
|
||||
stringsSectionEndOffsetInContents);
|
||||
} else {
|
||||
mStringsSection = ByteBuffer.allocate(0);
|
||||
}
|
||||
|
||||
mUtf8Encoded = (flags & FLAG_UTF8) != 0;
|
||||
mChunkContents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string located at the specified {@code 0}-based index in this pool.
|
||||
*
|
||||
* @throws XmlParserException if the string does not exist or cannot be decoded
|
||||
*/
|
||||
public String getString(long index) throws XmlParserException {
|
||||
if (index < 0) {
|
||||
throw new XmlParserException("Unsuported string index: " + index);
|
||||
} else if (index >= mStringCount) {
|
||||
throw new XmlParserException(
|
||||
"Unsuported string index: " + index + ", max: " + (mStringCount - 1));
|
||||
}
|
||||
|
||||
int idx = (int) index;
|
||||
String result = mCachedStrings.get(idx);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
|
||||
if (offsetInStringsSection >= mStringsSection.capacity()) {
|
||||
throw new XmlParserException(
|
||||
"Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
|
||||
+ ", max: " + (mStringsSection.capacity() - 1));
|
||||
}
|
||||
mStringsSection.position((int) offsetInStringsSection);
|
||||
result =
|
||||
(mUtf8Encoded)
|
||||
? getLengthPrefixedUtf8EncodedString(mStringsSection)
|
||||
: getLengthPrefixedUtf16EncodedString(mStringsSection);
|
||||
mCachedStrings.put(idx, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
|
||||
throws XmlParserException {
|
||||
// If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
|
||||
// Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
|
||||
// of supported values is 0 to 0x7fffffff inclusive.
|
||||
int lengthChars = getUnsignedInt16(encoded);
|
||||
if ((lengthChars & 0x8000) != 0) {
|
||||
lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
|
||||
}
|
||||
if (lengthChars > Integer.MAX_VALUE / 2) {
|
||||
throw new XmlParserException("String too long: " + lengthChars + " uint16s");
|
||||
}
|
||||
int lengthBytes = lengthChars * 2;
|
||||
|
||||
byte[] arr;
|
||||
int arrOffset;
|
||||
if (encoded.hasArray()) {
|
||||
arr = encoded.array();
|
||||
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||
encoded.position(encoded.position() + lengthBytes);
|
||||
} else {
|
||||
arr = new byte[lengthBytes];
|
||||
arrOffset = 0;
|
||||
encoded.get(arr);
|
||||
}
|
||||
// Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
|
||||
// array of bytes is NULL terminated.
|
||||
if ((arr[arrOffset + lengthBytes] != 0)
|
||||
|| (arr[arrOffset + lengthBytes + 1] != 0)) {
|
||||
throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
|
||||
}
|
||||
try {
|
||||
return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-16LE character encoding not supported", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
|
||||
throws XmlParserException {
|
||||
// If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
|
||||
// it is stored as a big-endian uint16 with highest bit set. Thus, the range of
|
||||
// supported values is 0 to 0x7fff inclusive.
|
||||
|
||||
// Skip UTF-16 encoded length (in uint16s)
|
||||
int lengthBytes = getUnsignedInt8(encoded);
|
||||
if ((lengthBytes & 0x80) != 0) {
|
||||
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||
}
|
||||
|
||||
// Read UTF-8 encoded length (in bytes)
|
||||
lengthBytes = getUnsignedInt8(encoded);
|
||||
if ((lengthBytes & 0x80) != 0) {
|
||||
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||
}
|
||||
|
||||
byte[] arr;
|
||||
int arrOffset;
|
||||
if (encoded.hasArray()) {
|
||||
arr = encoded.array();
|
||||
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||
encoded.position(encoded.position() + lengthBytes);
|
||||
} else {
|
||||
arr = new byte[lengthBytes];
|
||||
arrOffset = 0;
|
||||
encoded.get(arr);
|
||||
}
|
||||
// Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
|
||||
// of bytes is NULL terminated.
|
||||
if (arr[arrOffset + lengthBytes] != 0) {
|
||||
throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
|
||||
}
|
||||
try {
|
||||
return new String(arr, arrOffset, lengthBytes, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 character encoding not supported", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
|
||||
* map.
|
||||
*/
|
||||
private static class ResourceMap {
|
||||
private final ByteBuffer mChunkContents;
|
||||
private final int mEntryCount;
|
||||
|
||||
/**
|
||||
* Constructs a new resource map from the provided chunk.
|
||||
*
|
||||
* @throws XmlParserException if a parsing error occurred
|
||||
*/
|
||||
public ResourceMap(Chunk chunk) throws XmlParserException {
|
||||
mChunkContents = chunk.getContents().slice();
|
||||
mChunkContents.order(chunk.getContents().order());
|
||||
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||
mEntryCount = mChunkContents.remaining() / 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource ID located at the specified {@code 0}-based index in this pool or
|
||||
* {@code 0} if the index is out of range.
|
||||
*/
|
||||
public int getResourceId(long index) {
|
||||
if ((index < 0) || (index >= mEntryCount)) {
|
||||
return 0;
|
||||
}
|
||||
int idx = (int) index;
|
||||
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||
return mChunkContents.getInt(idx * 4);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
return sliceFromTo(source, (int) start, (int) end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int originalPosition = source.position();
|
||||
try {
|
||||
source.position(0);
|
||||
source.limit(end);
|
||||
source.position(start);
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
return result;
|
||||
} finally {
|
||||
source.position(0);
|
||||
source.limit(originalLimit);
|
||||
source.position(originalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getUnsignedInt8(ByteBuffer buffer) {
|
||||
return buffer.get() & 0xff;
|
||||
}
|
||||
|
||||
private static int getUnsignedInt16(ByteBuffer buffer) {
|
||||
return buffer.getShort() & 0xffff;
|
||||
}
|
||||
|
||||
private static long getUnsignedInt32(ByteBuffer buffer) {
|
||||
return buffer.getInt() & 0xffffffffL;
|
||||
}
|
||||
|
||||
private static long getUnsignedInt32(ByteBuffer buffer, int position) {
|
||||
return buffer.getInt(position) & 0xffffffffL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred while parsing a document.
|
||||
*/
|
||||
public static class XmlParserException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public XmlParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public XmlParserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base implementation of an APK signature verification result.
|
||||
*/
|
||||
public class ApkSigResult {
|
||||
public final int signatureSchemeVersion;
|
||||
|
||||
/** Whether the APK's Signature Scheme signature verifies. */
|
||||
public boolean verified;
|
||||
|
||||
public final List<ApkSignerInfo> mSigners = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
|
||||
public ApkSigResult(int signatureSchemeVersion) {
|
||||
this.signatureSchemeVersion = signatureSchemeVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this result encountered errors during verification.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
if (!mErrors.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (!mSigners.isEmpty()) {
|
||||
for (ApkSignerInfo signer : mSigners) {
|
||||
if (signer.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this result encountered warnings during verification.
|
||||
*/
|
||||
public boolean containsWarnings() {
|
||||
if (!mWarnings.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (!mSigners.isEmpty()) {
|
||||
for (ApkSignerInfo signer : mSigners) {
|
||||
if (signer.containsWarnings()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addError(int issueId, Object... parameters) {
|
||||
mErrors.add(new ApkVerificationIssue(issueId, parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addWarning(int issueId, Object... parameters) {
|
||||
mWarnings.add(new ApkVerificationIssue(issueId, parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errors encountered during verification.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warnings encountered during verification.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base implementation of an APK signer.
|
||||
*/
|
||||
public class ApkSignerInfo {
|
||||
public int index;
|
||||
public long timestamp;
|
||||
public List<X509Certificate> certs = new ArrayList<>();
|
||||
public List<X509Certificate> certificateLineage = new ArrayList<>();
|
||||
|
||||
private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addError(int issueId, Object... params) {
|
||||
mErrors.add(new ApkVerificationIssue(issueId, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addWarning(int issueId, Object... params) {
|
||||
mWarnings.add(new ApkVerificationIssue(issueId, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as an info message to this signer config using the
|
||||
* provided {@code issueId} and {@code params}.
|
||||
*/
|
||||
public void addInfoMessage(int issueId, Object... params) {
|
||||
mInfoMessages.add(new ApkVerificationIssue(issueId, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any errors were encountered during verification for this signer.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any warnings were encountered during verification for this signer.
|
||||
*/
|
||||
public boolean containsWarnings() {
|
||||
return !mWarnings.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any info messages were encountered during verification of this
|
||||
* signer.
|
||||
*/
|
||||
public boolean containsInfoMessages() {
|
||||
return !mInfoMessages.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errors encountered during verification for this signer.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warnings encountered during verification for this signer.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the info messages encountered during verification of this signer.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getInfoMessages() {
|
||||
return mInfoMessages;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,393 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
|
||||
import com.android.apksig.apk.ApkUtilsLite;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
|
||||
* utility functionality.
|
||||
*/
|
||||
public class ApkSigningBlockUtilsLite {
|
||||
private ApkSigningBlockUtilsLite() {}
|
||||
|
||||
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
|
||||
/**
|
||||
* Returns the APK Signature Scheme block contained in the provided APK file for the given ID
|
||||
* and the additional information relevant for verifying the block against the file.
|
||||
*
|
||||
* @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
|
||||
* identifying the appropriate block to find, e.g. the APK Signature Scheme v2
|
||||
* block ID.
|
||||
*
|
||||
* @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
|
||||
* @throws IOException if an I/O error occurs while reading the APK
|
||||
*/
|
||||
public static SignatureInfo findSignature(
|
||||
DataSource apk, ZipSections zipSections, int blockId)
|
||||
throws IOException, SignatureNotFoundException {
|
||||
// Find the APK Signing Block.
|
||||
DataSource apkSigningBlock;
|
||||
long apkSigningBlockOffset;
|
||||
try {
|
||||
ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
|
||||
ApkUtilsLite.findApkSigningBlock(apk, zipSections);
|
||||
apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
|
||||
apkSigningBlock = apkSigningBlockInfo.getContents();
|
||||
} catch (ApkSigningBlockNotFoundException e) {
|
||||
throw new SignatureNotFoundException(e.getMessage(), e);
|
||||
}
|
||||
ByteBuffer apkSigningBlockBuf =
|
||||
apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
|
||||
apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Find the APK Signature Scheme Block inside the APK Signing Block.
|
||||
ByteBuffer apkSignatureSchemeBlock =
|
||||
findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
|
||||
return new SignatureInfo(
|
||||
apkSignatureSchemeBlock,
|
||||
apkSigningBlockOffset,
|
||||
zipSections.getZipCentralDirectoryOffset(),
|
||||
zipSections.getZipEndOfCentralDirectoryOffset(),
|
||||
zipSections.getZipEndOfCentralDirectory());
|
||||
}
|
||||
|
||||
public static ByteBuffer findApkSignatureSchemeBlock(
|
||||
ByteBuffer apkSigningBlock,
|
||||
int blockId) throws SignatureNotFoundException {
|
||||
checkByteOrderLittleEndian(apkSigningBlock);
|
||||
// FORMAT:
|
||||
// OFFSET DATA TYPE DESCRIPTION
|
||||
// * @+0 bytes uint64: size in bytes (excluding this field)
|
||||
// * @+8 bytes pairs
|
||||
// * @-24 bytes uint64: size in bytes (same as the one above)
|
||||
// * @-16 bytes uint128: magic
|
||||
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
|
||||
|
||||
int entryCount = 0;
|
||||
while (pairs.hasRemaining()) {
|
||||
entryCount++;
|
||||
if (pairs.remaining() < 8) {
|
||||
throw new SignatureNotFoundException(
|
||||
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
|
||||
}
|
||||
long lenLong = pairs.getLong();
|
||||
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block entry #" + entryCount
|
||||
+ " size out of range: " + lenLong);
|
||||
}
|
||||
int len = (int) lenLong;
|
||||
int nextEntryPos = pairs.position() + len;
|
||||
if (len > pairs.remaining()) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block entry #" + entryCount + " size out of range: " + len
|
||||
+ ", available: " + pairs.remaining());
|
||||
}
|
||||
int id = pairs.getInt();
|
||||
if (id == blockId) {
|
||||
return getByteBuffer(pairs, len - 4);
|
||||
}
|
||||
pairs.position(nextEntryPos);
|
||||
}
|
||||
|
||||
throw new SignatureNotFoundException(
|
||||
"No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
|
||||
}
|
||||
|
||||
public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
|
||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of signatures which are expected to be verified by at least one Android
|
||||
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
|
||||
* guaranteed to contain at least one signature.
|
||||
*
|
||||
* <p>Each Android platform version typically verifies exactly one signature from the provided
|
||||
* {@code signatures} set. This method returns the set of these signatures collected over all
|
||||
* requested platform versions. As a result, the result may contain more than one signature.
|
||||
*
|
||||
* @throws NoApkSupportedSignaturesException if no supported signatures were
|
||||
* found for an Android platform version in the range.
|
||||
*/
|
||||
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
|
||||
List<T> signatures, int minSdkVersion, int maxSdkVersion)
|
||||
throws NoApkSupportedSignaturesException {
|
||||
return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of signatures which are expected to be verified by at least one Android
|
||||
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
|
||||
* guaranteed to contain at least one signature.
|
||||
*
|
||||
* <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
|
||||
* signature within the signing block using the standard JCA.
|
||||
*
|
||||
* <p>Each Android platform version typically verifies exactly one signature from the provided
|
||||
* {@code signatures} set. This method returns the set of these signatures collected over all
|
||||
* requested platform versions. As a result, the result may contain more than one signature.
|
||||
*
|
||||
* @throws NoApkSupportedSignaturesException if no supported signatures were
|
||||
* found for an Android platform version in the range.
|
||||
*/
|
||||
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
|
||||
List<T> signatures, int minSdkVersion, int maxSdkVersion,
|
||||
boolean onlyRequireJcaSupport) throws
|
||||
NoApkSupportedSignaturesException {
|
||||
// Pick the signature with the strongest algorithm at all required SDK versions, to mimic
|
||||
// Android's behavior on those versions.
|
||||
//
|
||||
// Here we assume that, once introduced, a signature algorithm continues to be supported in
|
||||
// all future Android versions. We also assume that the better-than relationship between
|
||||
// algorithms is exactly the same on all Android platform versions (except that older
|
||||
// platforms might support fewer algorithms). If these assumption are no longer true, the
|
||||
// logic here will need to change accordingly.
|
||||
Map<Integer, T>
|
||||
bestSigAlgorithmOnSdkVersion = new HashMap<>();
|
||||
int minProvidedSignaturesVersion = Integer.MAX_VALUE;
|
||||
for (T sig : signatures) {
|
||||
SignatureAlgorithm sigAlgorithm = sig.algorithm;
|
||||
int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
|
||||
: sigAlgorithm.getMinSdkVersion();
|
||||
if (sigMinSdkVersion > maxSdkVersion) {
|
||||
continue;
|
||||
}
|
||||
if (sigMinSdkVersion < minProvidedSignaturesVersion) {
|
||||
minProvidedSignaturesVersion = sigMinSdkVersion;
|
||||
}
|
||||
|
||||
T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
|
||||
if ((candidate == null)
|
||||
|| (compareSignatureAlgorithm(
|
||||
sigAlgorithm, candidate.algorithm) > 0)) {
|
||||
bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
|
||||
}
|
||||
}
|
||||
|
||||
// Must have some supported signature algorithms for minSdkVersion.
|
||||
if (minSdkVersion < minProvidedSignaturesVersion) {
|
||||
throw new NoApkSupportedSignaturesException(
|
||||
"Minimum provided signature version " + minProvidedSignaturesVersion +
|
||||
" > minSdkVersion " + minSdkVersion);
|
||||
}
|
||||
if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
|
||||
throw new NoApkSupportedSignaturesException("No supported signature");
|
||||
}
|
||||
List<T> signaturesToVerify =
|
||||
new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
|
||||
Collections.sort(
|
||||
signaturesToVerify,
|
||||
(sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
|
||||
return signaturesToVerify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
|
||||
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
|
||||
*/
|
||||
public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
|
||||
ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
|
||||
ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
|
||||
return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
|
||||
* if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
|
||||
*/
|
||||
private static int compareContentDigestAlgorithm(
|
||||
ContentDigestAlgorithm alg1,
|
||||
ContentDigestAlgorithm alg2) {
|
||||
switch (alg1) {
|
||||
case CHUNKED_SHA256:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
return 0;
|
||||
case CHUNKED_SHA512:
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return -1;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
case CHUNKED_SHA512:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return 1;
|
||||
case CHUNKED_SHA512:
|
||||
return 0;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
return 1;
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return 0;
|
||||
case CHUNKED_SHA512:
|
||||
return -1;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg1: " + alg1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int originalPosition = source.position();
|
||||
try {
|
||||
source.position(0);
|
||||
source.limit(end);
|
||||
source.position(start);
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
return result;
|
||||
} finally {
|
||||
source.position(0);
|
||||
source.limit(originalLimit);
|
||||
source.position(originalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
|
||||
* position of this buffer.
|
||||
*
|
||||
* <p>This method reads the next {@code size} bytes at this buffer's current position,
|
||||
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
|
||||
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
|
||||
* {@code size}.
|
||||
*/
|
||||
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
|
||||
if (size < 0) {
|
||||
throw new IllegalArgumentException("size: " + size);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int position = source.position();
|
||||
int limit = position + size;
|
||||
if ((limit < position) || (limit > originalLimit)) {
|
||||
throw new BufferUnderflowException();
|
||||
}
|
||||
source.limit(limit);
|
||||
try {
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
source.position(limit);
|
||||
return result;
|
||||
} finally {
|
||||
source.limit(originalLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public static String toHex(byte[] value) {
|
||||
StringBuilder sb = new StringBuilder(value.length * 2);
|
||||
int len = value.length;
|
||||
for (int i = 0; i < len; i++) {
|
||||
int hi = (value[i] & 0xff) >>> 4;
|
||||
int lo = value[i] & 0x0f;
|
||||
sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
|
||||
if (source.remaining() < 4) {
|
||||
throw new ApkFormatException(
|
||||
"Remaining buffer too short to contain length of length-prefixed field"
|
||||
+ ". Remaining: " + source.remaining());
|
||||
}
|
||||
int len = source.getInt();
|
||||
if (len < 0) {
|
||||
throw new IllegalArgumentException("Negative length");
|
||||
} else if (len > source.remaining()) {
|
||||
throw new ApkFormatException(
|
||||
"Length-prefixed field longer than remaining buffer"
|
||||
+ ". Field length: " + len + ", remaining: " + source.remaining());
|
||||
}
|
||||
return getByteBuffer(source, len);
|
||||
}
|
||||
|
||||
public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
|
||||
int len = buf.getInt();
|
||||
if (len < 0) {
|
||||
throw new ApkFormatException("Negative length");
|
||||
} else if (len > buf.remaining()) {
|
||||
throw new ApkFormatException(
|
||||
"Underflow while reading length-prefixed value. Length: " + len
|
||||
+ ", available: " + buf.remaining());
|
||||
}
|
||||
byte[] result = new byte[len];
|
||||
buf.get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
List<Pair<Integer, byte[]>> sequence) {
|
||||
int resultSize = 0;
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
resultSize += 12 + element.getSecond().length;
|
||||
}
|
||||
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
byte[] second = element.getSecond();
|
||||
result.putInt(8 + second.length);
|
||||
result.putInt(element.getFirst());
|
||||
result.putInt(second.length);
|
||||
result.put(second);
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/**
|
||||
* Base implementation of a supported signature for an APK.
|
||||
*/
|
||||
public class ApkSupportedSignature {
|
||||
public final SignatureAlgorithm algorithm;
|
||||
public final byte[] signature;
|
||||
|
||||
/**
|
||||
* Constructs a new supported signature using the provided {@code algorithm} and {@code
|
||||
* signature} bytes.
|
||||
*/
|
||||
public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
|
||||
this.algorithm = algorithm;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/** APK Signature Scheme v2 content digest algorithm. */
|
||||
public enum ContentDigestAlgorithm {
|
||||
/** SHA2-256 over 1 MB chunks. */
|
||||
CHUNKED_SHA256(1, "SHA-256", 256 / 8),
|
||||
|
||||
/** SHA2-512 over 1 MB chunks. */
|
||||
CHUNKED_SHA512(2, "SHA-512", 512 / 8),
|
||||
|
||||
/** SHA2-256 over 4 KB chunks for APK verity. */
|
||||
VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
|
||||
|
||||
/** Non-chunk SHA2-256. */
|
||||
SHA256(4, "SHA-256", 256 / 8);
|
||||
|
||||
private final int mId;
|
||||
private final String mJcaMessageDigestAlgorithm;
|
||||
private final int mChunkDigestOutputSizeBytes;
|
||||
|
||||
private ContentDigestAlgorithm(
|
||||
int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
|
||||
mId = id;
|
||||
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
|
||||
mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
|
||||
}
|
||||
|
||||
/** Returns the ID of the digest algorithm used on the APK. */
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
|
||||
* chunks by this content digest algorithm.
|
||||
*/
|
||||
String getJcaMessageDigestAlgorithm() {
|
||||
return mJcaMessageDigestAlgorithm;
|
||||
}
|
||||
|
||||
/** Returns the size (in bytes) of the digest of a chunk of content. */
|
||||
int getChunkDigestOutputSizeBytes() {
|
||||
return mChunkDigestOutputSizeBytes;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/**
|
||||
* Base exception that is thrown when there are no signatures that support the full range of
|
||||
* requested platform versions.
|
||||
*/
|
||||
public class NoApkSupportedSignaturesException extends Exception {
|
||||
public NoApkSupportedSignaturesException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PSSParameterSpec;
|
||||
|
||||
/**
|
||||
* APK Signing Block signature algorithm.
|
||||
*/
|
||||
public enum SignatureAlgorithm {
|
||||
// TODO reserve the 0x0000 ID to mean null
|
||||
/**
|
||||
* RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
|
||||
* digested using SHA2-256 in 1 MB chunks.
|
||||
*/
|
||||
RSA_PSS_WITH_SHA256(
|
||||
0x0101,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"RSA",
|
||||
Pair.of("SHA256withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.M),
|
||||
|
||||
/**
|
||||
* RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
|
||||
* digested using SHA2-512 in 1 MB chunks.
|
||||
*/
|
||||
RSA_PSS_WITH_SHA512(
|
||||
0x0102,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||
"RSA",
|
||||
Pair.of(
|
||||
"SHA512withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.M),
|
||||
|
||||
/** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||
RSA_PKCS1_V1_5_WITH_SHA256(
|
||||
0x0103,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"RSA",
|
||||
Pair.of("SHA256withRSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||
RSA_PKCS1_V1_5_WITH_SHA512(
|
||||
0x0104,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||
"RSA",
|
||||
Pair.of("SHA512withRSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||
ECDSA_WITH_SHA256(
|
||||
0x0201,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"EC",
|
||||
Pair.of("SHA256withECDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.HONEYCOMB),
|
||||
|
||||
/** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||
ECDSA_WITH_SHA512(
|
||||
0x0202,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||
"EC",
|
||||
Pair.of("SHA512withECDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.HONEYCOMB),
|
||||
|
||||
/** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||
DSA_WITH_SHA256(
|
||||
0x0301,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"DSA",
|
||||
Pair.of("SHA256withDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/**
|
||||
* DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
|
||||
* deterministically according to RFC 6979.
|
||||
*/
|
||||
DETDSA_WITH_SHA256(
|
||||
0x0301,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"DSA",
|
||||
Pair.of("SHA256withDetDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/**
|
||||
* RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
|
||||
* the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
|
||||
* in little endian) construct the final digest.
|
||||
*/
|
||||
VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
|
||||
0x0421,
|
||||
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||
"RSA",
|
||||
Pair.of("SHA256withRSA", null),
|
||||
AndroidSdkVersion.P,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/**
|
||||
* ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||
* endian) construct the final digest.
|
||||
*/
|
||||
VERITY_ECDSA_WITH_SHA256(
|
||||
0x0423,
|
||||
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||
"EC",
|
||||
Pair.of("SHA256withECDSA", null),
|
||||
AndroidSdkVersion.P,
|
||||
AndroidSdkVersion.HONEYCOMB),
|
||||
|
||||
/**
|
||||
* DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||
* endian) construct the final digest.
|
||||
*/
|
||||
VERITY_DSA_WITH_SHA256(
|
||||
0x0425,
|
||||
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||
"DSA",
|
||||
Pair.of("SHA256withDSA", null),
|
||||
AndroidSdkVersion.P,
|
||||
AndroidSdkVersion.INITIAL_RELEASE);
|
||||
|
||||
private final int mId;
|
||||
private final String mJcaKeyAlgorithm;
|
||||
private final ContentDigestAlgorithm mContentDigestAlgorithm;
|
||||
private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
|
||||
private final int mMinSdkVersion;
|
||||
private final int mJcaSigAlgMinSdkVersion;
|
||||
|
||||
SignatureAlgorithm(int id,
|
||||
ContentDigestAlgorithm contentDigestAlgorithm,
|
||||
String jcaKeyAlgorithm,
|
||||
Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
|
||||
int minSdkVersion,
|
||||
int jcaSigAlgMinSdkVersion) {
|
||||
mId = id;
|
||||
mContentDigestAlgorithm = contentDigestAlgorithm;
|
||||
mJcaKeyAlgorithm = jcaKeyAlgorithm;
|
||||
mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
|
||||
*/
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content digest algorithm associated with this signature algorithm.
|
||||
*/
|
||||
public ContentDigestAlgorithm getContentDigestAlgorithm() {
|
||||
return mContentDigestAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
|
||||
*/
|
||||
public String getJcaKeyAlgorithm() {
|
||||
return mJcaKeyAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
|
||||
* (or null if not needed) to parameterize the {@code Signature}.
|
||||
*/
|
||||
public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
|
||||
return mJcaSignatureAlgAndParams;
|
||||
}
|
||||
|
||||
public int getMinSdkVersion() {
|
||||
return mMinSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum SDK version that supports the JCA signature algorithm.
|
||||
*/
|
||||
public int getJcaSigAlgMinSdkVersion() {
|
||||
return mJcaSigAlgMinSdkVersion;
|
||||
}
|
||||
|
||||
public static SignatureAlgorithm findById(int id) {
|
||||
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
|
||||
if (alg.getId() == id) {
|
||||
return alg;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme block and additional information relevant to verifying the signatures
|
||||
* contained in the block against the file.
|
||||
*/
|
||||
public class SignatureInfo {
|
||||
/** Contents of APK Signature Scheme block. */
|
||||
public final ByteBuffer signatureBlock;
|
||||
|
||||
/** Position of the APK Signing Block in the file. */
|
||||
public final long apkSigningBlockOffset;
|
||||
|
||||
/** Position of the ZIP Central Directory in the file. */
|
||||
public final long centralDirOffset;
|
||||
|
||||
/** Position of the ZIP End of Central Directory (EoCD) in the file. */
|
||||
public final long eocdOffset;
|
||||
|
||||
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
|
||||
public final ByteBuffer eocd;
|
||||
|
||||
public SignatureInfo(
|
||||
ByteBuffer signatureBlock,
|
||||
long apkSigningBlockOffset,
|
||||
long centralDirOffset,
|
||||
long eocdOffset,
|
||||
ByteBuffer eocd) {
|
||||
this.signatureBlock = signatureBlock;
|
||||
this.apkSigningBlockOffset = apkSigningBlockOffset;
|
||||
this.centralDirOffset = centralDirOffset;
|
||||
this.eocdOffset = eocdOffset;
|
||||
this.eocd = eocd;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/**
|
||||
* Base exception that is thrown when the APK is not signed with the requested signature scheme.
|
||||
*/
|
||||
public class SignatureNotFoundException extends Exception {
|
||||
public SignatureNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SignatureNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
|
||||
public class SourceStampCertificateLineage {
|
||||
|
||||
private final static int FIRST_VERSION = 1;
|
||||
private final static int CURRENT_VERSION = FIRST_VERSION;
|
||||
|
||||
/**
|
||||
* Deserializes the binary representation of a SourceStampCertificateLineage. Also
|
||||
* verifies that the structure is well-formed, e.g. that the signature for each node is from its
|
||||
* parent.
|
||||
*/
|
||||
public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
|
||||
throws IOException {
|
||||
List<SigningCertificateNode> result = new ArrayList<>();
|
||||
int nodeCount = 0;
|
||||
if (inputBytes == null || !inputBytes.hasRemaining()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
|
||||
|
||||
CertificateFactory certFactory;
|
||||
try {
|
||||
certFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||
}
|
||||
|
||||
// FORMAT (little endian):
|
||||
// * uint32: version code
|
||||
// * sequence of length-prefixed (uint32): nodes
|
||||
// * length-prefixed bytes: signed data
|
||||
// * length-prefixed bytes: certificate
|
||||
// * uint32: signature algorithm id
|
||||
// * uint32: flags
|
||||
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||
// * length-prefixed bytes: signature over above signed data
|
||||
|
||||
X509Certificate lastCert = null;
|
||||
int lastSigAlgorithmId = 0;
|
||||
|
||||
try {
|
||||
int version = inputBytes.getInt();
|
||||
if (version != CURRENT_VERSION) {
|
||||
// we only have one version to worry about right now, so just check it
|
||||
throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
|
||||
+ " different than any of which we are aware");
|
||||
}
|
||||
HashSet<X509Certificate> certHistorySet = new HashSet<>();
|
||||
while (inputBytes.hasRemaining()) {
|
||||
nodeCount++;
|
||||
ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
|
||||
ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
|
||||
int flags = nodeBytes.getInt();
|
||||
int sigAlgorithmId = nodeBytes.getInt();
|
||||
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
|
||||
byte[] signature = readLengthPrefixedByteArray(nodeBytes);
|
||||
|
||||
if (lastCert != null) {
|
||||
// Use previous level cert to verify current level
|
||||
String jcaSignatureAlgorithm =
|
||||
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
PublicKey publicKey = lastCert.getPublicKey();
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
sig.update(signedData);
|
||||
if (!sig.verify(signature)) {
|
||||
throw new SecurityException("Unable to verify signature of certificate #"
|
||||
+ nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
|
||||
+ " SourceStampCertificateLineage object");
|
||||
}
|
||||
}
|
||||
|
||||
signedData.rewind();
|
||||
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
|
||||
int signedSigAlgorithm = signedData.getInt();
|
||||
if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
|
||||
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
|
||||
+ nodeBytes + " when verifying SourceStampCertificateLineage object");
|
||||
}
|
||||
lastCert = (X509Certificate) certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(encodedCert));
|
||||
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
|
||||
if (certHistorySet.contains(lastCert)) {
|
||||
throw new SecurityException("Encountered duplicate entries in "
|
||||
+ "SigningCertificateLineage at certificate #" + nodeCount + ". All "
|
||||
+ "signing certificates should be unique");
|
||||
}
|
||||
certHistorySet.add(lastCert);
|
||||
lastSigAlgorithmId = sigAlgorithmId;
|
||||
result.add(new SigningCertificateNode(
|
||||
lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
|
||||
SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
|
||||
}
|
||||
} catch(ApkFormatException | BufferUnderflowException e){
|
||||
throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
|
||||
} catch(NoSuchAlgorithmException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException | SignatureException e){
|
||||
throw new SecurityException(
|
||||
"Failed to verify signature over signed data for certificate #" + nodeCount
|
||||
+ " when parsing SourceStampCertificateLineage object", e);
|
||||
} catch(CertificateException e){
|
||||
throw new SecurityException("Failed to decode certificate #" + nodeCount
|
||||
+ " when parsing SourceStampCertificateLineage object", e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents one signing certificate in the SourceStampCertificateLineage, which
|
||||
* generally means it is/was used at some point to sign source stamps.
|
||||
*/
|
||||
public static class SigningCertificateNode {
|
||||
|
||||
public SigningCertificateNode(
|
||||
X509Certificate signingCert,
|
||||
SignatureAlgorithm parentSigAlgorithm,
|
||||
SignatureAlgorithm sigAlgorithm,
|
||||
byte[] signature,
|
||||
int flags) {
|
||||
this.signingCert = signingCert;
|
||||
this.parentSigAlgorithm = parentSigAlgorithm;
|
||||
this.sigAlgorithm = sigAlgorithm;
|
||||
this.signature = signature;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof SigningCertificateNode)) return false;
|
||||
|
||||
SigningCertificateNode that = (SigningCertificateNode) o;
|
||||
if (!signingCert.equals(that.signingCert)) return false;
|
||||
if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
|
||||
if (sigAlgorithm != that.sigAlgorithm) return false;
|
||||
if (!Arrays.equals(signature, that.signature)) return false;
|
||||
if (flags != that.flags) return false;
|
||||
|
||||
// we made it
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
|
||||
result = prime * result +
|
||||
((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
|
||||
result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
|
||||
result = prime * result + Arrays.hashCode(signature);
|
||||
result = prime * result + flags;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* the signing cert for this node. This is part of the data signed by the parent node.
|
||||
*/
|
||||
public final X509Certificate signingCert;
|
||||
|
||||
/**
|
||||
* the algorithm used by this node's parent to bless this data. Its ID value is part of
|
||||
* the data signed by the parent node. {@code null} for first node.
|
||||
*/
|
||||
public final SignatureAlgorithm parentSigAlgorithm;
|
||||
|
||||
/**
|
||||
* the algorithm used by this node to bless the next node's data. Its ID value is part
|
||||
* of the signed data of the next node. {@code null} for the last node.
|
||||
*/
|
||||
public SignatureAlgorithm sigAlgorithm;
|
||||
|
||||
/**
|
||||
* signature over the signed data (above). The signature is from this node's parent
|
||||
* signing certificate, which should correspond to the signing certificate used to sign an
|
||||
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
|
||||
*/
|
||||
public final byte[] signature;
|
||||
|
||||
/**
|
||||
* the flags detailing how the platform should treat this signing cert
|
||||
*/
|
||||
public int flags;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
/** Constants used for source stamp signing and verification. */
|
||||
public class SourceStampConstants {
|
||||
private SourceStampConstants() {}
|
||||
|
||||
public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
|
||||
public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
|
||||
public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
|
||||
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
|
||||
/**
|
||||
* The source stamp timestamp attribute value is an 8-byte little-endian encoded long
|
||||
* representing the epoch time in seconds when the stamp block was signed. The first 8 bytes
|
||||
* of the attribute value buffer will be used to read the timestamp, and any additional buffer
|
||||
* space will be ignored.
|
||||
*/
|
||||
public static final int STAMP_TIME_ATTR_ID = 0xe43c5946;
|
||||
}
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
import com.android.apksig.Constants;
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||
import com.android.apksig.internal.apk.ApkSupportedSignature;
|
||||
import com.android.apksig.internal.apk.NoApkSupportedSignaturesException;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Source Stamp verifier.
|
||||
*
|
||||
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||
*
|
||||
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||
*
|
||||
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||
* block.
|
||||
*/
|
||||
class SourceStampVerifier {
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private SourceStampVerifier() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SourceStamp block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over digest provided.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the {@code [minSdkVersion,
|
||||
* maxSdkVersion]} range.
|
||||
*/
|
||||
public static void verifyV1SourceStamp(
|
||||
ByteBuffer sourceStampBlockData,
|
||||
CertificateFactory certFactory,
|
||||
ApkSignerInfo result,
|
||||
byte[] apkDigest,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws ApkFormatException, NoSuchAlgorithmException {
|
||||
X509Certificate sourceStampCertificate =
|
||||
verifySourceStampCertificate(
|
||||
sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
|
||||
if (result.containsWarnings() || result.containsErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData);
|
||||
verifySourceStampSignature(
|
||||
apkDigest,
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
sourceStampCertificate,
|
||||
apkDigestSignatures,
|
||||
result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SourceStamp block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over digest of multiple signature schemes provided.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the {@code [minSdkVersion,
|
||||
* maxSdkVersion]} range.
|
||||
*/
|
||||
public static void verifyV2SourceStamp(
|
||||
ByteBuffer sourceStampBlockData,
|
||||
CertificateFactory certFactory,
|
||||
ApkSignerInfo result,
|
||||
Map<Integer, byte[]> signatureSchemeApkDigests,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws ApkFormatException, NoSuchAlgorithmException {
|
||||
X509Certificate sourceStampCertificate =
|
||||
verifySourceStampCertificate(
|
||||
sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
|
||||
if (result.containsWarnings() || result.containsErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse signed signature schemes block.
|
||||
ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData);
|
||||
Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
|
||||
while (signedSignatureSchemes.hasRemaining()) {
|
||||
ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes);
|
||||
int signatureSchemeId = signedSignatureScheme.getInt();
|
||||
ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme);
|
||||
signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures);
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
|
||||
signatureSchemeApkDigests.entrySet()) {
|
||||
// TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a
|
||||
// v3.0 block must always be present with a v3.1 block is it sufficient to just use the
|
||||
// v3.0 block?
|
||||
if (signatureSchemeApkDigest.getKey()
|
||||
== Constants.VERSION_APK_SIGNATURE_SCHEME_V31) {
|
||||
continue;
|
||||
}
|
||||
if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
|
||||
return;
|
||||
}
|
||||
verifySourceStampSignature(
|
||||
signatureSchemeApkDigest.getValue(),
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
sourceStampCertificate,
|
||||
signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
|
||||
result);
|
||||
if (result.containsWarnings() || result.containsErrors()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceStampBlockData.hasRemaining()) {
|
||||
// The stamp block contains some additional attributes.
|
||||
ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
|
||||
ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
|
||||
|
||||
byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
|
||||
stampAttributeData.get(stampAttributeBytes);
|
||||
stampAttributeData.flip();
|
||||
|
||||
verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion,
|
||||
sourceStampCertificate, stampAttributeDataSignatures, result);
|
||||
if (result.containsErrors() || result.containsWarnings()) {
|
||||
return;
|
||||
}
|
||||
parseStampAttributes(stampAttributeData, sourceStampCertificate, result);
|
||||
}
|
||||
}
|
||||
|
||||
private static X509Certificate verifySourceStampCertificate(
|
||||
ByteBuffer sourceStampBlockData,
|
||||
CertificateFactory certFactory,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
ApkSignerInfo result)
|
||||
throws NoSuchAlgorithmException, ApkFormatException {
|
||||
// Parse the SourceStamp certificate.
|
||||
byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData);
|
||||
X509Certificate sourceStampCertificate;
|
||||
try {
|
||||
sourceStampCertificate = (X509Certificate) certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(sourceStampEncodedCertificate));
|
||||
} catch (CertificateException e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
|
||||
return null;
|
||||
}
|
||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||
// form. Without this, getEncoded may return a different form from what was stored in
|
||||
// the signature. This is because some X509Certificate(Factory) implementations
|
||||
// re-encode certificates.
|
||||
sourceStampCertificate =
|
||||
new GuaranteedEncodedFormX509Certificate(
|
||||
sourceStampCertificate, sourceStampEncodedCertificate);
|
||||
result.certs.add(sourceStampCertificate);
|
||||
// Verify the SourceStamp certificate found in the signing block is the same as the
|
||||
// SourceStamp certificate found in the APK.
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
messageDigest.update(sourceStampEncodedCertificate);
|
||||
byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
|
||||
if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
|
||||
result.addWarning(
|
||||
ApkVerificationIssue
|
||||
.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
|
||||
toHex(sourceStampBlockCertificateDigest),
|
||||
toHex(sourceStampCertificateDigest));
|
||||
return null;
|
||||
}
|
||||
return sourceStampCertificate;
|
||||
}
|
||||
|
||||
private static void verifySourceStampSignature(
|
||||
byte[] data,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
X509Certificate sourceStampCertificate,
|
||||
ByteBuffer signatures,
|
||||
ApkSignerInfo result) {
|
||||
// Parse the signatures block and identify supported signatures
|
||||
int signatureCount = 0;
|
||||
List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||
while (signatures.hasRemaining()) {
|
||||
signatureCount++;
|
||||
try {
|
||||
ByteBuffer signature = getLengthPrefixedSlice(signatures);
|
||||
int sigAlgorithmId = signature.getInt();
|
||||
byte[] sigBytes = readLengthPrefixedByteArray(signature);
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||
if (signatureAlgorithm == null) {
|
||||
result.addInfoMessage(
|
||||
ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
|
||||
sigAlgorithmId);
|
||||
continue;
|
||||
}
|
||||
supportedSignatures.add(
|
||||
new ApkSupportedSignature(signatureAlgorithm, sigBytes));
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addWarning(
|
||||
ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (supportedSignatures.isEmpty()) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
|
||||
return;
|
||||
}
|
||||
// Verify signatures over digests using the SourceStamp's certificate.
|
||||
List<ApkSupportedSignature> signaturesToVerify;
|
||||
try {
|
||||
signaturesToVerify =
|
||||
getSignaturesToVerify(
|
||||
supportedSignatures, minSdkVersion, maxSdkVersion, true);
|
||||
} catch (NoApkSupportedSignaturesException e) {
|
||||
// To facilitate debugging capture the signature algorithms and resulting exception in
|
||||
// the warning.
|
||||
StringBuilder signatureAlgorithms = new StringBuilder();
|
||||
for (ApkSupportedSignature supportedSignature : supportedSignatures) {
|
||||
if (signatureAlgorithms.length() > 0) {
|
||||
signatureAlgorithms.append(", ");
|
||||
}
|
||||
signatureAlgorithms.append(supportedSignature.algorithm);
|
||||
}
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
|
||||
signatureAlgorithms.toString(), e);
|
||||
return;
|
||||
}
|
||||
for (ApkSupportedSignature signature : signaturesToVerify) {
|
||||
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||
String jcaSignatureAlgorithm =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
PublicKey publicKey = sourceStampCertificate.getPublicKey();
|
||||
try {
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
sig.update(data);
|
||||
byte[] sigBytes = signature.signature;
|
||||
if (!sig.verify(sigBytes)) {
|
||||
result.addWarning(
|
||||
ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
|
||||
return;
|
||||
}
|
||||
} catch (InvalidKeyException
|
||||
| InvalidAlgorithmParameterException
|
||||
| SignatureException
|
||||
| NoSuchAlgorithmException e) {
|
||||
result.addWarning(
|
||||
ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseStampAttributes(ByteBuffer stampAttributeData,
|
||||
X509Certificate sourceStampCertificate, ApkSignerInfo result)
|
||||
throws ApkFormatException {
|
||||
ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
|
||||
int stampAttributeCount = 0;
|
||||
while (stampAttributes.hasRemaining()) {
|
||||
stampAttributeCount++;
|
||||
try {
|
||||
ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
|
||||
int id = attribute.getInt();
|
||||
byte[] value = ByteBufferUtils.toByteArray(attribute);
|
||||
if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) {
|
||||
readStampCertificateLineage(value, sourceStampCertificate, result);
|
||||
} else if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) {
|
||||
long timestamp = ByteBuffer.wrap(value).order(
|
||||
ByteOrder.LITTLE_ENDIAN).getLong();
|
||||
if (timestamp > 0) {
|
||||
result.timestamp = timestamp;
|
||||
} else {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
|
||||
timestamp);
|
||||
}
|
||||
} else {
|
||||
result.addInfoMessage(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
|
||||
}
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
|
||||
stampAttributeCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void readStampCertificateLineage(byte[] lineageBytes,
|
||||
X509Certificate sourceStampCertificate, ApkSignerInfo result) {
|
||||
try {
|
||||
// SourceStampCertificateLineage is verified when built
|
||||
List<SourceStampCertificateLineage.SigningCertificateNode> nodes =
|
||||
SourceStampCertificateLineage.readSigningCertificateLineage(
|
||||
ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN));
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
result.certificateLineage.add(nodes.get(i).signingCert);
|
||||
}
|
||||
// Make sure that the last cert in the chain matches this signer cert
|
||||
if (!sourceStampCertificate.equals(
|
||||
result.certificateLineage.get(result.certificateLineage.size() - 1))) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
|
||||
} catch (IllegalArgumentException e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
|
||||
} catch (Exception e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SourceStamp signer.
|
||||
*
|
||||
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||
*
|
||||
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||
*
|
||||
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||
* block.
|
||||
*
|
||||
* <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
|
||||
*/
|
||||
public abstract class V1SourceStampSigner {
|
||||
public static final int V1_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V1SourceStampSigner() {}
|
||||
|
||||
public static Pair<byte[], Integer> generateSourceStampBlock(
|
||||
SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
|
||||
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||
if (sourceStampSignerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
|
||||
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||
|
||||
SourceStampBlock sourceStampBlock = new SourceStampBlock();
|
||||
|
||||
try {
|
||||
sourceStampBlock.stampCertificate =
|
||||
sourceStampSignerConfig.certificates.get(0).getEncoded();
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new SignatureException(
|
||||
"Retrieving the encoded form of the stamp certificate failed", e);
|
||||
}
|
||||
|
||||
byte[] digestBytes =
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
|
||||
sourceStampBlock.signedDigests =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(
|
||||
sourceStampSignerConfig, digestBytes);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
|
||||
// * length-prefixed sequence of length-prefixed signatures:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: signature of signed data
|
||||
byte[] sourceStampSignerBlock =
|
||||
encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
sourceStampBlock.stampCertificate,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
sourceStampBlock.signedDigests),
|
||||
});
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed stamp block.
|
||||
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
|
||||
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
|
||||
}
|
||||
|
||||
private static final class SourceStampBlock {
|
||||
public byte[] stampCertificate;
|
||||
public List<Pair<Integer, byte[]>> signedDigests;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
import com.android.apksig.ApkVerifier;
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Source Stamp verifier.
|
||||
*
|
||||
* <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
|
||||
*/
|
||||
public abstract class V1SourceStampVerifier {
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V1SourceStampVerifier() {}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
|
||||
* The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
|
||||
* {@code true}. If verification fails, the result will contain errors -- see {@link
|
||||
* ApkSigningBlockUtils.Result#getErrors()}.
|
||||
*
|
||||
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||
* required cryptographic algorithm implementation is missing
|
||||
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
|
||||
* found
|
||||
* @throws IOException if an I/O error occurs when reading the APK
|
||||
*/
|
||||
public static ApkSigningBlockUtils.Result verify(
|
||||
DataSource apk,
|
||||
ApkUtils.ZipSections zipSections,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws IOException, NoSuchAlgorithmException,
|
||||
ApkSigningBlockUtils.SignatureNotFoundException {
|
||||
ApkSigningBlockUtils.Result result =
|
||||
new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
|
||||
SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtils.findSignature(
|
||||
apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
|
||||
|
||||
verify(
|
||||
signatureInfo.signatureBlock,
|
||||
sourceStampCertificateDigest,
|
||||
apkContentDigests,
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
|
||||
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
|
||||
* result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
|
||||
* more information about the contract of this method.
|
||||
*/
|
||||
private static void verify(
|
||||
ByteBuffer sourceStampBlock,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
ApkSigningBlockUtils.Result result)
|
||||
throws NoSuchAlgorithmException {
|
||||
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||
result.signers.add(signerInfo);
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
ByteBuffer sourceStampBlockData =
|
||||
ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
|
||||
byte[] digestBytes =
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
getApkDigests(apkContentDigests));
|
||||
SourceStampVerifier.verifyV1SourceStamp(
|
||||
sourceStampBlockData,
|
||||
certFactory,
|
||||
signerInfo,
|
||||
digestBytes,
|
||||
sourceStampCertificateDigest,
|
||||
minSdkVersion,
|
||||
maxSdkVersion);
|
||||
result.verified = !result.containsErrors() && !result.containsWarnings();
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Pair<Integer, byte[]>> getApkDigests(
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
|
||||
apkContentDigests.entrySet()) {
|
||||
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||
return digests;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
|
||||
import com.android.apksig.SigningCertificateLineage;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SourceStamp signer.
|
||||
*
|
||||
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||
*
|
||||
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||
*
|
||||
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||
* block.
|
||||
*
|
||||
* <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
|
||||
*/
|
||||
public class V2SourceStampSigner {
|
||||
public static final int V2_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
private final SignerConfig mSourceStampSignerConfig;
|
||||
private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
|
||||
private final boolean mSourceStampTimestampEnabled;
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V2SourceStampSigner(Builder builder) {
|
||||
mSourceStampSignerConfig = builder.mSourceStampSignerConfig;
|
||||
mSignatureSchemeDigestInfos = builder.mSignatureSchemeDigestInfos;
|
||||
mSourceStampTimestampEnabled = builder.mSourceStampTimestampEnabled;
|
||||
}
|
||||
|
||||
public static Pair<byte[], Integer> generateSourceStampBlock(
|
||||
SignerConfig sourceStampSignerConfig,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
|
||||
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||
return new Builder(sourceStampSignerConfig,
|
||||
signatureSchemeDigestInfos).build().generateSourceStampBlock();
|
||||
}
|
||||
|
||||
public Pair<byte[], Integer> generateSourceStampBlock()
|
||||
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||
if (mSourceStampSignerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
|
||||
// Extract the digests for signature schemes.
|
||||
List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
|
||||
getSignedDigestsFor(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V3,
|
||||
mSignatureSchemeDigestInfos,
|
||||
mSourceStampSignerConfig,
|
||||
signatureSchemeDigests);
|
||||
getSignedDigestsFor(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V2,
|
||||
mSignatureSchemeDigestInfos,
|
||||
mSourceStampSignerConfig,
|
||||
signatureSchemeDigests);
|
||||
getSignedDigestsFor(
|
||||
VERSION_JAR_SIGNATURE_SCHEME,
|
||||
mSignatureSchemeDigestInfos,
|
||||
mSourceStampSignerConfig,
|
||||
signatureSchemeDigests);
|
||||
Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
|
||||
|
||||
SourceStampBlock sourceStampBlock = new SourceStampBlock();
|
||||
|
||||
try {
|
||||
sourceStampBlock.stampCertificate =
|
||||
mSourceStampSignerConfig.certificates.get(0).getEncoded();
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new SignatureException(
|
||||
"Retrieving the encoded form of the stamp certificate failed", e);
|
||||
}
|
||||
|
||||
sourceStampBlock.signedDigests = signatureSchemeDigests;
|
||||
|
||||
sourceStampBlock.stampAttributes = encodeStampAttributes(
|
||||
generateStampAttributes(mSourceStampSignerConfig.signingCertificateLineage));
|
||||
sourceStampBlock.signedStampAttributes =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig,
|
||||
sourceStampBlock.stampAttributes);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
|
||||
// * length-prefixed sequence of length-prefixed signed signature scheme digests:
|
||||
// * uint32: signature scheme id
|
||||
// * length-prefixed bytes: signed digests for the respective signature scheme
|
||||
// * length-prefixed bytes: encoded stamp attributes
|
||||
// * length-prefixed sequence of length-prefixed signed stamp attributes:
|
||||
// * uint32: signature algorithm id
|
||||
// * length-prefixed bytes: signed stamp attributes for the respective signature algorithm
|
||||
byte[] sourceStampSignerBlock =
|
||||
encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][]{
|
||||
sourceStampBlock.stampCertificate,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
sourceStampBlock.signedDigests),
|
||||
sourceStampBlock.stampAttributes,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
sourceStampBlock.signedStampAttributes),
|
||||
});
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed stamp block.
|
||||
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
|
||||
}
|
||||
|
||||
private static void getSignedDigestsFor(
|
||||
int signatureSchemeVersion,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos,
|
||||
SignerConfig mSourceStampSignerConfig,
|
||||
List<Pair<Integer, byte[]>> signatureSchemeDigests)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||
if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<ContentDigestAlgorithm, byte[]> digestInfo =
|
||||
mSignatureSchemeDigestInfos.get(signatureSchemeVersion);
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
|
||||
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed digests:
|
||||
// * uint32: digest algorithm id
|
||||
// * length-prefixed bytes: digest of the respective digest algorithm
|
||||
byte[] digestBytes =
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed signed digests:
|
||||
// * uint32: signature algorithm id
|
||||
// * length-prefixed bytes: signed digest for the respective signature algorithm
|
||||
List<Pair<Integer, byte[]>> signedDigest =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(
|
||||
mSourceStampSignerConfig, digestBytes);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed signed signature scheme digests:
|
||||
// * uint32: signature scheme id
|
||||
// * length-prefixed bytes: signed digests for the respective signature scheme
|
||||
signatureSchemeDigests.add(
|
||||
Pair.of(
|
||||
signatureSchemeVersion,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
signedDigest)));
|
||||
}
|
||||
|
||||
private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) {
|
||||
int payloadSize = 0;
|
||||
for (byte[] attributeValue : stampAttributes.values()) {
|
||||
// Pair size + Attribute ID + Attribute value
|
||||
payloadSize += 4 + 4 + attributeValue.length;
|
||||
}
|
||||
|
||||
// FORMAT (little endian):
|
||||
// * length-prefixed bytes: pair
|
||||
// * uint32: ID
|
||||
// * bytes: value
|
||||
ByteBuffer result = ByteBuffer.allocate(4 + payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.putInt(payloadSize);
|
||||
for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) {
|
||||
// Pair size
|
||||
result.putInt(4 + stampAttribute.getValue().length);
|
||||
result.putInt(stampAttribute.getKey());
|
||||
result.put(stampAttribute.getValue());
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
|
||||
HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
|
||||
|
||||
if (mSourceStampTimestampEnabled) {
|
||||
// Write the current epoch time as the timestamp for the source stamp.
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
if (timestamp > 0) {
|
||||
ByteBuffer attributeBuffer = ByteBuffer.allocate(8);
|
||||
attributeBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
attributeBuffer.putLong(timestamp);
|
||||
stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID,
|
||||
attributeBuffer.array());
|
||||
} else {
|
||||
// The epoch time should never be <= 0, and since security decisions can potentially
|
||||
// be made based on the value in the timestamp, throw an Exception to ensure the
|
||||
// issues with the environment are resolved before allowing the signing.
|
||||
throw new IllegalStateException(
|
||||
"Received an invalid value from Instant#getTimestamp: " + timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
if (lineage != null) {
|
||||
stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID,
|
||||
lineage.encodeSigningCertificateLineage());
|
||||
}
|
||||
return stampAttributes;
|
||||
}
|
||||
|
||||
private static final class SourceStampBlock {
|
||||
public byte[] stampCertificate;
|
||||
public List<Pair<Integer, byte[]>> signedDigests;
|
||||
// Optional stamp attributes that are not required for verification.
|
||||
public byte[] stampAttributes;
|
||||
public List<Pair<Integer, byte[]>> signedStampAttributes;
|
||||
}
|
||||
|
||||
/** Builder of {@link V2SourceStampSigner} instances. */
|
||||
public static class Builder {
|
||||
private final SignerConfig mSourceStampSignerConfig;
|
||||
private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
|
||||
private boolean mSourceStampTimestampEnabled = true;
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Builder} with the provided {@code sourceStampSignerConfig}
|
||||
* and the {@code signatureSchemeDigestInfos}.
|
||||
*/
|
||||
public Builder(SignerConfig sourceStampSignerConfig,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) {
|
||||
mSourceStampSignerConfig = sourceStampSignerConfig;
|
||||
mSignatureSchemeDigestInfos = signatureSchemeDigestInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the source stamp should contain the timestamp attribute with the time
|
||||
* at which the source stamp was signed.
|
||||
*/
|
||||
public Builder setSourceStampTimestampEnabled(boolean value) {
|
||||
mSourceStampTimestampEnabled = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new V2SourceStampSigner that can be used to generate a new source stamp
|
||||
* block signed with the specified signing config.
|
||||
*/
|
||||
public V2SourceStampSigner build() {
|
||||
return new V2SourceStampSigner(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
import com.android.apksig.Constants;
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSigResult;
|
||||
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.apk.SignatureNotFoundException;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Source Stamp verifier.
|
||||
*
|
||||
* <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes.
|
||||
*/
|
||||
public abstract class V2SourceStampVerifier {
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V2SourceStampVerifier() {}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
|
||||
* The APK must be considered verified only if {@link ApkSigResult#verified} is
|
||||
* {@code true}. If verification fails, the result will contain errors -- see {@link
|
||||
* ApkSigResult#getErrors()}.
|
||||
*
|
||||
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||
* required cryptographic algorithm implementation is missing
|
||||
* @throws SignatureNotFoundException if no SourceStamp signatures are
|
||||
* found
|
||||
* @throws IOException if an I/O error occurs when reading the APK
|
||||
*/
|
||||
public static ApkSigResult verify(
|
||||
DataSource apk,
|
||||
ZipSections zipSections,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||
ApkSigResult result =
|
||||
new ApkSigResult(Constants.VERSION_SOURCE_STAMP);
|
||||
SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtilsLite.findSignature(
|
||||
apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID);
|
||||
|
||||
verify(
|
||||
signatureInfo.signatureBlock,
|
||||
sourceStampCertificateDigest,
|
||||
signatureSchemeApkContentDigests,
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
|
||||
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
|
||||
* result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for
|
||||
* more information about the contract of this method.
|
||||
*/
|
||||
private static void verify(
|
||||
ByteBuffer sourceStampBlock,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
ApkSigResult result)
|
||||
throws NoSuchAlgorithmException {
|
||||
ApkSignerInfo signerInfo = new ApkSignerInfo();
|
||||
result.mSigners.add(signerInfo);
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
ByteBuffer sourceStampBlockData =
|
||||
ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock);
|
||||
SourceStampVerifier.verifyV2SourceStamp(
|
||||
sourceStampBlockData,
|
||||
certFactory,
|
||||
signerInfo,
|
||||
getSignatureSchemeDigests(signatureSchemeApkContentDigests),
|
||||
sourceStampCertificateDigest,
|
||||
minSdkVersion,
|
||||
maxSdkVersion);
|
||||
result.verified = !result.containsErrors() && !result.containsWarnings();
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<Integer, byte[]> getSignatureSchemeDigests(
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) {
|
||||
Map<Integer, byte[]> digests = new HashMap<>();
|
||||
for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>>
|
||||
signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) {
|
||||
List<Pair<Integer, byte[]>> apkDigests =
|
||||
getApkDigests(signatureSchemeApkContentDigest.getValue());
|
||||
digests.put(
|
||||
signatureSchemeApkContentDigest.getKey(),
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests));
|
||||
}
|
||||
return digests;
|
||||
}
|
||||
|
||||
private static List<Pair<Integer, byte[]>> getApkDigests(
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
|
||||
apkContentDigests.entrySet()) {
|
||||
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, new Comparator<Pair<Integer, byte[]>>() {
|
||||
@Override
|
||||
public int compare(Pair<Integer, byte[]> pair1, Pair<Integer, byte[]> pair2) {
|
||||
return pair1.getFirst() - pair2.getFirst();
|
||||
}
|
||||
});
|
||||
return digests;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v1;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Digest algorithm used with JAR signing (aka v1 signing scheme).
|
||||
*/
|
||||
public enum DigestAlgorithm {
|
||||
/** SHA-1 */
|
||||
SHA1("SHA-1"),
|
||||
|
||||
/** SHA2-256 */
|
||||
SHA256("SHA-256");
|
||||
|
||||
private final String mJcaMessageDigestAlgorithm;
|
||||
|
||||
private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
|
||||
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link java.security.MessageDigest} algorithm represented by this digest
|
||||
* algorithm.
|
||||
*/
|
||||
String getJcaMessageDigestAlgorithm() {
|
||||
return mJcaMessageDigestAlgorithm;
|
||||
}
|
||||
|
||||
public static Comparator<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator();
|
||||
|
||||
private static class StrengthComparator implements Comparator<DigestAlgorithm> {
|
||||
@Override
|
||||
public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
|
||||
switch (a1) {
|
||||
case SHA1:
|
||||
switch (a2) {
|
||||
case SHA1:
|
||||
return 0;
|
||||
case SHA256:
|
||||
return -1;
|
||||
}
|
||||
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||
|
||||
case SHA256:
|
||||
switch (a2) {
|
||||
case SHA1:
|
||||
return 1;
|
||||
case SHA256:
|
||||
return 0;
|
||||
}
|
||||
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unsupported algorithm: " + a1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v1;
|
||||
|
||||
/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
|
||||
public class V1SchemeConstants {
|
||||
private V1SchemeConstants() {}
|
||||
|
||||
public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
|
||||
public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
|
||||
"X-Android-APK-Signed";
|
||||
}
|
||||
|
|
@ -0,0 +1,586 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v1;
|
||||
|
||||
import static com.android.apksig.Constants.MAX_APK_SIGNERS;
|
||||
import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
|
||||
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid;
|
||||
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.asn1.Asn1EncodingException;
|
||||
import com.android.apksig.internal.jar.ManifestWriter;
|
||||
import com.android.apksig.internal.jar.SignatureFileWriter;
|
||||
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
/**
|
||||
* APK signer which uses JAR signing (aka v1 signing scheme).
|
||||
*
|
||||
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
|
||||
*/
|
||||
public abstract class V1SchemeSigner {
|
||||
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||
|
||||
private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
|
||||
new Attributes.Name("Created-By");
|
||||
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
|
||||
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
|
||||
|
||||
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
|
||||
new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
|
||||
|
||||
/**
|
||||
* Signer configuration.
|
||||
*/
|
||||
public static class SignerConfig {
|
||||
/** Name. */
|
||||
public String name;
|
||||
|
||||
/** Private key. */
|
||||
public PrivateKey privateKey;
|
||||
|
||||
/**
|
||||
* Certificates, with the first certificate containing the public key corresponding to
|
||||
* {@link #privateKey}.
|
||||
*/
|
||||
public List<X509Certificate> certificates;
|
||||
|
||||
/**
|
||||
* Digest algorithm used for the signature.
|
||||
*/
|
||||
public DigestAlgorithm signatureDigestAlgorithm;
|
||||
|
||||
/**
|
||||
* If DSA is the signing algorithm, whether or not deterministic DSA signing should be used.
|
||||
*/
|
||||
public boolean deterministicDsaSigning;
|
||||
}
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V1SchemeSigner() {}
|
||||
|
||||
/**
|
||||
* Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
|
||||
*
|
||||
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||
* AndroidManifest.xml minSdkVersion attribute)
|
||||
*
|
||||
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
|
||||
* JAR signing (aka v1 signature scheme)
|
||||
*/
|
||||
public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
|
||||
PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
|
||||
String keyAlgorithm = signingKey.getAlgorithm();
|
||||
if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) {
|
||||
// Prior to API Level 18, only SHA-1 can be used with RSA.
|
||||
if (minSdkVersion < 18) {
|
||||
return DigestAlgorithm.SHA1;
|
||||
}
|
||||
return DigestAlgorithm.SHA256;
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// Prior to API Level 21, only SHA-1 can be used with DSA
|
||||
if (minSdkVersion < 21) {
|
||||
return DigestAlgorithm.SHA1;
|
||||
} else {
|
||||
return DigestAlgorithm.SHA256;
|
||||
}
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
if (minSdkVersion < 18) {
|
||||
throw new InvalidKeyException(
|
||||
"ECDSA signatures only supported for minSdkVersion 18 and higher");
|
||||
}
|
||||
return DigestAlgorithm.SHA256;
|
||||
} else {
|
||||
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a safe version of the provided signer name.
|
||||
*/
|
||||
public static String getSafeSignerName(String name) {
|
||||
if (name.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty name");
|
||||
}
|
||||
|
||||
// According to https://docs.oracle.com/javase/tutorial/deployment/jar/signing.html, the
|
||||
// name must not be longer than 8 characters and may contain only A-Z, 0-9, _, and -.
|
||||
StringBuilder result = new StringBuilder();
|
||||
char[] nameCharsUpperCase = name.toUpperCase(Locale.US).toCharArray();
|
||||
for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) {
|
||||
char c = nameCharsUpperCase[i];
|
||||
if (((c >= 'A') && (c <= 'Z'))
|
||||
|| ((c >= '0') && (c <= '9'))
|
||||
|| (c == '-')
|
||||
|| (c == '_')) {
|
||||
result.append(c);
|
||||
} else {
|
||||
result.append('_');
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
|
||||
*/
|
||||
private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm)
|
||||
throws NoSuchAlgorithmException {
|
||||
String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
|
||||
return MessageDigest.getInstance(jcaAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
|
||||
* algorithm.
|
||||
*/
|
||||
public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
|
||||
return digestAlgorithm.getJcaMessageDigestAlgorithm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
|
||||
* manifest.
|
||||
*/
|
||||
public static boolean isJarEntryDigestNeededInManifest(String entryName) {
|
||||
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
|
||||
|
||||
// Entries which represent directories sould not be listed in the manifest.
|
||||
if (entryName.endsWith("/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Entries outside of META-INF must be listed in the manifest.
|
||||
if (!entryName.startsWith("META-INF/")) {
|
||||
return true;
|
||||
}
|
||||
// Entries in subdirectories of META-INF must be listed in the manifest.
|
||||
if (entryName.indexOf('/', "META-INF/".length()) != -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ignored file names (case-insensitive) in META-INF directory:
|
||||
// MANIFEST.MF
|
||||
// *.SF
|
||||
// *.RSA
|
||||
// *.DSA
|
||||
// *.EC
|
||||
// SIG-*
|
||||
String fileNameLowerCase =
|
||||
entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
|
||||
if (("manifest.mf".equals(fileNameLowerCase))
|
||||
|| (fileNameLowerCase.endsWith(".sf"))
|
||||
|| (fileNameLowerCase.endsWith(".rsa"))
|
||||
|| (fileNameLowerCase.endsWith(".dsa"))
|
||||
|| (fileNameLowerCase.endsWith(".ec"))
|
||||
|| (fileNameLowerCase.startsWith("sig-"))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
||||
* JAR entries which need to be added to the APK as part of the signature.
|
||||
*
|
||||
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
||||
* must be provided.
|
||||
*
|
||||
* @throws ApkFormatException if the source manifest is malformed
|
||||
* @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
|
||||
* missing
|
||||
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
||||
* cannot be used in general
|
||||
* @throws SignatureException if an error occurs when computing digests of generating
|
||||
* signatures
|
||||
*/
|
||||
public static List<Pair<String, byte[]>> sign(
|
||||
List<SignerConfig> signerConfigs,
|
||||
DigestAlgorithm jarEntryDigestAlgorithm,
|
||||
Map<String, byte[]> jarEntryDigests,
|
||||
List<Integer> apkSigningSchemeIds,
|
||||
byte[] sourceManifestBytes,
|
||||
String createdBy)
|
||||
throws NoSuchAlgorithmException, ApkFormatException, InvalidKeyException,
|
||||
CertificateException, SignatureException {
|
||||
if (signerConfigs.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||
}
|
||||
if (signerConfigs.size() > MAX_APK_SIGNERS) {
|
||||
throw new IllegalArgumentException(
|
||||
"APK Signature Scheme v1 only supports a maximum of " + MAX_APK_SIGNERS + ", "
|
||||
+ signerConfigs.size() + " provided");
|
||||
}
|
||||
OutputManifestFile manifest =
|
||||
generateManifestFile(
|
||||
jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
|
||||
|
||||
return signManifest(
|
||||
signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, createdBy, manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
||||
* JAR entries which need to be added to the APK as part of the signature.
|
||||
*
|
||||
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
||||
* must be provided.
|
||||
*
|
||||
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
||||
* cannot be used in general
|
||||
* @throws SignatureException if an error occurs when computing digests of generating
|
||||
* signatures
|
||||
*/
|
||||
public static List<Pair<String, byte[]>> signManifest(
|
||||
List<SignerConfig> signerConfigs,
|
||||
DigestAlgorithm digestAlgorithm,
|
||||
List<Integer> apkSigningSchemeIds,
|
||||
String createdBy,
|
||||
OutputManifestFile manifest)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
||||
SignatureException {
|
||||
if (signerConfigs.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||
}
|
||||
|
||||
// For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
|
||||
List<Pair<String, byte[]>> signatureJarEntries =
|
||||
new ArrayList<>(2 * signerConfigs.size() + 1);
|
||||
byte[] sfBytes =
|
||||
generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, createdBy, manifest);
|
||||
for (SignerConfig signerConfig : signerConfigs) {
|
||||
String signerName = signerConfig.name;
|
||||
byte[] signatureBlock;
|
||||
try {
|
||||
signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException(
|
||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||
} catch (CertificateException e) {
|
||||
throw new CertificateException(
|
||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException(
|
||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||
}
|
||||
signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
|
||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||
String signatureBlockFileName =
|
||||
"META-INF/" + signerName + "."
|
||||
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
|
||||
signatureJarEntries.add(
|
||||
Pair.of(signatureBlockFileName, signatureBlock));
|
||||
}
|
||||
signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents));
|
||||
return signatureJarEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of JAR entries which this signer will produce as part of v1 signature.
|
||||
*/
|
||||
public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) {
|
||||
Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1);
|
||||
for (SignerConfig signerConfig : signerConfigs) {
|
||||
String signerName = signerConfig.name;
|
||||
result.add("META-INF/" + signerName + ".SF");
|
||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||
String signatureBlockFileName =
|
||||
"META-INF/" + signerName + "."
|
||||
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
|
||||
result.add(signatureBlockFileName);
|
||||
}
|
||||
result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
|
||||
* input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
|
||||
*/
|
||||
public static OutputManifestFile generateManifestFile(
|
||||
DigestAlgorithm jarEntryDigestAlgorithm,
|
||||
Map<String, byte[]> jarEntryDigests,
|
||||
byte[] sourceManifestBytes) throws ApkFormatException {
|
||||
Manifest sourceManifest = null;
|
||||
if (sourceManifestBytes != null) {
|
||||
try {
|
||||
sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
|
||||
} catch (IOException e) {
|
||||
throw new ApkFormatException("Malformed source META-INF/MANIFEST.MF", e);
|
||||
}
|
||||
}
|
||||
ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
|
||||
Attributes mainAttrs = new Attributes();
|
||||
// Copy the main section from the source manifest (if provided). Otherwise use defaults.
|
||||
// NOTE: We don't output our own Created-By header because this signer did not create the
|
||||
// JAR/APK being signed -- the signer only adds signatures to the already existing
|
||||
// JAR/APK.
|
||||
if (sourceManifest != null) {
|
||||
mainAttrs.putAll(sourceManifest.getMainAttributes());
|
||||
} else {
|
||||
mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
|
||||
}
|
||||
|
||||
try {
|
||||
ManifestWriter.writeMainSection(manifestOut, mainAttrs);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
|
||||
}
|
||||
|
||||
List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
|
||||
Collections.sort(sortedEntryNames);
|
||||
SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>();
|
||||
String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
|
||||
for (String entryName : sortedEntryNames) {
|
||||
checkEntryNameValid(entryName);
|
||||
byte[] entryDigest = jarEntryDigests.get(entryName);
|
||||
Attributes entryAttrs = new Attributes();
|
||||
entryAttrs.putValue(
|
||||
entryDigestAttributeName,
|
||||
Base64.getEncoder().encodeToString(entryDigest));
|
||||
ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
|
||||
byte[] sectionBytes;
|
||||
try {
|
||||
ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
|
||||
sectionBytes = sectionOut.toByteArray();
|
||||
manifestOut.write(sectionBytes);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
|
||||
}
|
||||
invidualSectionsContents.put(entryName, sectionBytes);
|
||||
}
|
||||
|
||||
OutputManifestFile result = new OutputManifestFile();
|
||||
result.contents = manifestOut.toByteArray();
|
||||
result.mainSectionAttributes = mainAttrs;
|
||||
result.individualSectionsContents = invidualSectionsContents;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void checkEntryNameValid(String name) throws ApkFormatException {
|
||||
// JAR signing spec says CR, LF, and NUL are not permitted in entry names
|
||||
// CR or LF in entry names will result in malformed MANIFEST.MF and .SF files because there
|
||||
// is no way to escape characters in MANIFEST.MF and .SF files. NUL can, presumably, cause
|
||||
// issues when parsing using C and C++ like languages.
|
||||
for (char c : name.toCharArray()) {
|
||||
if ((c == '\r') || (c == '\n') || (c == 0)) {
|
||||
throw new ApkFormatException(
|
||||
String.format(
|
||||
"Unsupported character 0x%1$02x in ZIP entry name \"%2$s\"",
|
||||
(int) c,
|
||||
name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class OutputManifestFile {
|
||||
public byte[] contents;
|
||||
public SortedMap<String, byte[]> individualSectionsContents;
|
||||
public Attributes mainSectionAttributes;
|
||||
}
|
||||
|
||||
private static byte[] generateSignatureFile(
|
||||
List<Integer> apkSignatureSchemeIds,
|
||||
DigestAlgorithm manifestDigestAlgorithm,
|
||||
String createdBy,
|
||||
OutputManifestFile manifest) throws NoSuchAlgorithmException {
|
||||
Manifest sf = new Manifest();
|
||||
Attributes mainAttrs = sf.getMainAttributes();
|
||||
mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
|
||||
mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, createdBy);
|
||||
if (!apkSignatureSchemeIds.isEmpty()) {
|
||||
// Add APK Signature Scheme v2 (and newer) signature stripping protection.
|
||||
// This attribute indicates that this APK is supposed to have been signed using one or
|
||||
// more APK-specific signature schemes in addition to the standard JAR signature scheme
|
||||
// used by this code. APK signature verifier should reject the APK if it does not
|
||||
// contain a signature for the signature scheme the verifier prefers out of this set.
|
||||
StringBuilder attrValue = new StringBuilder();
|
||||
for (int id : apkSignatureSchemeIds) {
|
||||
if (attrValue.length() > 0) {
|
||||
attrValue.append(", ");
|
||||
}
|
||||
attrValue.append(String.valueOf(id));
|
||||
}
|
||||
mainAttrs.put(
|
||||
SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
|
||||
attrValue.toString());
|
||||
}
|
||||
|
||||
// Add main attribute containing the digest of MANIFEST.MF.
|
||||
MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
|
||||
mainAttrs.putValue(
|
||||
getManifestDigestAttributeName(manifestDigestAlgorithm),
|
||||
Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try {
|
||||
SignatureFileWriter.writeMainSection(out, mainAttrs);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write in-memory .SF file", e);
|
||||
}
|
||||
String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
|
||||
for (Map.Entry<String, byte[]> manifestSection
|
||||
: manifest.individualSectionsContents.entrySet()) {
|
||||
String sectionName = manifestSection.getKey();
|
||||
byte[] sectionContents = manifestSection.getValue();
|
||||
byte[] sectionDigest = md.digest(sectionContents);
|
||||
Attributes attrs = new Attributes();
|
||||
attrs.putValue(
|
||||
entryDigestAttributeName,
|
||||
Base64.getEncoder().encodeToString(sectionDigest));
|
||||
|
||||
try {
|
||||
SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write in-memory .SF file", e);
|
||||
}
|
||||
}
|
||||
|
||||
// A bug in the java.util.jar implementation of Android platforms up to version 1.6 will
|
||||
// cause a spurious IOException to be thrown if the length of the signature file is a
|
||||
// multiple of 1024 bytes. As a workaround, add an extra CRLF in this case.
|
||||
if ((out.size() > 0) && ((out.size() % 1024) == 0)) {
|
||||
try {
|
||||
SignatureFileWriter.writeSectionDelimiter(out);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
|
||||
}
|
||||
}
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
|
||||
* signing configuration.
|
||||
*/
|
||||
private static byte[] generateSignatureBlock(
|
||||
SignerConfig signerConfig, byte[] signatureFileBytes)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
||||
SignatureException {
|
||||
// Obtain relevant bits of signing configuration
|
||||
List<X509Certificate> signerCerts = signerConfig.certificates;
|
||||
X509Certificate signingCert = signerCerts.get(0);
|
||||
PublicKey publicKey = signingCert.getPublicKey();
|
||||
DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
|
||||
Pair<String, AlgorithmIdentifier> signatureAlgs =
|
||||
getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm,
|
||||
signerConfig.deterministicDsaSigning);
|
||||
String jcaSignatureAlgorithm = signatureAlgs.getFirst();
|
||||
|
||||
// Generate the cryptographic signature of the signature file
|
||||
byte[] signatureBytes;
|
||||
try {
|
||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
signature.initSign(signerConfig.privateKey);
|
||||
signature.update(signatureFileBytes);
|
||||
signatureBytes = signature.sign();
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
||||
}
|
||||
|
||||
// Verify the signature against the public key in the signing certificate
|
||||
try {
|
||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(signatureFileBytes);
|
||||
if (!signature.verify(signatureBytes)) {
|
||||
throw new SignatureException("Signature did not verify");
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException(
|
||||
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
||||
+ " public key from certificate",
|
||||
e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException(
|
||||
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
||||
+ " public key from certificate",
|
||||
e);
|
||||
}
|
||||
|
||||
AlgorithmIdentifier digestAlgorithmId =
|
||||
getSignerInfoDigestAlgorithmOid(digestAlgorithm);
|
||||
AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
|
||||
try {
|
||||
return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
|
||||
signatureBytes,
|
||||
null,
|
||||
signerCerts, digestAlgorithmId,
|
||||
signatureAlgorithmId);
|
||||
} catch (Asn1EncodingException | CertificateEncodingException ex) {
|
||||
throw new SignatureException("Failed to encode signature block");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
return "SHA1-Digest";
|
||||
case SHA256:
|
||||
return "SHA-256-Digest";
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected content digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
return "SHA1-Digest-Manifest";
|
||||
case SHA256:
|
||||
return "SHA-256-Digest-Manifest";
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected content digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v2;
|
||||
|
||||
/** Constants used by the V2 Signature Scheme signing and verification. */
|
||||
public class V2SchemeConstants {
|
||||
private V2SchemeConstants() {}
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
||||
public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
|
||||
}
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v2;
|
||||
|
||||
import static com.android.apksig.Constants.MAX_APK_SIGNERS;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
|
||||
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.RunnablesExecutor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.interfaces.ECKey;
|
||||
import java.security.interfaces.RSAKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 signer.
|
||||
*
|
||||
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
||||
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||
* uncompressed contents of ZIP entries.
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||
*/
|
||||
public abstract class V2SchemeSigner {
|
||||
/*
|
||||
* The two main goals of APK Signature Scheme v2 are:
|
||||
* 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
|
||||
* cover every byte of the APK being signed.
|
||||
* 2. Enable much faster signature and integrity verification. This is achieved by requiring
|
||||
* only a minimal amount of APK parsing before the signature is verified, thus completely
|
||||
* bypassing ZIP entry decompression and by making integrity verification parallelizable by
|
||||
* employing a hash tree.
|
||||
*
|
||||
* The generated signature block is wrapped into an APK Signing Block and inserted into the
|
||||
* original APK immediately before the start of ZIP Central Directory. This is to ensure that
|
||||
* JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
|
||||
* extensibility. For example, a future signature scheme could insert its signatures there as
|
||||
* well. The contract of the APK Signing Block is that all contents outside of the block must be
|
||||
* protected by signatures inside the block.
|
||||
*/
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
|
||||
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V2SchemeSigner() {}
|
||||
|
||||
/**
|
||||
* Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
|
||||
* provided key.
|
||||
*
|
||||
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||
* AndroidManifest.xml minSdkVersion attribute).
|
||||
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
|
||||
* Signature Scheme v2
|
||||
*/
|
||||
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
|
||||
int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
|
||||
throws InvalidKeyException {
|
||||
String keyAlgorithm = signingKey.getAlgorithm();
|
||||
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
|
||||
// deterministic signatures which make life easier for OTA updates (fewer files
|
||||
// changed when deterministic signature schemes are used).
|
||||
|
||||
// Pick a digest which is no weaker than the key.
|
||||
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
|
||||
if (modulusLengthBits <= 3072) {
|
||||
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
|
||||
if (verityEnabled) {
|
||||
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
|
||||
}
|
||||
return algorithms;
|
||||
} else {
|
||||
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
|
||||
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
|
||||
}
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// DSA is supported only with SHA-256.
|
||||
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||
algorithms.add(
|
||||
deterministicDsaSigning ?
|
||||
SignatureAlgorithm.DETDSA_WITH_SHA256 :
|
||||
SignatureAlgorithm.DSA_WITH_SHA256);
|
||||
if (verityEnabled) {
|
||||
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
|
||||
}
|
||||
return algorithms;
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// Pick a digest which is no weaker than the key.
|
||||
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
|
||||
if (keySizeBits <= 256) {
|
||||
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
|
||||
if (verityEnabled) {
|
||||
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
|
||||
}
|
||||
return algorithms;
|
||||
} else {
|
||||
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
|
||||
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
|
||||
generateApkSignatureSchemeV2Block(RunnablesExecutor executor,
|
||||
DataSource beforeCentralDir,
|
||||
DataSource centralDir,
|
||||
DataSource eocd,
|
||||
List<SignerConfig> signerConfigs,
|
||||
boolean v3SigningEnabled)
|
||||
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
|
||||
SignatureException {
|
||||
return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd,
|
||||
signerConfigs, v3SigningEnabled, null);
|
||||
}
|
||||
|
||||
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
|
||||
generateApkSignatureSchemeV2Block(
|
||||
RunnablesExecutor executor,
|
||||
DataSource beforeCentralDir,
|
||||
DataSource centralDir,
|
||||
DataSource eocd,
|
||||
List<SignerConfig> signerConfigs,
|
||||
boolean v3SigningEnabled,
|
||||
List<byte[]> preservedV2SignerBlocks)
|
||||
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
|
||||
SignatureException {
|
||||
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
|
||||
ApkSigningBlockUtils.computeContentDigests(
|
||||
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
|
||||
return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
|
||||
generateApkSignatureSchemeV2Block(
|
||||
digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled,
|
||||
preservedV2SignerBlocks),
|
||||
digestInfo.getSecond());
|
||||
}
|
||||
|
||||
private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
|
||||
List<SignerConfig> signerConfigs,
|
||||
Map<ContentDigestAlgorithm, byte[]> contentDigests,
|
||||
boolean v3SigningEnabled,
|
||||
List<byte[]> preservedV2SignerBlocks)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed signer blocks.
|
||||
|
||||
if (signerConfigs.size() > MAX_APK_SIGNERS) {
|
||||
throw new IllegalArgumentException(
|
||||
"APK Signature Scheme v2 only supports a maximum of " + MAX_APK_SIGNERS + ", "
|
||||
+ signerConfigs.size() + " provided");
|
||||
}
|
||||
|
||||
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
|
||||
if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) {
|
||||
signerBlocks.addAll(preservedV2SignerBlocks);
|
||||
}
|
||||
int signerNumber = 0;
|
||||
for (SignerConfig signerConfig : signerConfigs) {
|
||||
signerNumber++;
|
||||
byte[] signerBlock;
|
||||
try {
|
||||
signerBlock = generateSignerBlock(signerConfig, contentDigests, v3SigningEnabled);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
||||
}
|
||||
signerBlocks.add(signerBlock);
|
||||
}
|
||||
|
||||
return Pair.of(
|
||||
encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
||||
}),
|
||||
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||
}
|
||||
|
||||
private static byte[] generateSignerBlock(
|
||||
SignerConfig signerConfig,
|
||||
Map<ContentDigestAlgorithm, byte[]> contentDigests,
|
||||
boolean v3SigningEnabled)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||
if (signerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||
|
||||
byte[] encodedPublicKey = encodePublicKey(publicKey);
|
||||
|
||||
V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
|
||||
try {
|
||||
signedData.certificates = encodeCertificates(signerConfig.certificates);
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new SignatureException("Failed to encode certificates", e);
|
||||
}
|
||||
|
||||
List<Pair<Integer, byte[]>> digests =
|
||||
new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
||||
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||
ContentDigestAlgorithm contentDigestAlgorithm =
|
||||
signatureAlgorithm.getContentDigestAlgorithm();
|
||||
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
|
||||
if (contentDigest == null) {
|
||||
throw new RuntimeException(
|
||||
contentDigestAlgorithm
|
||||
+ " content digest for "
|
||||
+ signatureAlgorithm
|
||||
+ " not computed");
|
||||
}
|
||||
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
|
||||
}
|
||||
signedData.digests = digests;
|
||||
signedData.additionalAttributes = generateAdditionalAttributes(v3SigningEnabled);
|
||||
|
||||
V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed digests:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: digest of contents
|
||||
// * length-prefixed sequence of certificates:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||
// * length-prefixed sequence of length-prefixed additional attributes:
|
||||
// * uint32: ID
|
||||
// * (length - 4) bytes: value
|
||||
|
||||
signer.signedData =
|
||||
encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
signedData.digests),
|
||||
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
|
||||
signedData.additionalAttributes,
|
||||
new byte[0],
|
||||
});
|
||||
signer.publicKey = encodedPublicKey;
|
||||
signer.signatures = new ArrayList<>();
|
||||
signer.signatures =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed signed data
|
||||
// * length-prefixed sequence of length-prefixed signatures:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: signature of signed data
|
||||
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
|
||||
return encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
signer.signedData,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
signer.signatures),
|
||||
signer.publicKey,
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) {
|
||||
if (v3SigningEnabled) {
|
||||
// FORMAT (little endian):
|
||||
// * length-prefixed bytes: attribute pair
|
||||
// * uint32: ID - STRIPPING_PROTECTION_ATTR_ID in this case
|
||||
// * uint32: value - 3 (v3 signature scheme id) in this case
|
||||
int payloadSize = 4 + 4 + 4;
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.putInt(payloadSize - 4);
|
||||
result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID);
|
||||
result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
|
||||
return result.array();
|
||||
} else {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
private static final class V2SignatureSchemeBlock {
|
||||
private static final class Signer {
|
||||
public byte[] signedData;
|
||||
public List<Pair<Integer, byte[]>> signatures;
|
||||
public byte[] publicKey;
|
||||
}
|
||||
|
||||
private static final class SignedData {
|
||||
public List<Pair<Integer, byte[]>> digests;
|
||||
public List<byte[]> certificates;
|
||||
public byte[] additionalAttributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v2;
|
||||
|
||||
import static com.android.apksig.Constants.MAX_APK_SIGNERS;
|
||||
|
||||
import com.android.apksig.ApkVerifier.Issue;
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.RunnablesExecutor;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 verifier.
|
||||
*
|
||||
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
||||
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||
* uncompressed contents of ZIP entries.
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||
*/
|
||||
public abstract class V2SchemeVerifier {
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V2SchemeVerifier() {}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of
|
||||
* verification. The APK must be considered verified only if
|
||||
* {@link ApkSigningBlockUtils.Result#verified} is
|
||||
* {@code true}. If verification fails, the result will contain errors -- see
|
||||
* {@link ApkSigningBlockUtils.Result#getErrors()}.
|
||||
*
|
||||
* <p>Verification succeeds iff the APK's APK Signature Scheme v2 signatures are expected to
|
||||
* verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
* If the APK's signature is expected to not verify on any of the specified platform versions,
|
||||
* this method returns a result with one or more errors and whose
|
||||
* {@code Result.verified == false}, or this method throws an exception.
|
||||
*
|
||||
* @throws ApkFormatException if the APK is malformed
|
||||
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||
* required cryptographic algorithm implementation is missing
|
||||
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no APK Signature Scheme v2
|
||||
* signatures are found
|
||||
* @throws IOException if an I/O error occurs when reading the APK
|
||||
*/
|
||||
public static ApkSigningBlockUtils.Result verify(
|
||||
RunnablesExecutor executor,
|
||||
DataSource apk,
|
||||
ApkUtils.ZipSections zipSections,
|
||||
Map<Integer, String> supportedApkSigSchemeNames,
|
||||
Set<Integer> foundSigSchemeIds,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||
ApkSigningBlockUtils.SignatureNotFoundException {
|
||||
ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
||||
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
|
||||
SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtils.findSignature(apk, zipSections,
|
||||
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
|
||||
|
||||
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
|
||||
DataSource centralDir =
|
||||
apk.slice(
|
||||
signatureInfo.centralDirOffset,
|
||||
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
|
||||
ByteBuffer eocd = signatureInfo.eocd;
|
||||
|
||||
verify(executor,
|
||||
beforeApkSigningBlock,
|
||||
signatureInfo.signatureBlock,
|
||||
centralDir,
|
||||
eocd,
|
||||
supportedApkSigSchemeNames,
|
||||
foundSigSchemeIds,
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's v2 signatures and outputs the results into the provided
|
||||
* {@code result}. APK is considered verified only if there are no errors reported in the
|
||||
* {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, Map,
|
||||
* Set, int, int)} for more information about the contract of this method.
|
||||
*
|
||||
* @param result result populated by this method with interesting information about the APK,
|
||||
* such as information about signers, and verification errors and warnings.
|
||||
*/
|
||||
private static void verify(
|
||||
RunnablesExecutor executor,
|
||||
DataSource beforeApkSigningBlock,
|
||||
ByteBuffer apkSignatureSchemeV2Block,
|
||||
DataSource centralDir,
|
||||
ByteBuffer eocd,
|
||||
Map<Integer, String> supportedApkSigSchemeNames,
|
||||
Set<Integer> foundSigSchemeIds,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
ApkSigningBlockUtils.Result result)
|
||||
throws IOException, NoSuchAlgorithmException {
|
||||
Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
|
||||
parseSigners(
|
||||
apkSignatureSchemeV2Block,
|
||||
contentDigestsToVerify,
|
||||
supportedApkSigSchemeNames,
|
||||
foundSigSchemeIds,
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
result);
|
||||
if (result.containsErrors()) {
|
||||
return;
|
||||
}
|
||||
ApkSigningBlockUtils.verifyIntegrity(
|
||||
executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
|
||||
if (!result.containsErrors()) {
|
||||
result.verified = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses each signer in the provided APK Signature Scheme v2 block and populates corresponding
|
||||
* {@code signerInfos} of the provided {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
|
||||
* However, this does not verify the integrity of the rest of the APK but rather simply reports
|
||||
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
public static void parseSigners(
|
||||
ByteBuffer apkSignatureSchemeV2Block,
|
||||
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||
Map<Integer, String> supportedApkSigSchemeNames,
|
||||
Set<Integer> foundApkSigSchemeIds,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
|
||||
ByteBuffer signers;
|
||||
try {
|
||||
signers = ApkSigningBlockUtils.getLengthPrefixedSlice(apkSignatureSchemeV2Block);
|
||||
} catch (ApkFormatException e) {
|
||||
result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
|
||||
return;
|
||||
}
|
||||
if (!signers.hasRemaining()) {
|
||||
result.addError(Issue.V2_SIG_NO_SIGNERS);
|
||||
return;
|
||||
}
|
||||
|
||||
CertificateFactory certFactory;
|
||||
try {
|
||||
certFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||
}
|
||||
int signerCount = 0;
|
||||
while (signers.hasRemaining()) {
|
||||
int signerIndex = signerCount;
|
||||
signerCount++;
|
||||
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||
signerInfo.index = signerIndex;
|
||||
result.signers.add(signerInfo);
|
||||
try {
|
||||
ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers);
|
||||
parseSigner(
|
||||
signer,
|
||||
certFactory,
|
||||
signerInfo,
|
||||
contentDigestsToVerify,
|
||||
supportedApkSigSchemeNames,
|
||||
foundApkSigSchemeIds,
|
||||
minSdkVersion,
|
||||
maxSdkVersion);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (signerCount > MAX_APK_SIGNERS) {
|
||||
result.addError(Issue.V2_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS, signerCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided signer block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
||||
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
|
||||
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
|
||||
* integrity of the APK.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
private static void parseSigner(
|
||||
ByteBuffer signerBlock,
|
||||
CertificateFactory certFactory,
|
||||
ApkSigningBlockUtils.Result.SignerInfo result,
|
||||
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||
Map<Integer, String> supportedApkSigSchemeNames,
|
||||
Set<Integer> foundApkSigSchemeIds,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException {
|
||||
ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
|
||||
byte[] signedDataBytes = new byte[signedData.remaining()];
|
||||
signedData.get(signedDataBytes);
|
||||
signedData.flip();
|
||||
result.signedData = signedDataBytes;
|
||||
|
||||
ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
|
||||
byte[] publicKeyBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signerBlock);
|
||||
|
||||
// Parse the signatures block and identify supported signatures
|
||||
int signatureCount = 0;
|
||||
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||
while (signatures.hasRemaining()) {
|
||||
signatureCount++;
|
||||
try {
|
||||
ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
|
||||
int sigAlgorithmId = signature.getInt();
|
||||
byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
|
||||
result.signatures.add(
|
||||
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
|
||||
sigAlgorithmId, sigBytes));
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||
if (signatureAlgorithm == null) {
|
||||
result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
||||
continue;
|
||||
}
|
||||
supportedSignatures.add(
|
||||
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (result.signatures.isEmpty()) {
|
||||
result.addError(Issue.V2_SIG_NO_SIGNATURES);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify signatures over signed-data block using the public key
|
||||
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
|
||||
try {
|
||||
signaturesToVerify =
|
||||
ApkSigningBlockUtils.getSignaturesToVerify(
|
||||
supportedSignatures, minSdkVersion, maxSdkVersion);
|
||||
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
|
||||
result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES, e);
|
||||
return;
|
||||
}
|
||||
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
|
||||
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||
String jcaSignatureAlgorithm =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
||||
PublicKey publicKey;
|
||||
try {
|
||||
publicKey =
|
||||
KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
||||
new X509EncodedKeySpec(publicKeyBytes));
|
||||
} catch (Exception e) {
|
||||
result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
signedData.position(0);
|
||||
sig.update(signedData);
|
||||
byte[] sigBytes = signature.signature;
|
||||
if (!sig.verify(sigBytes)) {
|
||||
result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
||||
return;
|
||||
}
|
||||
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
||||
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException
|
||||
| SignatureException e) {
|
||||
result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// At least one signature over signedData has verified. We can now parse signed-data.
|
||||
signedData.position(0);
|
||||
ByteBuffer digests = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
|
||||
ByteBuffer certificates = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
|
||||
ByteBuffer additionalAttributes = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
|
||||
|
||||
// Parse the certificates block
|
||||
int certificateIndex = -1;
|
||||
while (certificates.hasRemaining()) {
|
||||
certificateIndex++;
|
||||
byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates);
|
||||
X509Certificate certificate;
|
||||
try {
|
||||
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
|
||||
} catch (CertificateException e) {
|
||||
result.addError(
|
||||
Issue.V2_SIG_MALFORMED_CERTIFICATE,
|
||||
certificateIndex,
|
||||
certificateIndex + 1,
|
||||
e);
|
||||
return;
|
||||
}
|
||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||
// form. Without this, getEncoded may return a different form from what was stored in
|
||||
// the signature. This is because some X509Certificate(Factory) implementations
|
||||
// re-encode certificates.
|
||||
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||
result.certs.add(certificate);
|
||||
}
|
||||
|
||||
if (result.certs.isEmpty()) {
|
||||
result.addError(Issue.V2_SIG_NO_CERTIFICATES);
|
||||
return;
|
||||
}
|
||||
X509Certificate mainCertificate = result.certs.get(0);
|
||||
byte[] certificatePublicKeyBytes;
|
||||
try {
|
||||
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
|
||||
mainCertificate.getPublicKey());
|
||||
} catch (InvalidKeyException e) {
|
||||
System.out.println("Caught an exception encoding the public key: " + e);
|
||||
e.printStackTrace();
|
||||
certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
|
||||
}
|
||||
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
||||
result.addError(
|
||||
Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
||||
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
|
||||
ApkSigningBlockUtils.toHex(publicKeyBytes));
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the digests block
|
||||
int digestCount = 0;
|
||||
while (digests.hasRemaining()) {
|
||||
digestCount++;
|
||||
try {
|
||||
ByteBuffer digest = ApkSigningBlockUtils.getLengthPrefixedSlice(digests);
|
||||
int sigAlgorithmId = digest.getInt();
|
||||
byte[] digestBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(digest);
|
||||
result.contentDigests.add(
|
||||
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
|
||||
sigAlgorithmId, digestBytes));
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
|
||||
for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
|
||||
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
|
||||
}
|
||||
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
|
||||
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
|
||||
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
|
||||
}
|
||||
|
||||
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
|
||||
result.addError(
|
||||
Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
|
||||
sigAlgsFromSignaturesRecord,
|
||||
sigAlgsFromDigestsRecord);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the additional attributes block.
|
||||
int additionalAttributeCount = 0;
|
||||
Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
|
||||
Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
|
||||
while (additionalAttributes.hasRemaining()) {
|
||||
additionalAttributeCount++;
|
||||
try {
|
||||
ByteBuffer attribute =
|
||||
ApkSigningBlockUtils.getLengthPrefixedSlice(additionalAttributes);
|
||||
int id = attribute.getInt();
|
||||
byte[] value = ByteBufferUtils.toByteArray(attribute);
|
||||
result.additionalAttributes.add(
|
||||
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
|
||||
switch (id) {
|
||||
case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID:
|
||||
// stripping protection added when signing with a newer scheme
|
||||
int foundId = ByteBuffer.wrap(value).order(
|
||||
ByteOrder.LITTLE_ENDIAN).getInt();
|
||||
if (supportedApkSigSchemeIds.contains(foundId)) {
|
||||
supportedExpectedApkSigSchemeIds.add(foundId);
|
||||
} else {
|
||||
result.addWarning(
|
||||
Issue.V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID, result.index, foundId);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
|
||||
}
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addError(
|
||||
Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure that all known IDs indicated in stripping protection have already verified
|
||||
for (int id : supportedExpectedApkSigSchemeIds) {
|
||||
if (!foundApkSigSchemeIds.contains(id)) {
|
||||
String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
|
||||
result.addError(
|
||||
Issue.V2_SIG_MISSING_APK_SIG_REFERENCED,
|
||||
result.index,
|
||||
apkSigSchemeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v3;
|
||||
|
||||
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||
|
||||
/** Constants used by the V3 Signature Scheme signing and verification. */
|
||||
public class V3SchemeConstants {
|
||||
private V3SchemeConstants() {}
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
|
||||
public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61;
|
||||
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
|
||||
|
||||
public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P;
|
||||
public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T;
|
||||
/**
|
||||
* By default, APK signing key rotation will target T, but packages that have previously
|
||||
* rotated can continue rotating on pre-T by specifying an SDK version <= 32 as the
|
||||
* --rotation-min-sdk-version parameter when using apksigner or when invoking
|
||||
* {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}.
|
||||
*/
|
||||
public static final int DEFAULT_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.T;
|
||||
|
||||
/**
|
||||
* This attribute is intended to be written to the V3.0 signer block as an additional attribute
|
||||
* whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If
|
||||
* this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version
|
||||
* for rotation in the v3.1 signing block is not X, then the APK should be rejected.
|
||||
*/
|
||||
public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02;
|
||||
|
||||
/**
|
||||
* This attribute is written to the V3.1 signer block as an additional attribute to signify that
|
||||
* the rotation-min-sdk-version is targeting a development release. This is required to support
|
||||
* testing rotation on new development releases as the previous platform release SDK version
|
||||
* is used as the development release SDK version until the development release SDK is
|
||||
* finalized.
|
||||
*/
|
||||
public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba;
|
||||
|
||||
/**
|
||||
* The current development release; rotation / signing configs targeting this release should
|
||||
* be written with the {@link #PROD_RELEASE} SDK version and the dev release attribute.
|
||||
*/
|
||||
public static final int DEV_RELEASE = AndroidSdkVersion.U;
|
||||
|
||||
/**
|
||||
* The current production release.
|
||||
*/
|
||||
public static final int PROD_RELEASE = AndroidSdkVersion.T;
|
||||
}
|
||||
|
|
@ -0,0 +1,531 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v3;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
|
||||
|
||||
import com.android.apksig.SigningCertificateLineage;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SigningSchemeBlockAndDigests;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.RunnablesExecutor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.interfaces.ECKey;
|
||||
import java.security.interfaces.RSAKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v3 signer.
|
||||
*
|
||||
* <p>APK Signature Scheme v3 builds upon APK Signature Scheme v3, and maintains all of the APK
|
||||
* Signature Scheme v2 goals.
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||
* <p>The main contribution of APK Signature Scheme v3 is the introduction of the {@link
|
||||
* SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
|
||||
* it can prove the new siging certificate was signed by the old.
|
||||
*/
|
||||
public class V3SchemeSigner {
|
||||
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
|
||||
|
||||
private final RunnablesExecutor mExecutor;
|
||||
private final DataSource mBeforeCentralDir;
|
||||
private final DataSource mCentralDir;
|
||||
private final DataSource mEocd;
|
||||
private final List<SignerConfig> mSignerConfigs;
|
||||
private final int mBlockId;
|
||||
private final OptionalInt mOptionalV31MinSdkVersion;
|
||||
private final boolean mRotationTargetsDevRelease;
|
||||
|
||||
private V3SchemeSigner(DataSource beforeCentralDir,
|
||||
DataSource centralDir,
|
||||
DataSource eocd,
|
||||
List<SignerConfig> signerConfigs,
|
||||
RunnablesExecutor executor,
|
||||
int blockId,
|
||||
OptionalInt optionalV31MinSdkVersion,
|
||||
boolean rotationTargetsDevRelease) {
|
||||
mBeforeCentralDir = beforeCentralDir;
|
||||
mCentralDir = centralDir;
|
||||
mEocd = eocd;
|
||||
mSignerConfigs = signerConfigs;
|
||||
mExecutor = executor;
|
||||
mBlockId = blockId;
|
||||
mOptionalV31MinSdkVersion = optionalV31MinSdkVersion;
|
||||
mRotationTargetsDevRelease = rotationTargetsDevRelease;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the
|
||||
* provided key.
|
||||
*
|
||||
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||
* AndroidManifest.xml minSdkVersion attribute).
|
||||
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
|
||||
* Signature Scheme v3
|
||||
*/
|
||||
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
|
||||
int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
|
||||
throws InvalidKeyException {
|
||||
String keyAlgorithm = signingKey.getAlgorithm();
|
||||
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
|
||||
// deterministic signatures which make life easier for OTA updates (fewer files
|
||||
// changed when deterministic signature schemes are used).
|
||||
|
||||
// Pick a digest which is no weaker than the key.
|
||||
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
|
||||
if (modulusLengthBits <= 3072) {
|
||||
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
|
||||
if (verityEnabled) {
|
||||
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
|
||||
}
|
||||
return algorithms;
|
||||
} else {
|
||||
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
|
||||
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
|
||||
}
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// DSA is supported only with SHA-256.
|
||||
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||
algorithms.add(
|
||||
deterministicDsaSigning ?
|
||||
SignatureAlgorithm.DETDSA_WITH_SHA256 :
|
||||
SignatureAlgorithm.DSA_WITH_SHA256);
|
||||
if (verityEnabled) {
|
||||
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
|
||||
}
|
||||
return algorithms;
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// Pick a digest which is no weaker than the key.
|
||||
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
|
||||
if (keySizeBits <= 256) {
|
||||
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
|
||||
if (verityEnabled) {
|
||||
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
|
||||
}
|
||||
return algorithms;
|
||||
} else {
|
||||
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
|
||||
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block(
|
||||
RunnablesExecutor executor,
|
||||
DataSource beforeCentralDir,
|
||||
DataSource centralDir,
|
||||
DataSource eocd,
|
||||
List<SignerConfig> signerConfigs)
|
||||
throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
|
||||
return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs)
|
||||
.setRunnablesExecutor(executor)
|
||||
.setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
|
||||
.build()
|
||||
.generateApkSignatureSchemeV3BlockAndDigests();
|
||||
}
|
||||
|
||||
public static byte[] generateV3SignerAttribute(
|
||||
SigningCertificateLineage signingCertificateLineage) {
|
||||
// FORMAT (little endian):
|
||||
// * length-prefixed bytes: attribute pair
|
||||
// * uint32: ID
|
||||
// * bytes: value - encoded V3 SigningCertificateLineage
|
||||
byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage();
|
||||
int payloadSize = 4 + 4 + encodedLineage.length;
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.putInt(4 + encodedLineage.length);
|
||||
result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID);
|
||||
result.put(encodedLineage);
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private static byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute(
|
||||
int rotationMinSdkVersion) {
|
||||
// FORMAT (little endian):
|
||||
// * length-prefixed bytes: attribute pair
|
||||
// * uint32: ID
|
||||
// * bytes: value - int32 representing minimum SDK version for rotation
|
||||
int payloadSize = 4 + 4 + 4;
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.putInt(payloadSize - 4);
|
||||
result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID);
|
||||
result.putInt(rotationMinSdkVersion);
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private static byte[] generateV31RotationTargetsDevReleaseAttribute() {
|
||||
// FORMAT (little endian):
|
||||
// * length-prefixed bytes: attribute pair
|
||||
// * uint32: ID
|
||||
// * bytes: value - No value is used for this attribute
|
||||
int payloadSize = 4 + 4;
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.putInt(payloadSize - 4);
|
||||
result.putInt(V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
|
||||
return result.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x
|
||||
* signing scheme block and digests based on the parameters provided to the {@link Builder}.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
* @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
|
||||
* missing
|
||||
* @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained
|
||||
* @throws SignatureException if an error occurs when computing digests or generating
|
||||
* signatures
|
||||
*/
|
||||
public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests()
|
||||
throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
|
||||
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
|
||||
ApkSigningBlockUtils.computeContentDigests(
|
||||
mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs);
|
||||
return new SigningSchemeBlockAndDigests(
|
||||
generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond());
|
||||
}
|
||||
|
||||
private Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
|
||||
Map<ContentDigestAlgorithm, byte[]> contentDigests)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed signer blocks.
|
||||
List<byte[]> signerBlocks = new ArrayList<>(mSignerConfigs.size());
|
||||
int signerNumber = 0;
|
||||
for (SignerConfig signerConfig : mSignerConfigs) {
|
||||
signerNumber++;
|
||||
byte[] signerBlock;
|
||||
try {
|
||||
signerBlock = generateSignerBlock(signerConfig, contentDigests);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
||||
}
|
||||
signerBlocks.add(signerBlock);
|
||||
}
|
||||
|
||||
return Pair.of(
|
||||
encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
||||
}),
|
||||
mBlockId);
|
||||
}
|
||||
|
||||
private byte[] generateSignerBlock(
|
||||
SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||
if (signerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||
|
||||
byte[] encodedPublicKey = encodePublicKey(publicKey);
|
||||
|
||||
V3SignatureSchemeBlock.SignedData signedData = new V3SignatureSchemeBlock.SignedData();
|
||||
try {
|
||||
signedData.certificates = encodeCertificates(signerConfig.certificates);
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new SignatureException("Failed to encode certificates", e);
|
||||
}
|
||||
|
||||
List<Pair<Integer, byte[]>> digests =
|
||||
new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
||||
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||
ContentDigestAlgorithm contentDigestAlgorithm =
|
||||
signatureAlgorithm.getContentDigestAlgorithm();
|
||||
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
|
||||
if (contentDigest == null) {
|
||||
throw new RuntimeException(
|
||||
contentDigestAlgorithm
|
||||
+ " content digest for "
|
||||
+ signatureAlgorithm
|
||||
+ " not computed");
|
||||
}
|
||||
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
|
||||
}
|
||||
signedData.digests = digests;
|
||||
signedData.minSdkVersion = signerConfig.minSdkVersion;
|
||||
signedData.maxSdkVersion = signerConfig.maxSdkVersion;
|
||||
signedData.additionalAttributes = generateAdditionalAttributes(signerConfig);
|
||||
|
||||
V3SignatureSchemeBlock.Signer signer = new V3SignatureSchemeBlock.Signer();
|
||||
|
||||
signer.signedData = encodeSignedData(signedData);
|
||||
|
||||
signer.minSdkVersion = signerConfig.minSdkVersion;
|
||||
signer.maxSdkVersion = signerConfig.maxSdkVersion;
|
||||
signer.publicKey = encodedPublicKey;
|
||||
signer.signatures =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
|
||||
|
||||
return encodeSigner(signer);
|
||||
}
|
||||
|
||||
private byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
|
||||
byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData);
|
||||
byte[] signatures =
|
||||
encodeAsLengthPrefixedElement(
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
signer.signatures));
|
||||
byte[] publicKey = encodeAsLengthPrefixedElement(signer.publicKey);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed signed data
|
||||
// * uint32: minSdkVersion
|
||||
// * uint32: maxSdkVersion
|
||||
// * length-prefixed sequence of length-prefixed signatures:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: signature of signed data
|
||||
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
|
||||
int payloadSize = signedData.length + 4 + 4 + signatures.length + publicKey.length;
|
||||
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.put(signedData);
|
||||
result.putInt(signer.minSdkVersion);
|
||||
result.putInt(signer.maxSdkVersion);
|
||||
result.put(signatures);
|
||||
result.put(publicKey);
|
||||
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
|
||||
byte[] digests =
|
||||
encodeAsLengthPrefixedElement(
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
signedData.digests));
|
||||
byte[] certs =
|
||||
encodeAsLengthPrefixedElement(
|
||||
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates));
|
||||
byte[] attributes = encodeAsLengthPrefixedElement(signedData.additionalAttributes);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed digests:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: digest of contents
|
||||
// * length-prefixed sequence of certificates:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||
// * uint-32: minSdkVersion
|
||||
// * uint-32: maxSdkVersion
|
||||
// * length-prefixed sequence of length-prefixed additional attributes:
|
||||
// * uint32: ID
|
||||
// * (length - 4) bytes: value
|
||||
// * uint32: Proof-of-rotation ID: 0x3ba06f8c
|
||||
// * length-prefixed roof-of-rotation structure
|
||||
int payloadSize = digests.length + certs.length + 4 + 4 + attributes.length;
|
||||
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.put(digests);
|
||||
result.put(certs);
|
||||
result.putInt(signedData.minSdkVersion);
|
||||
result.putInt(signedData.maxSdkVersion);
|
||||
result.put(attributes);
|
||||
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
|
||||
List<byte[]> attributes = new ArrayList<>();
|
||||
if (signerConfig.signingCertificateLineage != null) {
|
||||
attributes.add(generateV3SignerAttribute(signerConfig.signingCertificateLineage));
|
||||
}
|
||||
if ((mRotationTargetsDevRelease || signerConfig.signerTargetsDevRelease)
|
||||
&& mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
|
||||
attributes.add(generateV31RotationTargetsDevReleaseAttribute());
|
||||
}
|
||||
if (mOptionalV31MinSdkVersion.isPresent()
|
||||
&& mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
|
||||
attributes.add(generateV3RotationMinSdkVersionStrippingProtectionAttribute(
|
||||
mOptionalV31MinSdkVersion.getAsInt()));
|
||||
}
|
||||
int attributesSize = attributes.stream().mapToInt(attribute -> attribute.length).sum();
|
||||
byte[] attributesBuffer = new byte[attributesSize];
|
||||
if (attributesSize == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
int index = 0;
|
||||
for (byte[] attribute : attributes) {
|
||||
System.arraycopy(attribute, 0, attributesBuffer, index, attribute.length);
|
||||
index += attribute.length;
|
||||
}
|
||||
return attributesBuffer;
|
||||
}
|
||||
|
||||
private static final class V3SignatureSchemeBlock {
|
||||
private static final class Signer {
|
||||
public byte[] signedData;
|
||||
public int minSdkVersion;
|
||||
public int maxSdkVersion;
|
||||
public List<Pair<Integer, byte[]>> signatures;
|
||||
public byte[] publicKey;
|
||||
}
|
||||
|
||||
private static final class SignedData {
|
||||
public List<Pair<Integer, byte[]>> digests;
|
||||
public List<byte[]> certificates;
|
||||
public int minSdkVersion;
|
||||
public int maxSdkVersion;
|
||||
public byte[] additionalAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
/** Builder of {@link V3SchemeSigner} instances. */
|
||||
public static class Builder {
|
||||
private final DataSource mBeforeCentralDir;
|
||||
private final DataSource mCentralDir;
|
||||
private final DataSource mEocd;
|
||||
private final List<SignerConfig> mSignerConfigs;
|
||||
|
||||
private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
|
||||
private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||
private OptionalInt mOptionalV31MinSdkVersion = OptionalInt.empty();
|
||||
private boolean mRotationTargetsDevRelease = false;
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code
|
||||
* centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to
|
||||
* be used to sign the APK.
|
||||
*/
|
||||
public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd,
|
||||
List<SignerConfig> signerConfigs) {
|
||||
mBeforeCentralDir = beforeCentralDir;
|
||||
mCentralDir = centralDir;
|
||||
mEocd = eocd;
|
||||
mSignerConfigs = signerConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests.
|
||||
*/
|
||||
public Builder setRunnablesExecutor(RunnablesExecutor executor) {
|
||||
mExecutor = executor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code blockId} to be used for the V3 signature block.
|
||||
*
|
||||
* <p>This {@code V3SchemeSigner} currently supports the block IDs for the {@link
|
||||
* V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
|
||||
* V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
|
||||
*/
|
||||
public Builder setBlockId(int blockId) {
|
||||
mBlockId = blockId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each
|
||||
* signer's block.
|
||||
*
|
||||
* <p>This value provides stripping protection to ensure a v3.1 signing block with rotation
|
||||
* is not modified or removed from the APK's signature block.
|
||||
*/
|
||||
public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
|
||||
return setMinSdkVersionForV31(rotationMinSdkVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code minSdkVersion} to be written as an additional attribute in each
|
||||
* signer's block.
|
||||
*
|
||||
* <p>This value provides the stripping protection to ensure a v3.1 signing block is not
|
||||
* modified or removed from the APK's signature block.
|
||||
*/
|
||||
public Builder setMinSdkVersionForV31(int minSdkVersion) {
|
||||
if (minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
|
||||
minSdkVersion = V3SchemeConstants.PROD_RELEASE;
|
||||
}
|
||||
mOptionalV31MinSdkVersion = OptionalInt.of(minSdkVersion);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the minimum SDK version of a signer is intended to target a development
|
||||
* release; this is primarily required after the T SDK is finalized, and an APK needs to
|
||||
* target U during its development cycle for rotation.
|
||||
*
|
||||
* <p>This is only required after the T SDK is finalized since S and earlier releases do
|
||||
* not know about the V3.1 block ID, but once T is released and work begins on U, U will
|
||||
* use the SDK version of T during development. A signer with a minimum SDK version of T's
|
||||
* SDK version along with setting {@code enabled} to true will allow an APK to use the
|
||||
* rotated key on a device running U while causing this to be bypassed for T.
|
||||
*
|
||||
* <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
|
||||
* Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
|
||||
* will be a noop.
|
||||
*/
|
||||
public Builder setRotationTargetsDevRelease(boolean enabled) {
|
||||
mRotationTargetsDevRelease = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link V3SchemeSigner} built with the configuration provided to this
|
||||
* {@code Builder}.
|
||||
*/
|
||||
public V3SchemeSigner build() {
|
||||
return new V3SchemeSigner(mBeforeCentralDir,
|
||||
mCentralDir,
|
||||
mEocd,
|
||||
mSignerConfigs,
|
||||
mExecutor,
|
||||
mBlockId,
|
||||
mOptionalV31MinSdkVersion,
|
||||
mRotationTargetsDevRelease);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,783 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v3;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
import com.android.apksig.ApkVerifier.Issue;
|
||||
import com.android.apksig.SigningCertificateLineage;
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundException;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.RunnablesExecutor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v3 verifier.
|
||||
*
|
||||
* <p>APK Signature Scheme v3, like v2 is a whole-file signature scheme which aims to protect every
|
||||
* single bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||
* uncompressed contents of ZIP entries.
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||
*/
|
||||
public class V3SchemeVerifier {
|
||||
private final RunnablesExecutor mExecutor;
|
||||
private final DataSource mApk;
|
||||
private final ApkUtils.ZipSections mZipSections;
|
||||
private final ApkSigningBlockUtils.Result mResult;
|
||||
private final Set<ContentDigestAlgorithm> mContentDigestsToVerify;
|
||||
private final int mMinSdkVersion;
|
||||
private final int mMaxSdkVersion;
|
||||
private final int mBlockId;
|
||||
private final OptionalInt mOptionalRotationMinSdkVersion;
|
||||
private final boolean mFullVerification;
|
||||
|
||||
private ByteBuffer mApkSignatureSchemeV3Block;
|
||||
|
||||
private V3SchemeVerifier(
|
||||
RunnablesExecutor executor,
|
||||
DataSource apk,
|
||||
ApkUtils.ZipSections zipSections,
|
||||
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||
ApkSigningBlockUtils.Result result,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
int blockId,
|
||||
OptionalInt optionalRotationMinSdkVersion,
|
||||
boolean fullVerification) {
|
||||
mExecutor = executor;
|
||||
mApk = apk;
|
||||
mZipSections = zipSections;
|
||||
mContentDigestsToVerify = contentDigestsToVerify;
|
||||
mResult = result;
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
mMaxSdkVersion = maxSdkVersion;
|
||||
mBlockId = blockId;
|
||||
mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
|
||||
mFullVerification = fullVerification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
|
||||
* verification. The APK must be considered verified only if
|
||||
* {@link ApkSigningBlockUtils.Result#verified} is
|
||||
* {@code true}. If verification fails, the result will contain errors -- see
|
||||
* {@link ApkSigningBlockUtils.Result#getErrors()}.
|
||||
*
|
||||
* <p>Verification succeeds iff the APK's APK Signature Scheme v3 signatures are expected to
|
||||
* verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
* If the APK's signature is expected to not verify on any of the specified platform versions,
|
||||
* this method returns a result with one or more errors and whose
|
||||
* {@code Result.verified == false}, or this method throws an exception.
|
||||
*
|
||||
* <p>This method only verifies the v3.0 signing block without platform targeted rotation from
|
||||
* a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence
|
||||
* of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}.
|
||||
*
|
||||
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||
* required cryptographic algorithm implementation is missing
|
||||
* @throws SignatureNotFoundException if no APK Signature Scheme v3
|
||||
* signatures are found
|
||||
* @throws IOException if an I/O error occurs when reading the APK
|
||||
*/
|
||||
public static ApkSigningBlockUtils.Result verify(
|
||||
RunnablesExecutor executor,
|
||||
DataSource apk,
|
||||
ApkUtils.ZipSections zipSections,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||
return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion)
|
||||
.setRunnablesExecutor(executor)
|
||||
.setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
|
||||
.build()
|
||||
.verify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's v3 signatures and outputs the results into the provided
|
||||
* {@code result}. APK is considered verified only if there are no errors reported in the
|
||||
* {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
|
||||
* int)} for more information about the contract of this method.
|
||||
*
|
||||
* @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the
|
||||
* APK, such as information about signers, and verification errors and warnings
|
||||
*/
|
||||
public ApkSigningBlockUtils.Result verify()
|
||||
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||
if (mApk == null || mZipSections == null) {
|
||||
throw new IllegalStateException(
|
||||
"A non-null apk and zip sections must be specified to verify an APK's v3 "
|
||||
+ "signatures");
|
||||
}
|
||||
SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
|
||||
mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
|
||||
|
||||
DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset);
|
||||
DataSource centralDir =
|
||||
mApk.slice(
|
||||
signatureInfo.centralDirOffset,
|
||||
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
|
||||
ByteBuffer eocd = signatureInfo.eocd;
|
||||
|
||||
parseSigners();
|
||||
|
||||
if (mResult.containsErrors()) {
|
||||
return mResult;
|
||||
}
|
||||
ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd,
|
||||
mContentDigestsToVerify, mResult);
|
||||
|
||||
// make sure that the v3 signers cover the entire targeted sdk version ranges and that the
|
||||
// longest SigningCertificateHistory, if present, corresponds to the newest platform
|
||||
// versions
|
||||
SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
|
||||
for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) {
|
||||
sortedSigners.put(signer.maxSdkVersion, signer);
|
||||
}
|
||||
|
||||
// first make sure there is neither overlap nor holes
|
||||
int firstMin = 0;
|
||||
int lastMax = 0;
|
||||
int lastLineageSize = 0;
|
||||
|
||||
// while we're iterating through the signers, build up the list of lineages
|
||||
List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.signers.size());
|
||||
|
||||
for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
|
||||
int currentMin = signer.minSdkVersion;
|
||||
int currentMax = signer.maxSdkVersion;
|
||||
if (firstMin == 0) {
|
||||
// first round sets up our basis
|
||||
firstMin = currentMin;
|
||||
} else {
|
||||
// A signer's minimum SDK can equal the previous signer's maximum SDK if this signer
|
||||
// is targeting a development release.
|
||||
if (currentMin != (lastMax + 1)
|
||||
&& !(currentMin == lastMax && signerTargetsDevRelease(signer))) {
|
||||
mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
|
||||
break;
|
||||
}
|
||||
}
|
||||
lastMax = currentMax;
|
||||
|
||||
// also, while we're here, make sure that the lineage sizes only increase
|
||||
if (signer.signingCertificateLineage != null) {
|
||||
int currLineageSize = signer.signingCertificateLineage.size();
|
||||
if (currLineageSize < lastLineageSize) {
|
||||
mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
|
||||
break;
|
||||
}
|
||||
lastLineageSize = currLineageSize;
|
||||
lineages.add(signer.signingCertificateLineage);
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we support our desired sdk ranges; if rotation is present in a v3.1 block
|
||||
// then the max level only needs to support up to that sdk version for rotation.
|
||||
if (firstMin > mMinSdkVersion
|
||||
|| lastMax < (mOptionalRotationMinSdkVersion.isPresent()
|
||||
? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) {
|
||||
mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
|
||||
}
|
||||
|
||||
try {
|
||||
mResult.signingCertificateLineage =
|
||||
SigningCertificateLineage.consolidateLineages(lineages);
|
||||
} catch (IllegalArgumentException e) {
|
||||
mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
|
||||
}
|
||||
if (!mResult.containsErrors()) {
|
||||
mResult.verified = true;
|
||||
}
|
||||
return mResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses each signer in the provided APK Signature Scheme v3 block and populates corresponding
|
||||
* {@code signerInfos} of the provided {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
|
||||
* However, this does not verify the integrity of the rest of the APK but rather simply reports
|
||||
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
public static void parseSigners(
|
||||
ByteBuffer apkSignatureSchemeV3Block,
|
||||
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
|
||||
try {
|
||||
new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block)
|
||||
.setResult(result)
|
||||
.setContentDigestsToVerify(contentDigestsToVerify)
|
||||
.setFullVerification(false)
|
||||
.build()
|
||||
.parseSigners();
|
||||
} catch (IOException | SignatureNotFoundException e) {
|
||||
// This should never occur since the apkSignatureSchemeV3Block was already provided.
|
||||
throw new IllegalStateException("An exception was encountered when attempting to parse"
|
||||
+ " the signers from the provided APK Signature Scheme v3 block", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses each signer in the APK Signature Scheme v3 block and populates corresponding
|
||||
* {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the
|
||||
* returned {@link ApkSigningBlockUtils.Result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
|
||||
* However, this does not verify the integrity of the rest of the APK but rather simply reports
|
||||
* the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}).
|
||||
*
|
||||
* <p>This method adds one or more errors to the returned {@code Result} if a verification error
|
||||
* is encountered when parsing the signers.
|
||||
*/
|
||||
public ApkSigningBlockUtils.Result parseSigners()
|
||||
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||
ByteBuffer signers;
|
||||
try {
|
||||
if (mApkSignatureSchemeV3Block == null) {
|
||||
SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
|
||||
mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
|
||||
}
|
||||
signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block);
|
||||
} catch (ApkFormatException e) {
|
||||
mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
|
||||
return mResult;
|
||||
}
|
||||
if (!signers.hasRemaining()) {
|
||||
mResult.addError(Issue.V3_SIG_NO_SIGNERS);
|
||||
return mResult;
|
||||
}
|
||||
|
||||
CertificateFactory certFactory;
|
||||
try {
|
||||
certFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||
}
|
||||
int signerCount = 0;
|
||||
while (signers.hasRemaining()) {
|
||||
int signerIndex = signerCount;
|
||||
signerCount++;
|
||||
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||
signerInfo.index = signerIndex;
|
||||
mResult.signers.add(signerInfo);
|
||||
try {
|
||||
ByteBuffer signer = getLengthPrefixedSlice(signers);
|
||||
parseSigner(signer, certFactory, signerInfo);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
|
||||
return mResult;
|
||||
}
|
||||
}
|
||||
return mResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided signer block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} contained in this block, as well as
|
||||
* the data contained therein, but does not verify the integrity of the rest of the APK. To
|
||||
* facilitate APK integrity verification, this method adds the {@code contentDigestsToVerify}.
|
||||
* These digests can then be used to verify the integrity of the APK.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory,
|
||||
ApkSigningBlockUtils.Result.SignerInfo result)
|
||||
throws ApkFormatException, NoSuchAlgorithmException {
|
||||
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
|
||||
byte[] signedDataBytes = new byte[signedData.remaining()];
|
||||
signedData.get(signedDataBytes);
|
||||
signedData.flip();
|
||||
result.signedData = signedDataBytes;
|
||||
|
||||
int parsedMinSdkVersion = signerBlock.getInt();
|
||||
int parsedMaxSdkVersion = signerBlock.getInt();
|
||||
result.minSdkVersion = parsedMinSdkVersion;
|
||||
result.maxSdkVersion = parsedMaxSdkVersion;
|
||||
if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
|
||||
result.addError(
|
||||
Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
|
||||
}
|
||||
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
|
||||
byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
|
||||
|
||||
// Parse the signatures block and identify supported signatures
|
||||
int signatureCount = 0;
|
||||
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||
while (signatures.hasRemaining()) {
|
||||
signatureCount++;
|
||||
try {
|
||||
ByteBuffer signature = getLengthPrefixedSlice(signatures);
|
||||
int sigAlgorithmId = signature.getInt();
|
||||
byte[] sigBytes = readLengthPrefixedByteArray(signature);
|
||||
result.signatures.add(
|
||||
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
|
||||
sigAlgorithmId, sigBytes));
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||
if (signatureAlgorithm == null) {
|
||||
result.addWarning(Issue.V3_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
||||
continue;
|
||||
}
|
||||
// TODO consider dropping deprecated signatures for v3 or modifying
|
||||
// getSignaturesToVerify (called below)
|
||||
supportedSignatures.add(
|
||||
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addError(Issue.V3_SIG_MALFORMED_SIGNATURE, signatureCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (result.signatures.isEmpty()) {
|
||||
result.addError(Issue.V3_SIG_NO_SIGNATURES);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify signatures over signed-data block using the public key
|
||||
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
|
||||
try {
|
||||
signaturesToVerify =
|
||||
ApkSigningBlockUtils.getSignaturesToVerify(
|
||||
supportedSignatures, result.minSdkVersion, result.maxSdkVersion);
|
||||
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
|
||||
result.addError(Issue.V3_SIG_NO_SUPPORTED_SIGNATURES);
|
||||
return;
|
||||
}
|
||||
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
|
||||
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||
String jcaSignatureAlgorithm =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
||||
PublicKey publicKey;
|
||||
try {
|
||||
publicKey =
|
||||
KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
||||
new X509EncodedKeySpec(publicKeyBytes));
|
||||
} catch (Exception e) {
|
||||
result.addError(Issue.V3_SIG_MALFORMED_PUBLIC_KEY, e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
signedData.position(0);
|
||||
sig.update(signedData);
|
||||
byte[] sigBytes = signature.signature;
|
||||
if (!sig.verify(sigBytes)) {
|
||||
result.addError(Issue.V3_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
||||
return;
|
||||
}
|
||||
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
||||
mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException
|
||||
| SignatureException e) {
|
||||
result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// At least one signature over signedData has verified. We can now parse signed-data.
|
||||
signedData.position(0);
|
||||
ByteBuffer digests = getLengthPrefixedSlice(signedData);
|
||||
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
|
||||
|
||||
int signedMinSdkVersion = signedData.getInt();
|
||||
if (signedMinSdkVersion != parsedMinSdkVersion) {
|
||||
result.addError(
|
||||
Issue.V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
|
||||
parsedMinSdkVersion,
|
||||
signedMinSdkVersion);
|
||||
}
|
||||
int signedMaxSdkVersion = signedData.getInt();
|
||||
if (signedMaxSdkVersion != parsedMaxSdkVersion) {
|
||||
result.addError(
|
||||
Issue.V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
|
||||
parsedMaxSdkVersion,
|
||||
signedMaxSdkVersion);
|
||||
}
|
||||
ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
|
||||
|
||||
// Parse the certificates block
|
||||
int certificateIndex = -1;
|
||||
while (certificates.hasRemaining()) {
|
||||
certificateIndex++;
|
||||
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
|
||||
X509Certificate certificate;
|
||||
try {
|
||||
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
|
||||
} catch (CertificateException e) {
|
||||
result.addError(
|
||||
Issue.V3_SIG_MALFORMED_CERTIFICATE,
|
||||
certificateIndex,
|
||||
certificateIndex + 1,
|
||||
e);
|
||||
return;
|
||||
}
|
||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||
// form. Without this, getEncoded may return a different form from what was stored in
|
||||
// the signature. This is because some X509Certificate(Factory) implementations
|
||||
// re-encode certificates.
|
||||
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||
result.certs.add(certificate);
|
||||
}
|
||||
|
||||
if (result.certs.isEmpty()) {
|
||||
result.addError(Issue.V3_SIG_NO_CERTIFICATES);
|
||||
return;
|
||||
}
|
||||
X509Certificate mainCertificate = result.certs.get(0);
|
||||
byte[] certificatePublicKeyBytes;
|
||||
try {
|
||||
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
|
||||
mainCertificate.getPublicKey());
|
||||
} catch (InvalidKeyException e) {
|
||||
System.out.println("Caught an exception encoding the public key: " + e);
|
||||
e.printStackTrace();
|
||||
certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
|
||||
}
|
||||
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
||||
result.addError(
|
||||
Issue.V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
||||
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
|
||||
ApkSigningBlockUtils.toHex(publicKeyBytes));
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the digests block
|
||||
int digestCount = 0;
|
||||
while (digests.hasRemaining()) {
|
||||
digestCount++;
|
||||
try {
|
||||
ByteBuffer digest = getLengthPrefixedSlice(digests);
|
||||
int sigAlgorithmId = digest.getInt();
|
||||
byte[] digestBytes = readLengthPrefixedByteArray(digest);
|
||||
result.contentDigests.add(
|
||||
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
|
||||
sigAlgorithmId, digestBytes));
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addError(Issue.V3_SIG_MALFORMED_DIGEST, digestCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
|
||||
for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
|
||||
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
|
||||
}
|
||||
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
|
||||
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
|
||||
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
|
||||
}
|
||||
|
||||
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
|
||||
result.addError(
|
||||
Issue.V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
|
||||
sigAlgsFromSignaturesRecord,
|
||||
sigAlgsFromDigestsRecord);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the additional attributes block.
|
||||
int additionalAttributeCount = 0;
|
||||
boolean rotationAttrFound = false;
|
||||
while (additionalAttributes.hasRemaining()) {
|
||||
additionalAttributeCount++;
|
||||
try {
|
||||
ByteBuffer attribute =
|
||||
getLengthPrefixedSlice(additionalAttributes);
|
||||
int id = attribute.getInt();
|
||||
byte[] value = ByteBufferUtils.toByteArray(attribute);
|
||||
result.additionalAttributes.add(
|
||||
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
|
||||
if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
|
||||
try {
|
||||
// SigningCertificateLineage is verified when built
|
||||
result.signingCertificateLineage =
|
||||
SigningCertificateLineage.readFromV3AttributeValue(value);
|
||||
// make sure that the last cert in the chain matches this signer cert
|
||||
SigningCertificateLineage subLineage =
|
||||
result.signingCertificateLineage.getSubLineage(result.certs.get(0));
|
||||
if (result.signingCertificateLineage.size() != subLineage.size()) {
|
||||
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
result.addError(Issue.V3_SIG_POR_DID_NOT_VERIFY);
|
||||
} catch (IllegalArgumentException e) {
|
||||
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
|
||||
} catch (Exception e) {
|
||||
result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
|
||||
}
|
||||
} else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) {
|
||||
rotationAttrFound = true;
|
||||
// API targeting for rotation was added with V3.1; if the maxSdkVersion
|
||||
// does not support v3.1 then ignore this attribute.
|
||||
if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT
|
||||
&& mFullVerification) {
|
||||
int attrRotationMinSdkVersion = ByteBuffer.wrap(value)
|
||||
.order(ByteOrder.LITTLE_ENDIAN).getInt();
|
||||
if (mOptionalRotationMinSdkVersion.isPresent()) {
|
||||
int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt();
|
||||
if (attrRotationMinSdkVersion != rotationMinSdkVersion) {
|
||||
result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH,
|
||||
attrRotationMinSdkVersion, rotationMinSdkVersion);
|
||||
}
|
||||
} else {
|
||||
result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion);
|
||||
}
|
||||
}
|
||||
} else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) {
|
||||
// This attribute should only be used by a v3.1 signer to indicate rotation
|
||||
// is targeting the development release that is using the SDK version of the
|
||||
// previously released platform version.
|
||||
if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
|
||||
result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER);
|
||||
}
|
||||
} else {
|
||||
result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
|
||||
}
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addError(
|
||||
Issue.V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) {
|
||||
result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING,
|
||||
mOptionalRotationMinSdkVersion.getAsInt());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the specified {@code signerInfo} is targeting a development release.
|
||||
*/
|
||||
public static boolean signerTargetsDevRelease(
|
||||
ApkSigningBlockUtils.Result.SignerInfo signerInfo) {
|
||||
boolean result = signerInfo.additionalAttributes.stream()
|
||||
.mapToInt(attribute -> attribute.getId())
|
||||
.anyMatch(attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Builder of {@link V3SchemeVerifier} instances. */
|
||||
public static class Builder {
|
||||
private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
|
||||
private DataSource mApk;
|
||||
private ApkUtils.ZipSections mZipSections;
|
||||
private ByteBuffer mApkSignatureSchemeV3Block;
|
||||
private Set<ContentDigestAlgorithm> mContentDigestsToVerify;
|
||||
private ApkSigningBlockUtils.Result mResult;
|
||||
private int mMinSdkVersion;
|
||||
private int mMaxSdkVersion;
|
||||
private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||
private boolean mFullVerification = true;
|
||||
private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
|
||||
* verify the V3 signing block of the provided {@code apk} with the specified {@code
|
||||
* zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}.
|
||||
*/
|
||||
public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion,
|
||||
int maxSdkVersion) {
|
||||
mApk = apk;
|
||||
mZipSections = zipSections;
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
mMaxSdkVersion = maxSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
|
||||
* parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code
|
||||
* apkSignatureSchemeV3Block}.
|
||||
*
|
||||
* <note>Full verification of the v3 signature is not possible when instantiating a new
|
||||
* {@code V3SchemeVerifier} with this method.</note>
|
||||
*/
|
||||
public Builder(ByteBuffer apkSignatureSchemeV3Block) {
|
||||
mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests.
|
||||
*/
|
||||
public Builder setRunnablesExecutor(RunnablesExecutor executor) {
|
||||
mExecutor = executor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the V3 {code blockId} to be verified in the provided APK.
|
||||
*
|
||||
* <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link
|
||||
* V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
|
||||
* V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
|
||||
*/
|
||||
public Builder setBlockId(int blockId) {
|
||||
mBlockId = blockId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional
|
||||
* attribute.
|
||||
*
|
||||
* <p>This value can be obtained from the signers returned when verifying the v3.1 signing
|
||||
* block of an APK; in the case of multiple signers targeting different SDK versions in the
|
||||
* v3.1 signing block, the minimum SDK version from all the signers should be used.
|
||||
*/
|
||||
public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
|
||||
mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code result} instance to be used when returning verification results.
|
||||
*
|
||||
* <p>This method can be used when the caller already has a {@link
|
||||
* ApkSigningBlockUtils.Result} and wants to store the verification results in this
|
||||
* instance.
|
||||
*/
|
||||
public Builder setResult(ApkSigningBlockUtils.Result result) {
|
||||
mResult = result;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the instance to be used to store the {@code contentDigestsToVerify}.
|
||||
*
|
||||
* <p>This method can be used when the caller needs access to the {@code
|
||||
* contentDigestsToVerify} computed by this {@code V3SchemeVerifier}.
|
||||
*/
|
||||
public Builder setContentDigestsToVerify(
|
||||
Set<ContentDigestAlgorithm> contentDigestsToVerify) {
|
||||
mContentDigestsToVerify = contentDigestsToVerify;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether full verification should be performed by the {@code V3SchemeVerifier} built
|
||||
* from this instance.
|
||||
*
|
||||
* <note>{@link #verify()} will always verify the content digests for the APK, but this
|
||||
* allows verification of the rotation minimum SDK version stripping attribute to be skipped
|
||||
* for scenarios where this value may not have been parsed from a V3.1 signing block (such
|
||||
* as when only {@link #parseSigners()} will be invoked.</note>
|
||||
*/
|
||||
public Builder setFullVerification(boolean fullVerification) {
|
||||
mFullVerification = fullVerification;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link V3SchemeVerifier} built with the configuration provided to this
|
||||
* {@code Builder}.
|
||||
*/
|
||||
public V3SchemeVerifier build() {
|
||||
int sigSchemeVersion;
|
||||
switch (mBlockId) {
|
||||
case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID:
|
||||
sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||
mMinSdkVersion = Math.max(mMinSdkVersion,
|
||||
V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT);
|
||||
break;
|
||||
case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID:
|
||||
sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
|
||||
// V3.1 supports targeting an SDK version later than that of the initial release
|
||||
// in which it is supported; allow any range for V3.1 as long as V3.0 covers the
|
||||
// rest of the range.
|
||||
mMinSdkVersion = mMaxSdkVersion;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x",
|
||||
mBlockId));
|
||||
}
|
||||
if (mResult == null) {
|
||||
mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion);
|
||||
}
|
||||
if (mContentDigestsToVerify == null) {
|
||||
mContentDigestsToVerify = new HashSet<>(1);
|
||||
}
|
||||
|
||||
V3SchemeVerifier verifier = new V3SchemeVerifier(
|
||||
mExecutor,
|
||||
mApk,
|
||||
mZipSections,
|
||||
mContentDigestsToVerify,
|
||||
mResult,
|
||||
mMinSdkVersion,
|
||||
mMaxSdkVersion,
|
||||
mBlockId,
|
||||
mOptionalRotationMinSdkVersion,
|
||||
mFullVerification);
|
||||
if (mApkSignatureSchemeV3Block != null) {
|
||||
verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block;
|
||||
}
|
||||
return verifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v3;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* APK Signer Lineage.
|
||||
*
|
||||
* <p>The signer lineage contains a history of signing certificates with each ancestor attesting to
|
||||
* the validity of its descendant. Each additional descendant represents a new identity that can be
|
||||
* used to sign an APK, and each generation has accompanying attributes which represent how the
|
||||
* APK would like to view the older signing certificates, specifically how they should be trusted in
|
||||
* certain situations.
|
||||
*
|
||||
* <p> Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies
|
||||
* the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
|
||||
* Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
|
||||
* allow upgrades to the new certificate.
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
|
||||
*/
|
||||
public class V3SigningCertificateLineage {
|
||||
|
||||
private final static int FIRST_VERSION = 1;
|
||||
private final static int CURRENT_VERSION = FIRST_VERSION;
|
||||
|
||||
/**
|
||||
* Deserializes the binary representation of an {@link V3SigningCertificateLineage}. Also
|
||||
* verifies that the structure is well-formed, e.g. that the signature for each node is from its
|
||||
* parent.
|
||||
*/
|
||||
public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
|
||||
throws IOException {
|
||||
List<SigningCertificateNode> result = new ArrayList<>();
|
||||
int nodeCount = 0;
|
||||
if (inputBytes == null || !inputBytes.hasRemaining()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ApkSigningBlockUtils.checkByteOrderLittleEndian(inputBytes);
|
||||
|
||||
// FORMAT (little endian):
|
||||
// * uint32: version code
|
||||
// * sequence of length-prefixed (uint32): nodes
|
||||
// * length-prefixed bytes: signed data
|
||||
// * length-prefixed bytes: certificate
|
||||
// * uint32: signature algorithm id
|
||||
// * uint32: flags
|
||||
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||
// * length-prefixed bytes: signature over above signed data
|
||||
|
||||
X509Certificate lastCert = null;
|
||||
int lastSigAlgorithmId = 0;
|
||||
|
||||
try {
|
||||
int version = inputBytes.getInt();
|
||||
if (version != CURRENT_VERSION) {
|
||||
// we only have one version to worry about right now, so just check it
|
||||
throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
|
||||
+ " different than any of which we are aware");
|
||||
}
|
||||
HashSet<X509Certificate> certHistorySet = new HashSet<>();
|
||||
while (inputBytes.hasRemaining()) {
|
||||
nodeCount++;
|
||||
ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
|
||||
ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
|
||||
int flags = nodeBytes.getInt();
|
||||
int sigAlgorithmId = nodeBytes.getInt();
|
||||
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
|
||||
byte[] signature = readLengthPrefixedByteArray(nodeBytes);
|
||||
|
||||
if (lastCert != null) {
|
||||
// Use previous level cert to verify current level
|
||||
String jcaSignatureAlgorithm =
|
||||
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
PublicKey publicKey = lastCert.getPublicKey();
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
sig.update(signedData);
|
||||
if (!sig.verify(signature)) {
|
||||
throw new SecurityException("Unable to verify signature of certificate #"
|
||||
+ nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
|
||||
+ " V3SigningCertificateLineage object");
|
||||
}
|
||||
}
|
||||
|
||||
signedData.rewind();
|
||||
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
|
||||
int signedSigAlgorithm = signedData.getInt();
|
||||
if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
|
||||
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
|
||||
+ nodeBytes + " when verifying V3SigningCertificateLineage object");
|
||||
}
|
||||
lastCert = X509CertificateUtils.generateCertificate(encodedCert);
|
||||
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
|
||||
if (certHistorySet.contains(lastCert)) {
|
||||
throw new SecurityException("Encountered duplicate entries in "
|
||||
+ "SigningCertificateLineage at certificate #" + nodeCount + ". All "
|
||||
+ "signing certificates should be unique");
|
||||
}
|
||||
certHistorySet.add(lastCert);
|
||||
lastSigAlgorithmId = sigAlgorithmId;
|
||||
result.add(new SigningCertificateNode(
|
||||
lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
|
||||
SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
|
||||
}
|
||||
} catch(ApkFormatException | BufferUnderflowException e){
|
||||
throw new IOException("Failed to parse V3SigningCertificateLineage object", e);
|
||||
} catch(NoSuchAlgorithmException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException | SignatureException e){
|
||||
throw new SecurityException(
|
||||
"Failed to verify signature over signed data for certificate #" + nodeCount
|
||||
+ " when parsing V3SigningCertificateLineage object", e);
|
||||
} catch(CertificateException e){
|
||||
throw new SecurityException("Failed to decode certificate #" + nodeCount
|
||||
+ " when parsing V3SigningCertificateLineage object", e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* encode the in-memory representation of this {@code V3SigningCertificateLineage}
|
||||
*/
|
||||
public static byte[] encodeSigningCertificateLineage(
|
||||
List<SigningCertificateNode> signingCertificateLineage) {
|
||||
// FORMAT (little endian):
|
||||
// * version code
|
||||
// * sequence of length-prefixed (uint32): nodes
|
||||
// * length-prefixed bytes: signed data
|
||||
// * length-prefixed bytes: certificate
|
||||
// * uint32: signature algorithm id
|
||||
// * uint32: flags
|
||||
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||
|
||||
List<byte[]> nodes = new ArrayList<>();
|
||||
for (SigningCertificateNode node : signingCertificateLineage) {
|
||||
nodes.add(encodeSigningCertificateNode(node));
|
||||
}
|
||||
byte [] encodedSigningCertificateLineage = encodeAsSequenceOfLengthPrefixedElements(nodes);
|
||||
|
||||
// add the version code (uint32) on top of the encoded nodes
|
||||
int payloadSize = 4 + encodedSigningCertificateLineage.length;
|
||||
ByteBuffer encodedWithVersion = ByteBuffer.allocate(payloadSize);
|
||||
encodedWithVersion.order(ByteOrder.LITTLE_ENDIAN);
|
||||
encodedWithVersion.putInt(CURRENT_VERSION);
|
||||
encodedWithVersion.put(encodedSigningCertificateLineage);
|
||||
return encodedWithVersion.array();
|
||||
}
|
||||
|
||||
public static byte[] encodeSigningCertificateNode(SigningCertificateNode node) {
|
||||
// FORMAT (little endian):
|
||||
// * length-prefixed bytes: signed data
|
||||
// * length-prefixed bytes: certificate
|
||||
// * uint32: signature algorithm id
|
||||
// * uint32: flags
|
||||
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||
// * length-prefixed bytes: signature over signed data
|
||||
int parentSigAlgorithmId = 0;
|
||||
if (node.parentSigAlgorithm != null) {
|
||||
parentSigAlgorithmId = node.parentSigAlgorithm.getId();
|
||||
}
|
||||
int sigAlgorithmId = 0;
|
||||
if (node.sigAlgorithm != null) {
|
||||
sigAlgorithmId = node.sigAlgorithm.getId();
|
||||
}
|
||||
byte[] prefixedSignedData = encodeSignedData(node.signingCert, parentSigAlgorithmId);
|
||||
byte[] prefixedSignature = encodeAsLengthPrefixedElement(node.signature);
|
||||
int payloadSize = prefixedSignedData.length + 4 + 4 + prefixedSignature.length;
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.put(prefixedSignedData);
|
||||
result.putInt(node.flags);
|
||||
result.putInt(sigAlgorithmId);
|
||||
result.put(prefixedSignature);
|
||||
return result.array();
|
||||
}
|
||||
|
||||
public static byte[] encodeSignedData(X509Certificate certificate, int flags) {
|
||||
try {
|
||||
byte[] prefixedCertificate = encodeAsLengthPrefixedElement(certificate.getEncoded());
|
||||
int payloadSize = 4 + prefixedCertificate.length;
|
||||
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.put(prefixedCertificate);
|
||||
result.putInt(flags);
|
||||
return encodeAsLengthPrefixedElement(result.array());
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to encode V3SigningCertificateLineage certificate", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents one signing certificate in the {@link V3SigningCertificateLineage}, which
|
||||
* generally means it is/was used at some point to sign the same APK of the others in the
|
||||
* lineage.
|
||||
*/
|
||||
public static class SigningCertificateNode {
|
||||
|
||||
public SigningCertificateNode(
|
||||
X509Certificate signingCert,
|
||||
SignatureAlgorithm parentSigAlgorithm,
|
||||
SignatureAlgorithm sigAlgorithm,
|
||||
byte[] signature,
|
||||
int flags) {
|
||||
this.signingCert = signingCert;
|
||||
this.parentSigAlgorithm = parentSigAlgorithm;
|
||||
this.sigAlgorithm = sigAlgorithm;
|
||||
this.signature = signature;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof SigningCertificateNode)) return false;
|
||||
|
||||
SigningCertificateNode that = (SigningCertificateNode) o;
|
||||
if (!signingCert.equals(that.signingCert)) return false;
|
||||
if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
|
||||
if (sigAlgorithm != that.sigAlgorithm) return false;
|
||||
if (!Arrays.equals(signature, that.signature)) return false;
|
||||
if (flags != that.flags) return false;
|
||||
|
||||
// we made it
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(signingCert, parentSigAlgorithm, sigAlgorithm, flags);
|
||||
result = 31 * result + Arrays.hashCode(signature);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* the signing cert for this node. This is part of the data signed by the parent node.
|
||||
*/
|
||||
public final X509Certificate signingCert;
|
||||
|
||||
/**
|
||||
* the algorithm used by the this node's parent to bless this data. Its ID value is part of
|
||||
* the data signed by the parent node. {@code null} for first node.
|
||||
*/
|
||||
public final SignatureAlgorithm parentSigAlgorithm;
|
||||
|
||||
/**
|
||||
* the algorithm used by the this nodeto bless the next node's data. Its ID value is part
|
||||
* of the signed data of the next node. {@code null} for the last node.
|
||||
*/
|
||||
public SignatureAlgorithm sigAlgorithm;
|
||||
|
||||
/**
|
||||
* signature over the signed data (above). The signature is from this node's parent
|
||||
* signing certificate, which should correspond to the signing certificate used to sign an
|
||||
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
|
||||
*/
|
||||
public final byte[] signature;
|
||||
|
||||
/**
|
||||
* the flags detailing how the platform should treat this signing cert
|
||||
*/
|
||||
public int flags;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v4;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
|
||||
import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
|
||||
import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
|
||||
import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||
|
||||
import com.android.apksig.apk.ApkUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
|
||||
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
|
||||
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipFormatException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during
|
||||
* installation. And optional verity tree - has to be present during session commit.
|
||||
* <p>
|
||||
* The fields:
|
||||
* <p>
|
||||
* 1. hashingInfo - verity root hash and hashing info,
|
||||
* 2. signingInfo - certificate, public key and signature,
|
||||
* For more details see V4Signature.
|
||||
* </p>
|
||||
* (optional) verityTree: integer size prepended bytes of the verity hash tree.
|
||||
* <p>
|
||||
*/
|
||||
public abstract class V4SchemeSigner {
|
||||
/**
|
||||
* Hidden constructor to prevent instantiation.
|
||||
*/
|
||||
private V4SchemeSigner() {
|
||||
}
|
||||
|
||||
public static class SignerConfig {
|
||||
final public ApkSigningBlockUtils.SignerConfig v4Config;
|
||||
final public ApkSigningBlockUtils.SignerConfig v41Config;
|
||||
|
||||
public SignerConfig(List<ApkSigningBlockUtils.SignerConfig> v4Configs,
|
||||
List<ApkSigningBlockUtils.SignerConfig> v41Configs) throws InvalidKeyException {
|
||||
if (v4Configs == null || v4Configs.size() != 1) {
|
||||
throw new InvalidKeyException("Only accepting one signer config for V4 Signature.");
|
||||
}
|
||||
if (v41Configs != null && v41Configs.size() != 1) {
|
||||
throw new InvalidKeyException("Only accepting one signer config for V4.1 Signature.");
|
||||
}
|
||||
this.v4Config = v4Configs.get(0);
|
||||
this.v41Config = v41Configs != null ? v41Configs.get(0) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on a public key, return a signing algorithm that supports verity.
|
||||
*/
|
||||
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
|
||||
int minSdkVersion, boolean apkSigningBlockPaddingSupported,
|
||||
boolean deterministicDsaSigning)
|
||||
throws InvalidKeyException {
|
||||
List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
|
||||
signingKey, minSdkVersion,
|
||||
apkSigningBlockPaddingSupported, deterministicDsaSigning);
|
||||
// Keeping only supported algorithms.
|
||||
for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
|
||||
final SignatureAlgorithm algorithm = iter.next();
|
||||
if (!isSupported(algorithm.getContentDigestAlgorithm(), false)) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
return algorithms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute hash tree and generate v4 signature for a given APK. Write the serialized data to
|
||||
* output file.
|
||||
*/
|
||||
public static void generateV4Signature(
|
||||
DataSource apkContent, SignerConfig signerConfig, File outputFile)
|
||||
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
Pair<V4Signature, byte[]> pair = generateV4Signature(apkContent, signerConfig);
|
||||
try (final OutputStream output = new FileOutputStream(outputFile)) {
|
||||
pair.getFirst().writeTo(output);
|
||||
V4Signature.writeBytes(output, pair.getSecond());
|
||||
} catch (IOException e) {
|
||||
outputFile.delete();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate v4 signature and hash tree for a given APK. */
|
||||
public static Pair<V4Signature, byte[]> generateV4Signature(
|
||||
DataSource apkContent,
|
||||
SignerConfig signerConfig)
|
||||
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
// Salt has to stay empty for fs-verity compatibility.
|
||||
final byte[] salt = null;
|
||||
// Not used by apksigner.
|
||||
final byte[] additionalData = null;
|
||||
|
||||
final long fileSize = apkContent.size();
|
||||
|
||||
// Obtaining the strongest supported digest for each of the v2/v3/v3.1 blocks
|
||||
// (CHUNKED_SHA256 or CHUNKED_SHA512).
|
||||
final Map<Integer, byte[]> apkDigests = getApkDigests(apkContent);
|
||||
|
||||
// Obtaining the merkle tree and the root hash in verity format.
|
||||
ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
|
||||
ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
|
||||
|
||||
final ContentDigestAlgorithm verityContentDigestAlgorithm =
|
||||
verityContentDigestInfo.contentDigestAlgorithm;
|
||||
final byte[] rootHash = verityContentDigestInfo.rootHash;
|
||||
final byte[] tree = verityContentDigestInfo.tree;
|
||||
|
||||
final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo(
|
||||
verityContentDigestAlgorithm);
|
||||
final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo(
|
||||
hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(),
|
||||
salt, rootHash);
|
||||
|
||||
// Generating SigningInfo and combining everything into V4Signature.
|
||||
final V4Signature signature;
|
||||
try {
|
||||
signature = generateSignature(signerConfig, hashingInfo, apkDigests, additionalData,
|
||||
fileSize);
|
||||
} catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
|
||||
throw new InvalidKeyException("Signer failed", e);
|
||||
}
|
||||
|
||||
return Pair.of(signature, tree);
|
||||
}
|
||||
|
||||
private static V4Signature.SigningInfo generateSigningInfo(
|
||||
ApkSigningBlockUtils.SignerConfig signerConfig,
|
||||
V4Signature.HashingInfo hashingInfo,
|
||||
byte[] apkDigest, byte[] additionalData, long fileSize)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
|
||||
CertificateEncodingException {
|
||||
if (signerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
if (signerConfig.certificates.size() != 1) {
|
||||
throw new CertificateEncodingException("Should only have one certificate");
|
||||
}
|
||||
|
||||
// Collecting data for signing.
|
||||
final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||
|
||||
final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
|
||||
final byte[] encodedCertificate = encodedCertificates.get(0);
|
||||
|
||||
final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
|
||||
encodedCertificate, additionalData, publicKey.getEncoded(), -1, null);
|
||||
|
||||
final byte[] data = V4Signature.getSignedData(fileSize, hashingInfo,
|
||||
signingInfoNoSignature);
|
||||
|
||||
// Signing.
|
||||
final List<Pair<Integer, byte[]>> signatures =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
|
||||
if (signatures.size() != 1) {
|
||||
throw new SignatureException("Should only be one signature generated");
|
||||
}
|
||||
|
||||
final int signatureAlgorithmId = signatures.get(0).getFirst();
|
||||
final byte[] signature = signatures.get(0).getSecond();
|
||||
|
||||
return new V4Signature.SigningInfo(apkDigest,
|
||||
encodedCertificate, additionalData, publicKey.getEncoded(), signatureAlgorithmId,
|
||||
signature);
|
||||
}
|
||||
|
||||
private static V4Signature generateSignature(
|
||||
SignerConfig signerConfig,
|
||||
V4Signature.HashingInfo hashingInfo,
|
||||
Map<Integer, byte[]> apkDigests, byte[] additionalData, long fileSize)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
|
||||
CertificateEncodingException {
|
||||
byte[] apkDigest = apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V3)
|
||||
? apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V3)
|
||||
: apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V2);
|
||||
final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config,
|
||||
hashingInfo, apkDigest, additionalData, fileSize);
|
||||
|
||||
final V4Signature.SigningInfos signingInfos;
|
||||
if (signerConfig.v41Config != null) {
|
||||
if (!apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V31)) {
|
||||
throw new IllegalStateException(
|
||||
"V4.1 cannot be signed without a V3.1 content digest");
|
||||
}
|
||||
apkDigest = apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V31);
|
||||
final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock(
|
||||
APK_SIGNATURE_SCHEME_V31_BLOCK_ID,
|
||||
generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest,
|
||||
additionalData, fileSize).toByteArray());
|
||||
signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock);
|
||||
} else {
|
||||
signingInfos = new V4Signature.SigningInfos(signingInfo);
|
||||
}
|
||||
|
||||
return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
|
||||
signingInfos.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code Map} from the APK signature scheme version to a {@code byte[]} of the
|
||||
* strongest supported content digest found in that version's signature block for the V2,
|
||||
* V3, and V3.1 signatures in the provided {@code apk}.
|
||||
*
|
||||
* <p>If a supported content digest algorithm is not found in any of the signature blocks,
|
||||
* or if the APK is not signed by any of these signature schemes, then an {@code IOException}
|
||||
* is thrown.
|
||||
*/
|
||||
private static Map<Integer, byte[]> getApkDigests(DataSource apk) throws IOException {
|
||||
ApkUtils.ZipSections zipSections;
|
||||
try {
|
||||
zipSections = ApkUtils.findZipSections(apk);
|
||||
} catch (ZipFormatException e) {
|
||||
throw new IOException("Malformed APK: not a ZIP archive", e);
|
||||
}
|
||||
|
||||
Map<Integer, byte[]> sigSchemeToDigest = new HashMap<>(1);
|
||||
try {
|
||||
byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V31);
|
||||
sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V31, digest);
|
||||
} catch (SignatureException expected) {
|
||||
// It is expected to catch a SignatureException if the APK does not have a v3.1
|
||||
// signature.
|
||||
}
|
||||
|
||||
SignatureException v3Exception = null;
|
||||
try {
|
||||
byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V3);
|
||||
sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V3, digest);
|
||||
} catch (SignatureException e) {
|
||||
v3Exception = e;
|
||||
}
|
||||
|
||||
SignatureException v2Exception = null;
|
||||
try {
|
||||
byte[] digest = getBestV2Digest(apk, zipSections);
|
||||
sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V2, digest);
|
||||
} catch (SignatureException e) {
|
||||
v2Exception = e;
|
||||
}
|
||||
|
||||
if (sigSchemeToDigest.size() > 0) {
|
||||
return sigSchemeToDigest;
|
||||
}
|
||||
|
||||
throw new IOException(
|
||||
"Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: "
|
||||
+ v2Exception);
|
||||
}
|
||||
|
||||
private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections,
|
||||
int v3SchemeVersion) throws SignatureException {
|
||||
final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
|
||||
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
||||
v3SchemeVersion);
|
||||
final int blockId;
|
||||
switch (v3SchemeVersion) {
|
||||
case VERSION_APK_SIGNATURE_SCHEME_V31:
|
||||
blockId = APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
|
||||
break;
|
||||
case VERSION_APK_SIGNATURE_SCHEME_V3:
|
||||
blockId = APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid V3 scheme provided: " + v3SchemeVersion);
|
||||
}
|
||||
try {
|
||||
final SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtils.findSignature(apk, zipSections, blockId, result);
|
||||
final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
|
||||
V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify,
|
||||
result);
|
||||
} catch (Exception e) {
|
||||
throw new SignatureException("Failed to extract and parse v3 block", e);
|
||||
}
|
||||
|
||||
if (result.signers.size() != 1) {
|
||||
throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
|
||||
}
|
||||
|
||||
ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
|
||||
if (signer.containsErrors()) {
|
||||
throw new SignatureException("Parsing failed: " + signer.getErrors());
|
||||
}
|
||||
|
||||
final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
|
||||
result.signers.get(0).contentDigests;
|
||||
return pickBestDigest(contentDigests);
|
||||
}
|
||||
|
||||
private static byte[] getBestV2Digest(DataSource apk, ApkUtils.ZipSections zipSections)
|
||||
throws SignatureException {
|
||||
final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
|
||||
final Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
|
||||
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
||||
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
|
||||
try {
|
||||
final SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtils.findSignature(apk, zipSections,
|
||||
APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result);
|
||||
final ByteBuffer apkSignatureSchemeV2Block = signatureInfo.signatureBlock;
|
||||
V2SchemeVerifier.parseSigners(
|
||||
apkSignatureSchemeV2Block,
|
||||
contentDigestsToVerify,
|
||||
Collections.emptyMap(),
|
||||
foundApkSigSchemeIds,
|
||||
Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
result);
|
||||
} catch (Exception e) {
|
||||
throw new SignatureException("Failed to extract and parse v2 block", e);
|
||||
}
|
||||
|
||||
if (result.signers.size() != 1) {
|
||||
throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
|
||||
}
|
||||
|
||||
ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
|
||||
if (signer.containsErrors()) {
|
||||
throw new SignatureException("Parsing failed: " + signer.getErrors());
|
||||
}
|
||||
|
||||
final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
|
||||
signer.contentDigests;
|
||||
return pickBestDigest(contentDigests);
|
||||
}
|
||||
|
||||
private static byte[] pickBestDigest(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) throws SignatureException {
|
||||
if (contentDigests == null || contentDigests.isEmpty()) {
|
||||
throw new SignatureException("Should have at least one digest");
|
||||
}
|
||||
|
||||
int bestAlgorithmOrder = -1;
|
||||
byte[] bestDigest = null;
|
||||
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
|
||||
final SignatureAlgorithm signatureAlgorithm =
|
||||
SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
|
||||
final ContentDigestAlgorithm contentDigestAlgorithm =
|
||||
signatureAlgorithm.getContentDigestAlgorithm();
|
||||
if (!isSupported(contentDigestAlgorithm, true)) {
|
||||
continue;
|
||||
}
|
||||
final int algorithmOrder = digestAlgorithmSortingOrder(contentDigestAlgorithm);
|
||||
if (bestAlgorithmOrder < algorithmOrder) {
|
||||
bestAlgorithmOrder = algorithmOrder;
|
||||
bestDigest = contentDigest.getValue();
|
||||
}
|
||||
}
|
||||
if (bestDigest == null) {
|
||||
throw new SignatureException("Failed to find a supported digest in the source APK");
|
||||
}
|
||||
return bestDigest;
|
||||
}
|
||||
|
||||
public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) {
|
||||
switch (contentDigestAlgorithm) {
|
||||
case CHUNKED_SHA256:
|
||||
return 0;
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return 1;
|
||||
case CHUNKED_SHA512:
|
||||
return 2;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
|
||||
boolean forV3Digest) {
|
||||
if (contentDigestAlgorithm == null) {
|
||||
return false;
|
||||
}
|
||||
if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
|
||||
|| contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512
|
||||
|| (forV3Digest
|
||||
&& contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm)
|
||||
throws NoSuchAlgorithmException {
|
||||
switch (algorithm) {
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256,
|
||||
V4Signature.LOG2_BLOCK_SIZE_4096_BYTES);
|
||||
default:
|
||||
throw new NoSuchAlgorithmException(
|
||||
"Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v4;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
|
||||
|
||||
import com.android.apksig.ApkVerifier;
|
||||
import com.android.apksig.ApkVerifier.Issue;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||
import com.android.apksig.util.DataSource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme V4 verifier.
|
||||
* <p>
|
||||
* Verifies the serialized V4Signature file against an APK.
|
||||
*/
|
||||
public abstract class V4SchemeVerifier {
|
||||
/**
|
||||
* Hidden constructor to prevent instantiation.
|
||||
*/
|
||||
private V4SchemeVerifier() {
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7
|
||||
* signature block against the raw root hash bytes in the proto field 3) verifies that the raw
|
||||
* root hash matches with the actual hash tree root of the give APK 4) if the file contains a
|
||||
* verity tree, verifies that it matches with the actual verity tree computed from the given
|
||||
* APK.
|
||||
* </p>
|
||||
*/
|
||||
public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
|
||||
throws IOException, NoSuchAlgorithmException {
|
||||
final V4Signature signature;
|
||||
final byte[] tree;
|
||||
try (InputStream input = new FileInputStream(v4SignatureFile)) {
|
||||
signature = V4Signature.readFrom(input);
|
||||
tree = V4Signature.readBytes(input);
|
||||
}
|
||||
|
||||
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
||||
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
|
||||
|
||||
if (signature == null) {
|
||||
result.addError(Issue.V4_SIG_NO_SIGNATURES,
|
||||
"Signature file does not contain a v4 signature.");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (signature.version != V4Signature.CURRENT_VERSION) {
|
||||
result.addWarning(Issue.V4_SIG_VERSION_NOT_CURRENT, signature.version,
|
||||
V4Signature.CURRENT_VERSION);
|
||||
}
|
||||
|
||||
V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
|
||||
signature.hashingInfo);
|
||||
|
||||
V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray(
|
||||
signature.signingInfos);
|
||||
|
||||
final ApkSigningBlockUtils.Result.SignerInfo signerInfo;
|
||||
|
||||
// Verify the primary signature over signedData.
|
||||
{
|
||||
V4Signature.SigningInfo signingInfo = signingInfos.signingInfo;
|
||||
final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
|
||||
signingInfo);
|
||||
signerInfo = parseAndVerifySignatureBlock(signingInfo, signedData);
|
||||
result.signers.add(signerInfo);
|
||||
if (result.containsErrors()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all subsequent signatures.
|
||||
for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) {
|
||||
V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
|
||||
signingInfoBlock.signingInfo);
|
||||
final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
|
||||
signingInfo);
|
||||
result.signers.add(parseAndVerifySignatureBlock(signingInfo, signedData));
|
||||
if (result.containsErrors()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the root hash and the tree are correct.
|
||||
verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
|
||||
if (!result.containsErrors()) {
|
||||
result.verified = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided signature block and populates the {@code result}.
|
||||
* <p>
|
||||
* This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate
|
||||
* contained in the signature block. This method adds one or more errors to the {@code result}.
|
||||
*/
|
||||
private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock(
|
||||
V4Signature.SigningInfo signingInfo,
|
||||
final byte[] signedData) throws NoSuchAlgorithmException {
|
||||
final ApkSigningBlockUtils.Result.SignerInfo result =
|
||||
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||
result.index = 0;
|
||||
|
||||
final int sigAlgorithmId = signingInfo.signatureAlgorithmId;
|
||||
final byte[] sigBytes = signingInfo.signature;
|
||||
result.signatures.add(
|
||||
new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
|
||||
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||
if (signatureAlgorithm == null) {
|
||||
result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
||||
return result;
|
||||
}
|
||||
|
||||
String jcaSignatureAlgorithm =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
|
||||
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
||||
|
||||
final byte[] publicKeyBytes = signingInfo.publicKey;
|
||||
PublicKey publicKey;
|
||||
try {
|
||||
publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
||||
new X509EncodedKeySpec(publicKeyBytes));
|
||||
} catch (Exception e) {
|
||||
result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
sig.update(signedData);
|
||||
if (!sig.verify(sigBytes)) {
|
||||
result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
||||
return result;
|
||||
}
|
||||
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException
|
||||
| SignatureException e) {
|
||||
result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (signingInfo.certificate == null) {
|
||||
result.addError(Issue.V4_SIG_NO_CERTIFICATE);
|
||||
return result;
|
||||
}
|
||||
|
||||
final X509Certificate certificate;
|
||||
try {
|
||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||
// form. Without this, getEncoded may return a different form from what was stored in
|
||||
// the signature. This is because some X509Certificate(Factory) implementations
|
||||
// re-encode certificates.
|
||||
certificate = new GuaranteedEncodedFormX509Certificate(
|
||||
X509CertificateUtils.generateCertificate(signingInfo.certificate),
|
||||
signingInfo.certificate);
|
||||
} catch (CertificateException e) {
|
||||
result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e);
|
||||
return result;
|
||||
}
|
||||
result.certs.add(certificate);
|
||||
|
||||
byte[] certificatePublicKeyBytes;
|
||||
try {
|
||||
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
|
||||
certificate.getPublicKey());
|
||||
} catch (InvalidKeyException e) {
|
||||
System.out.println("Caught an exception encoding the public key: " + e);
|
||||
e.printStackTrace();
|
||||
certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
|
||||
}
|
||||
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
||||
result.addError(
|
||||
Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
||||
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
|
||||
ApkSigningBlockUtils.toHex(publicKeyBytes));
|
||||
return result;
|
||||
}
|
||||
|
||||
// Add apk digest from the file to the result.
|
||||
ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest =
|
||||
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
|
||||
0 /* signature algorithm id doesn't matter here */, signingInfo.apkDigest);
|
||||
result.contentDigests.add(contentDigest);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void verifyRootHashAndTree(DataSource apkContent,
|
||||
ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest,
|
||||
byte[] expectedTree) throws IOException, NoSuchAlgorithmException {
|
||||
ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo =
|
||||
ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
|
||||
|
||||
ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm;
|
||||
final byte[] actualDigest = actualContentDigestInfo.rootHash;
|
||||
final byte[] actualTree = actualContentDigestInfo.tree;
|
||||
|
||||
if (!Arrays.equals(expectedDigest, actualDigest)) {
|
||||
signerInfo.addError(
|
||||
ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
|
||||
algorithm,
|
||||
toHex(expectedDigest),
|
||||
toHex(actualDigest));
|
||||
return;
|
||||
}
|
||||
// Only check verity tree if it is not empty
|
||||
if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) {
|
||||
signerInfo.addError(
|
||||
ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
|
||||
algorithm,
|
||||
toHex(expectedDigest),
|
||||
toHex(actualDigest));
|
||||
return;
|
||||
}
|
||||
|
||||
signerInfo.verifiedContentDigests.put(algorithm, actualDigest);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v4;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class V4Signature {
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
|
||||
public static final int HASHING_ALGORITHM_SHA256 = 1;
|
||||
public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
|
||||
|
||||
public static final int MAX_SIGNING_INFOS_SIZE = 7168;
|
||||
|
||||
public static class HashingInfo {
|
||||
public final int hashAlgorithm; // only 1 == SHA256 supported
|
||||
public final byte log2BlockSize; // only 12 (block size 4096) supported now
|
||||
public final byte[] salt; // used exactly as in fs-verity, 32 bytes max
|
||||
public final byte[] rawRootHash; // salted digest of the first Merkle tree page
|
||||
|
||||
HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) {
|
||||
this.hashAlgorithm = hashAlgorithm;
|
||||
this.log2BlockSize = log2BlockSize;
|
||||
this.salt = salt;
|
||||
this.rawRootHash = rawRootHash;
|
||||
}
|
||||
|
||||
static HashingInfo fromByteArray(byte[] bytes) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||
final int hashAlgorithm = buffer.getInt();
|
||||
final byte log2BlockSize = buffer.get();
|
||||
byte[] salt = readBytes(buffer);
|
||||
byte[] rawRootHash = readBytes(buffer);
|
||||
return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash);
|
||||
}
|
||||
|
||||
byte[] toByteArray() {
|
||||
final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt)
|
||||
+ bytesSize(this.rawRootHash);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.putInt(this.hashAlgorithm);
|
||||
buffer.put(this.log2BlockSize);
|
||||
writeBytes(buffer, this.salt);
|
||||
writeBytes(buffer, this.rawRootHash);
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SigningInfo {
|
||||
public final byte[] apkDigest; // used to match with the corresponding APK
|
||||
public final byte[] certificate; // ASN.1 DER form
|
||||
public final byte[] additionalData; // a free-form binary data blob
|
||||
public final byte[] publicKey; // ASN.1 DER, must match the certificate
|
||||
public final int signatureAlgorithmId; // see the APK v2 doc for the list
|
||||
public final byte[] signature;
|
||||
|
||||
SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData,
|
||||
byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
|
||||
this.apkDigest = apkDigest;
|
||||
this.certificate = certificate;
|
||||
this.additionalData = additionalData;
|
||||
this.publicKey = publicKey;
|
||||
this.signatureAlgorithmId = signatureAlgorithmId;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
static SigningInfo fromByteArray(byte[] bytes) throws IOException {
|
||||
return fromByteBuffer(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN));
|
||||
}
|
||||
|
||||
static SigningInfo fromByteBuffer(ByteBuffer buffer) throws IOException {
|
||||
byte[] apkDigest = readBytes(buffer);
|
||||
byte[] certificate = readBytes(buffer);
|
||||
byte[] additionalData = readBytes(buffer);
|
||||
byte[] publicKey = readBytes(buffer);
|
||||
int signatureAlgorithmId = buffer.getInt();
|
||||
byte[] signature = readBytes(buffer);
|
||||
return new SigningInfo(apkDigest, certificate, additionalData, publicKey,
|
||||
signatureAlgorithmId, signature);
|
||||
}
|
||||
|
||||
byte[] toByteArray() {
|
||||
final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize(
|
||||
this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/
|
||||
+ bytesSize(this.signature);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||
writeBytes(buffer, this.apkDigest);
|
||||
writeBytes(buffer, this.certificate);
|
||||
writeBytes(buffer, this.additionalData);
|
||||
writeBytes(buffer, this.publicKey);
|
||||
buffer.putInt(this.signatureAlgorithmId);
|
||||
writeBytes(buffer, this.signature);
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SigningInfoBlock {
|
||||
public final int blockId;
|
||||
public final byte[] signingInfo;
|
||||
|
||||
public SigningInfoBlock(int blockId, byte[] signingInfo) {
|
||||
this.blockId = blockId;
|
||||
this.signingInfo = signingInfo;
|
||||
}
|
||||
|
||||
static SigningInfoBlock fromByteBuffer(ByteBuffer buffer) throws IOException {
|
||||
int blockId = buffer.getInt();
|
||||
byte[] signingInfo = readBytes(buffer);
|
||||
return new SigningInfoBlock(blockId, signingInfo);
|
||||
}
|
||||
|
||||
byte[] toByteArray() {
|
||||
final int size = 4/*blockId*/ + bytesSize(this.signingInfo);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.putInt(this.blockId);
|
||||
writeBytes(buffer, this.signingInfo);
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SigningInfos {
|
||||
public final SigningInfo signingInfo;
|
||||
public final SigningInfoBlock[] signingInfoBlocks;
|
||||
|
||||
public SigningInfos(SigningInfo signingInfo) {
|
||||
this.signingInfo = signingInfo;
|
||||
this.signingInfoBlocks = new SigningInfoBlock[0];
|
||||
}
|
||||
|
||||
public SigningInfos(SigningInfo signingInfo, SigningInfoBlock... signingInfoBlocks) {
|
||||
this.signingInfo = signingInfo;
|
||||
this.signingInfoBlocks = signingInfoBlocks;
|
||||
}
|
||||
|
||||
public static SigningInfos fromByteArray(byte[] bytes) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||
SigningInfo signingInfo = SigningInfo.fromByteBuffer(buffer);
|
||||
if (!buffer.hasRemaining()) {
|
||||
return new SigningInfos(signingInfo);
|
||||
}
|
||||
ArrayList<SigningInfoBlock> signingInfoBlocks = new ArrayList<>(1);
|
||||
while (buffer.hasRemaining()) {
|
||||
signingInfoBlocks.add(SigningInfoBlock.fromByteBuffer(buffer));
|
||||
}
|
||||
return new SigningInfos(signingInfo,
|
||||
signingInfoBlocks.toArray(new SigningInfoBlock[signingInfoBlocks.size()]));
|
||||
}
|
||||
|
||||
byte[] toByteArray() {
|
||||
byte[][] arrays = new byte[1 + this.signingInfoBlocks.length][];
|
||||
arrays[0] = this.signingInfo.toByteArray();
|
||||
int size = arrays[0].length;
|
||||
for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
|
||||
arrays[i + 1] = this.signingInfoBlocks[i].toByteArray();
|
||||
size += arrays[i + 1].length;
|
||||
}
|
||||
if (size > MAX_SIGNING_INFOS_SIZE) {
|
||||
throw new IllegalArgumentException(
|
||||
"Combined SigningInfos length exceeded limit of 7K: " + size);
|
||||
}
|
||||
|
||||
// Combine all arrays into one.
|
||||
byte[] result = Arrays.copyOf(arrays[0], size);
|
||||
int offset = arrays[0].length;
|
||||
for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
|
||||
System.arraycopy(arrays[i + 1], 0, result, offset, arrays[i + 1].length);
|
||||
offset += arrays[i + 1].length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Always 2 for now.
|
||||
public final int version;
|
||||
public final byte[] hashingInfo;
|
||||
// Can contain either SigningInfo or SigningInfo + one or multiple SigningInfoBlock.
|
||||
// Passed as-is to the kernel. Can be retrieved later.
|
||||
public final byte[] signingInfos;
|
||||
|
||||
V4Signature(int version, byte[] hashingInfo, byte[] signingInfos) {
|
||||
this.version = version;
|
||||
this.hashingInfo = hashingInfo;
|
||||
this.signingInfos = signingInfos;
|
||||
}
|
||||
|
||||
static V4Signature readFrom(InputStream stream) throws IOException {
|
||||
final int version = readIntLE(stream);
|
||||
if (version != CURRENT_VERSION) {
|
||||
throw new IOException("Invalid signature version.");
|
||||
}
|
||||
final byte[] hashingInfo = readBytes(stream);
|
||||
final byte[] signingInfo = readBytes(stream);
|
||||
return new V4Signature(version, hashingInfo, signingInfo);
|
||||
}
|
||||
|
||||
public void writeTo(OutputStream stream) throws IOException {
|
||||
writeIntLE(stream, this.version);
|
||||
writeBytes(stream, this.hashingInfo);
|
||||
writeBytes(stream, this.signingInfos);
|
||||
}
|
||||
|
||||
static byte[] getSignedData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
|
||||
final int size =
|
||||
4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize(
|
||||
hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize(
|
||||
signingInfo.apkDigest) + bytesSize(signingInfo.certificate) + bytesSize(
|
||||
signingInfo.additionalData);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.putInt(size);
|
||||
buffer.putLong(fileSize);
|
||||
buffer.putInt(hashingInfo.hashAlgorithm);
|
||||
buffer.put(hashingInfo.log2BlockSize);
|
||||
writeBytes(buffer, hashingInfo.salt);
|
||||
writeBytes(buffer, hashingInfo.rawRootHash);
|
||||
writeBytes(buffer, signingInfo.apkDigest);
|
||||
writeBytes(buffer, signingInfo.certificate);
|
||||
writeBytes(buffer, signingInfo.additionalData);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
// Utility methods.
|
||||
static int bytesSize(byte[] bytes) {
|
||||
return 4/*length*/ + (bytes == null ? 0 : bytes.length);
|
||||
}
|
||||
|
||||
static void readFully(InputStream stream, byte[] buffer) throws IOException {
|
||||
int len = buffer.length;
|
||||
int n = 0;
|
||||
while (n < len) {
|
||||
int count = stream.read(buffer, n, len - n);
|
||||
if (count < 0) {
|
||||
throw new EOFException();
|
||||
}
|
||||
n += count;
|
||||
}
|
||||
}
|
||||
|
||||
static int readIntLE(InputStream stream) throws IOException {
|
||||
final byte[] buffer = new byte[4];
|
||||
readFully(stream, buffer);
|
||||
return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
|
||||
}
|
||||
|
||||
static void writeIntLE(OutputStream stream, int v) throws IOException {
|
||||
final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
|
||||
stream.write(buffer);
|
||||
}
|
||||
|
||||
static byte[] readBytes(InputStream stream) throws IOException {
|
||||
try {
|
||||
final int size = readIntLE(stream);
|
||||
final byte[] bytes = new byte[size];
|
||||
readFully(stream, bytes);
|
||||
return bytes;
|
||||
} catch (EOFException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] readBytes(ByteBuffer buffer) throws IOException {
|
||||
if (buffer.remaining() < 4) {
|
||||
throw new EOFException();
|
||||
}
|
||||
final int size = buffer.getInt();
|
||||
if (buffer.remaining() < size) {
|
||||
throw new EOFException();
|
||||
}
|
||||
final byte[] bytes = new byte[size];
|
||||
buffer.get(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static void writeBytes(OutputStream stream, byte[] bytes) throws IOException {
|
||||
if (bytes == null) {
|
||||
writeIntLE(stream, 0);
|
||||
return;
|
||||
}
|
||||
writeIntLE(stream, bytes.length);
|
||||
stream.write(bytes);
|
||||
}
|
||||
|
||||
static void writeBytes(ByteBuffer buffer, byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
buffer.putInt(0);
|
||||
return;
|
||||
}
|
||||
buffer.putInt(bytes.length);
|
||||
buffer.put(bytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,673 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
import com.android.apksig.internal.asn1.ber.BerDataValue;
|
||||
import com.android.apksig.internal.asn1.ber.BerDataValueFormatException;
|
||||
import com.android.apksig.internal.asn1.ber.BerDataValueReader;
|
||||
import com.android.apksig.internal.asn1.ber.BerEncoding;
|
||||
import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader;
|
||||
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Parser of ASN.1 BER-encoded structures.
|
||||
*
|
||||
* <p>Structure is described to the parser by providing a class annotated with {@link Asn1Class},
|
||||
* containing fields annotated with {@link Asn1Field}.
|
||||
*/
|
||||
public final class Asn1BerParser {
|
||||
private Asn1BerParser() {}
|
||||
|
||||
/**
|
||||
* Returns the ASN.1 structure contained in the BER encoded input.
|
||||
*
|
||||
* @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
|
||||
* is advanced to the first position following the end of the consumed structure.
|
||||
* @param containerClass class describing the structure of the input. The class must meet the
|
||||
* following requirements:
|
||||
* <ul>
|
||||
* <li>The class must be annotated with {@link Asn1Class}.</li>
|
||||
* <li>The class must expose a public no-arg constructor.</li>
|
||||
* <li>Member fields of the class which are populated with parsed input must be
|
||||
* annotated with {@link Asn1Field} and be public and non-final.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @throws Asn1DecodingException if the input could not be decoded into the specified Java
|
||||
* object
|
||||
*/
|
||||
public static <T> T parse(ByteBuffer encoded, Class<T> containerClass)
|
||||
throws Asn1DecodingException {
|
||||
BerDataValue containerDataValue;
|
||||
try {
|
||||
containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
|
||||
} catch (BerDataValueFormatException e) {
|
||||
throw new Asn1DecodingException("Failed to decode top-level data value", e);
|
||||
}
|
||||
if (containerDataValue == null) {
|
||||
throw new Asn1DecodingException("Empty input");
|
||||
}
|
||||
return parse(containerDataValue, containerClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means
|
||||
* that this method does not care whether the tag number of this data structure is
|
||||
* {@code SET OF} and whether the tag class is {@code UNIVERSAL}.
|
||||
*
|
||||
* <p>Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1
|
||||
* SET may contain duplicate elements.
|
||||
*
|
||||
* @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
|
||||
* is advanced to the first position following the end of the consumed structure.
|
||||
* @param elementClass class describing the structure of the values/elements contained in this
|
||||
* container. The class must meet the following requirements:
|
||||
* <ul>
|
||||
* <li>The class must be annotated with {@link Asn1Class}.</li>
|
||||
* <li>The class must expose a public no-arg constructor.</li>
|
||||
* <li>Member fields of the class which are populated with parsed input must be
|
||||
* annotated with {@link Asn1Field} and be public and non-final.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @throws Asn1DecodingException if the input could not be decoded into the specified Java
|
||||
* object
|
||||
*/
|
||||
public static <T> List<T> parseImplicitSetOf(ByteBuffer encoded, Class<T> elementClass)
|
||||
throws Asn1DecodingException {
|
||||
BerDataValue containerDataValue;
|
||||
try {
|
||||
containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
|
||||
} catch (BerDataValueFormatException e) {
|
||||
throw new Asn1DecodingException("Failed to decode top-level data value", e);
|
||||
}
|
||||
if (containerDataValue == null) {
|
||||
throw new Asn1DecodingException("Empty input");
|
||||
}
|
||||
return parseSetOf(containerDataValue, elementClass);
|
||||
}
|
||||
|
||||
private static <T> T parse(BerDataValue container, Class<T> containerClass)
|
||||
throws Asn1DecodingException {
|
||||
if (container == null) {
|
||||
throw new NullPointerException("container == null");
|
||||
}
|
||||
if (containerClass == null) {
|
||||
throw new NullPointerException("containerClass == null");
|
||||
}
|
||||
|
||||
Asn1Type dataType = getContainerAsn1Type(containerClass);
|
||||
switch (dataType) {
|
||||
case CHOICE:
|
||||
return parseChoice(container, containerClass);
|
||||
|
||||
case SEQUENCE:
|
||||
{
|
||||
int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL;
|
||||
int expectedTagNumber = BerEncoding.getTagNumber(dataType);
|
||||
if ((container.getTagClass() != expectedTagClass)
|
||||
|| (container.getTagNumber() != expectedTagNumber)) {
|
||||
throw new Asn1UnexpectedTagException(
|
||||
"Unexpected data value read as " + containerClass.getName()
|
||||
+ ". Expected " + BerEncoding.tagClassAndNumberToString(
|
||||
expectedTagClass, expectedTagNumber)
|
||||
+ ", but read: " + BerEncoding.tagClassAndNumberToString(
|
||||
container.getTagClass(), container.getTagNumber()));
|
||||
}
|
||||
return parseSequence(container, containerClass);
|
||||
}
|
||||
case UNENCODED_CONTAINER:
|
||||
return parseSequence(container, containerClass, true);
|
||||
default:
|
||||
throw new Asn1DecodingException("Parsing container " + dataType + " not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T parseChoice(BerDataValue dataValue, Class<T> containerClass)
|
||||
throws Asn1DecodingException {
|
||||
List<AnnotatedField> fields = getAnnotatedFields(containerClass);
|
||||
if (fields.isEmpty()) {
|
||||
throw new Asn1DecodingException(
|
||||
"No fields annotated with " + Asn1Field.class.getName()
|
||||
+ " in CHOICE class " + containerClass.getName());
|
||||
}
|
||||
|
||||
// Check that class + tagNumber don't clash between the choices
|
||||
for (int i = 0; i < fields.size() - 1; i++) {
|
||||
AnnotatedField f1 = fields.get(i);
|
||||
int tagNumber1 = f1.getBerTagNumber();
|
||||
int tagClass1 = f1.getBerTagClass();
|
||||
for (int j = i + 1; j < fields.size(); j++) {
|
||||
AnnotatedField f2 = fields.get(j);
|
||||
int tagNumber2 = f2.getBerTagNumber();
|
||||
int tagClass2 = f2.getBerTagClass();
|
||||
if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) {
|
||||
throw new Asn1DecodingException(
|
||||
"CHOICE fields are indistinguishable because they have the same tag"
|
||||
+ " class and number: " + containerClass.getName()
|
||||
+ "." + f1.getField().getName()
|
||||
+ " and ." + f2.getField().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate the container object / result
|
||||
T obj;
|
||||
try {
|
||||
obj = containerClass.getConstructor().newInstance();
|
||||
} catch (IllegalArgumentException | ReflectiveOperationException e) {
|
||||
throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
|
||||
}
|
||||
// Set the matching field's value from the data value
|
||||
for (AnnotatedField field : fields) {
|
||||
try {
|
||||
field.setValueFrom(dataValue, obj);
|
||||
return obj;
|
||||
} catch (Asn1UnexpectedTagException expected) {
|
||||
// not a match
|
||||
}
|
||||
}
|
||||
|
||||
throw new Asn1DecodingException(
|
||||
"No options of CHOICE " + containerClass.getName() + " matched");
|
||||
}
|
||||
|
||||
private static <T> T parseSequence(BerDataValue container, Class<T> containerClass)
|
||||
throws Asn1DecodingException {
|
||||
return parseSequence(container, containerClass, false);
|
||||
}
|
||||
|
||||
private static <T> T parseSequence(BerDataValue container, Class<T> containerClass,
|
||||
boolean isUnencodedContainer) throws Asn1DecodingException {
|
||||
List<AnnotatedField> fields = getAnnotatedFields(containerClass);
|
||||
Collections.sort(
|
||||
fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
|
||||
// Check that there are no fields with the same index
|
||||
if (fields.size() > 1) {
|
||||
AnnotatedField lastField = null;
|
||||
for (AnnotatedField field : fields) {
|
||||
if ((lastField != null)
|
||||
&& (lastField.getAnnotation().index() == field.getAnnotation().index())) {
|
||||
throw new Asn1DecodingException(
|
||||
"Fields have the same index: " + containerClass.getName()
|
||||
+ "." + lastField.getField().getName()
|
||||
+ " and ." + field.getField().getName());
|
||||
}
|
||||
lastField = field;
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate the container object / result
|
||||
T t;
|
||||
try {
|
||||
t = containerClass.getConstructor().newInstance();
|
||||
} catch (IllegalArgumentException | ReflectiveOperationException e) {
|
||||
throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
|
||||
}
|
||||
|
||||
// Parse fields one by one. A complication is that there may be optional fields.
|
||||
int nextUnreadFieldIndex = 0;
|
||||
BerDataValueReader elementsReader = container.contentsReader();
|
||||
while (nextUnreadFieldIndex < fields.size()) {
|
||||
BerDataValue dataValue;
|
||||
try {
|
||||
// if this is the first field of an unencoded container then the entire contents of
|
||||
// the container should be used when assigning to this field.
|
||||
if (isUnencodedContainer && nextUnreadFieldIndex == 0) {
|
||||
dataValue = container;
|
||||
} else {
|
||||
dataValue = elementsReader.readDataValue();
|
||||
}
|
||||
} catch (BerDataValueFormatException e) {
|
||||
throw new Asn1DecodingException("Malformed data value", e);
|
||||
}
|
||||
if (dataValue == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = nextUnreadFieldIndex; i < fields.size(); i++) {
|
||||
AnnotatedField field = fields.get(i);
|
||||
try {
|
||||
if (field.isOptional()) {
|
||||
// Optional field -- might not be present and we may thus be trying to set
|
||||
// it from the wrong tag.
|
||||
try {
|
||||
field.setValueFrom(dataValue, t);
|
||||
nextUnreadFieldIndex = i + 1;
|
||||
break;
|
||||
} catch (Asn1UnexpectedTagException e) {
|
||||
// This field is not present, attempt to use this data value for the
|
||||
// next / iteration of the loop
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Mandatory field -- if we can't set its value from this data value, then
|
||||
// it's an error
|
||||
field.setValueFrom(dataValue, t);
|
||||
nextUnreadFieldIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
} catch (Asn1DecodingException e) {
|
||||
throw new Asn1DecodingException(
|
||||
"Failed to parse " + containerClass.getName()
|
||||
+ "." + field.getField().getName(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
// NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness
|
||||
// of elements -- it's an unordered collection.
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> List<T> parseSetOf(BerDataValue container, Class<T> elementClass)
|
||||
throws Asn1DecodingException {
|
||||
List<T> result = new ArrayList<>();
|
||||
BerDataValueReader elementsReader = container.contentsReader();
|
||||
while (true) {
|
||||
BerDataValue dataValue;
|
||||
try {
|
||||
dataValue = elementsReader.readDataValue();
|
||||
} catch (BerDataValueFormatException e) {
|
||||
throw new Asn1DecodingException("Malformed data value", e);
|
||||
}
|
||||
if (dataValue == null) {
|
||||
break;
|
||||
}
|
||||
T element;
|
||||
if (ByteBuffer.class.equals(elementClass)) {
|
||||
element = (T) dataValue.getEncodedContents();
|
||||
} else if (Asn1OpaqueObject.class.equals(elementClass)) {
|
||||
element = (T) new Asn1OpaqueObject(dataValue.getEncoded());
|
||||
} else {
|
||||
element = parse(dataValue, elementClass);
|
||||
}
|
||||
result.add(element);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Asn1Type getContainerAsn1Type(Class<?> containerClass)
|
||||
throws Asn1DecodingException {
|
||||
Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
|
||||
if (containerAnnotation == null) {
|
||||
throw new Asn1DecodingException(
|
||||
containerClass.getName() + " is not annotated with "
|
||||
+ Asn1Class.class.getName());
|
||||
}
|
||||
|
||||
switch (containerAnnotation.type()) {
|
||||
case CHOICE:
|
||||
case SEQUENCE:
|
||||
case UNENCODED_CONTAINER:
|
||||
return containerAnnotation.type();
|
||||
default:
|
||||
throw new Asn1DecodingException(
|
||||
"Unsupported ASN.1 container annotation type: "
|
||||
+ containerAnnotation.type());
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?> getElementType(Field field)
|
||||
throws Asn1DecodingException, ClassNotFoundException {
|
||||
String type = field.getGenericType().getTypeName();
|
||||
int delimiterIndex = type.indexOf('<');
|
||||
if (delimiterIndex == -1) {
|
||||
throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
|
||||
}
|
||||
int startIndex = delimiterIndex + 1;
|
||||
int endIndex = type.indexOf('>', startIndex);
|
||||
// TODO: handle comma?
|
||||
if (endIndex == -1) {
|
||||
throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
|
||||
}
|
||||
String elementClassName = type.substring(startIndex, endIndex);
|
||||
return Class.forName(elementClassName);
|
||||
}
|
||||
|
||||
private static final class AnnotatedField {
|
||||
private final Field mField;
|
||||
private final Asn1Field mAnnotation;
|
||||
private final Asn1Type mDataType;
|
||||
private final Asn1TagClass mTagClass;
|
||||
private final int mBerTagClass;
|
||||
private final int mBerTagNumber;
|
||||
private final Asn1Tagging mTagging;
|
||||
private final boolean mOptional;
|
||||
|
||||
public AnnotatedField(Field field, Asn1Field annotation) throws Asn1DecodingException {
|
||||
mField = field;
|
||||
mAnnotation = annotation;
|
||||
mDataType = annotation.type();
|
||||
|
||||
Asn1TagClass tagClass = annotation.cls();
|
||||
if (tagClass == Asn1TagClass.AUTOMATIC) {
|
||||
if (annotation.tagNumber() != -1) {
|
||||
tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
|
||||
} else {
|
||||
tagClass = Asn1TagClass.UNIVERSAL;
|
||||
}
|
||||
}
|
||||
mTagClass = tagClass;
|
||||
mBerTagClass = BerEncoding.getTagClass(mTagClass);
|
||||
|
||||
int tagNumber;
|
||||
if (annotation.tagNumber() != -1) {
|
||||
tagNumber = annotation.tagNumber();
|
||||
} else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
|
||||
tagNumber = -1;
|
||||
} else {
|
||||
tagNumber = BerEncoding.getTagNumber(mDataType);
|
||||
}
|
||||
mBerTagNumber = tagNumber;
|
||||
|
||||
mTagging = annotation.tagging();
|
||||
if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
|
||||
&& (annotation.tagNumber() == -1)) {
|
||||
throw new Asn1DecodingException(
|
||||
"Tag number must be specified when tagging mode is " + mTagging);
|
||||
}
|
||||
|
||||
mOptional = annotation.optional();
|
||||
}
|
||||
|
||||
public Field getField() {
|
||||
return mField;
|
||||
}
|
||||
|
||||
public Asn1Field getAnnotation() {
|
||||
return mAnnotation;
|
||||
}
|
||||
|
||||
public boolean isOptional() {
|
||||
return mOptional;
|
||||
}
|
||||
|
||||
public int getBerTagClass() {
|
||||
return mBerTagClass;
|
||||
}
|
||||
|
||||
public int getBerTagNumber() {
|
||||
return mBerTagNumber;
|
||||
}
|
||||
|
||||
public void setValueFrom(BerDataValue dataValue, Object obj) throws Asn1DecodingException {
|
||||
int readTagClass = dataValue.getTagClass();
|
||||
if (mBerTagNumber != -1) {
|
||||
int readTagNumber = dataValue.getTagNumber();
|
||||
if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) {
|
||||
throw new Asn1UnexpectedTagException(
|
||||
"Tag mismatch. Expected: "
|
||||
+ BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber)
|
||||
+ ", but found "
|
||||
+ BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber));
|
||||
}
|
||||
} else {
|
||||
if (readTagClass != mBerTagClass) {
|
||||
throw new Asn1UnexpectedTagException(
|
||||
"Tag mismatch. Expected class: "
|
||||
+ BerEncoding.tagClassToString(mBerTagClass)
|
||||
+ ", but found "
|
||||
+ BerEncoding.tagClassToString(readTagClass));
|
||||
}
|
||||
}
|
||||
|
||||
if (mTagging == Asn1Tagging.EXPLICIT) {
|
||||
try {
|
||||
dataValue = dataValue.contentsReader().readDataValue();
|
||||
} catch (BerDataValueFormatException e) {
|
||||
throw new Asn1DecodingException(
|
||||
"Failed to read contents of EXPLICIT data value", e);
|
||||
}
|
||||
}
|
||||
|
||||
BerToJavaConverter.setFieldValue(obj, mField, mDataType, dataValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Asn1UnexpectedTagException extends Asn1DecodingException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public Asn1UnexpectedTagException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException {
|
||||
if (!encodedOid.hasRemaining()) {
|
||||
throw new Asn1DecodingException("Empty OBJECT IDENTIFIER");
|
||||
}
|
||||
|
||||
// First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2
|
||||
long firstComponent = decodeBase128UnsignedLong(encodedOid);
|
||||
int firstNode = (int) Math.min(firstComponent / 40, 2);
|
||||
long secondNode = firstComponent - firstNode * 40;
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(Long.toString(firstNode)).append('.')
|
||||
.append(Long.toString(secondNode));
|
||||
|
||||
// Each consecutive node is encoded as a separate component
|
||||
while (encodedOid.hasRemaining()) {
|
||||
long node = decodeBase128UnsignedLong(encodedOid);
|
||||
result.append('.').append(Long.toString(node));
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException {
|
||||
if (!encoded.hasRemaining()) {
|
||||
return 0;
|
||||
}
|
||||
long result = 0;
|
||||
while (encoded.hasRemaining()) {
|
||||
if (result > Long.MAX_VALUE >>> 7) {
|
||||
throw new Asn1DecodingException("Base-128 number too large");
|
||||
}
|
||||
int b = encoded.get() & 0xff;
|
||||
result <<= 7;
|
||||
result |= b & 0x7f;
|
||||
if ((b & 0x80) == 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
throw new Asn1DecodingException(
|
||||
"Truncated base-128 encoded input: missing terminating byte, with highest bit not"
|
||||
+ " set");
|
||||
}
|
||||
|
||||
private static BigInteger integerToBigInteger(ByteBuffer encoded) {
|
||||
if (!encoded.hasRemaining()) {
|
||||
return BigInteger.ZERO;
|
||||
}
|
||||
return new BigInteger(ByteBufferUtils.toByteArray(encoded));
|
||||
}
|
||||
|
||||
private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException {
|
||||
BigInteger value = integerToBigInteger(encoded);
|
||||
if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
|
||||
|| value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
|
||||
throw new Asn1DecodingException(
|
||||
String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value));
|
||||
}
|
||||
return value.intValue();
|
||||
}
|
||||
|
||||
private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
|
||||
BigInteger value = integerToBigInteger(encoded);
|
||||
if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0
|
||||
|| value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
|
||||
throw new Asn1DecodingException(
|
||||
String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value));
|
||||
}
|
||||
return value.longValue();
|
||||
}
|
||||
|
||||
private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
|
||||
throws Asn1DecodingException {
|
||||
Field[] declaredFields = containerClass.getDeclaredFields();
|
||||
List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
|
||||
for (Field field : declaredFields) {
|
||||
Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
|
||||
if (annotation == null) {
|
||||
continue;
|
||||
}
|
||||
if (Modifier.isStatic(field.getModifiers())) {
|
||||
throw new Asn1DecodingException(
|
||||
Asn1Field.class.getName() + " used on a static field: "
|
||||
+ containerClass.getName() + "." + field.getName());
|
||||
}
|
||||
|
||||
AnnotatedField annotatedField;
|
||||
try {
|
||||
annotatedField = new AnnotatedField(field, annotation);
|
||||
} catch (Asn1DecodingException e) {
|
||||
throw new Asn1DecodingException(
|
||||
"Invalid ASN.1 annotation on "
|
||||
+ containerClass.getName() + "." + field.getName(),
|
||||
e);
|
||||
}
|
||||
result.add(annotatedField);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final class BerToJavaConverter {
|
||||
private BerToJavaConverter() {}
|
||||
|
||||
public static void setFieldValue(
|
||||
Object obj, Field field, Asn1Type type, BerDataValue dataValue)
|
||||
throws Asn1DecodingException {
|
||||
try {
|
||||
switch (type) {
|
||||
case SET_OF:
|
||||
case SEQUENCE_OF:
|
||||
if (Asn1OpaqueObject.class.equals(field.getType())) {
|
||||
field.set(obj, convert(type, dataValue, field.getType()));
|
||||
} else {
|
||||
field.set(obj, parseSetOf(dataValue, getElementType(field)));
|
||||
}
|
||||
return;
|
||||
default:
|
||||
field.set(obj, convert(type, dataValue, field.getType()));
|
||||
break;
|
||||
}
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new Asn1DecodingException(
|
||||
"Failed to set value of " + obj.getClass().getName()
|
||||
+ "." + field.getName(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T convert(
|
||||
Asn1Type sourceType,
|
||||
BerDataValue dataValue,
|
||||
Class<T> targetType) throws Asn1DecodingException {
|
||||
if (ByteBuffer.class.equals(targetType)) {
|
||||
return (T) dataValue.getEncodedContents();
|
||||
} else if (byte[].class.equals(targetType)) {
|
||||
ByteBuffer resultBuf = dataValue.getEncodedContents();
|
||||
if (!resultBuf.hasRemaining()) {
|
||||
return (T) EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
byte[] result = new byte[resultBuf.remaining()];
|
||||
resultBuf.get(result);
|
||||
return (T) result;
|
||||
} else if (Asn1OpaqueObject.class.equals(targetType)) {
|
||||
return (T) new Asn1OpaqueObject(dataValue.getEncoded());
|
||||
}
|
||||
ByteBuffer encodedContents = dataValue.getEncodedContents();
|
||||
switch (sourceType) {
|
||||
case INTEGER:
|
||||
if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) {
|
||||
return (T) Integer.valueOf(integerToInt(encodedContents));
|
||||
} else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) {
|
||||
return (T) Long.valueOf(integerToLong(encodedContents));
|
||||
} else if (BigInteger.class.equals(targetType)) {
|
||||
return (T) integerToBigInteger(encodedContents);
|
||||
}
|
||||
break;
|
||||
case OBJECT_IDENTIFIER:
|
||||
if (String.class.equals(targetType)) {
|
||||
return (T) oidToString(encodedContents);
|
||||
}
|
||||
break;
|
||||
case UTC_TIME:
|
||||
case GENERALIZED_TIME:
|
||||
if (String.class.equals(targetType)) {
|
||||
return (T) new String(ByteBufferUtils.toByteArray(encodedContents));
|
||||
}
|
||||
break;
|
||||
case BOOLEAN:
|
||||
// A boolean should be encoded in a single byte with a value of 0 for false and
|
||||
// any non-zero value for true.
|
||||
if (boolean.class.equals(targetType)) {
|
||||
if (encodedContents.remaining() != 1) {
|
||||
throw new Asn1DecodingException(
|
||||
"Incorrect encoded size of boolean value: "
|
||||
+ encodedContents.remaining());
|
||||
}
|
||||
boolean result;
|
||||
if (encodedContents.get() == 0) {
|
||||
result = false;
|
||||
} else {
|
||||
result = true;
|
||||
}
|
||||
return (T) new Boolean(result);
|
||||
}
|
||||
break;
|
||||
case SEQUENCE:
|
||||
{
|
||||
Asn1Class containerAnnotation =
|
||||
targetType.getDeclaredAnnotation(Asn1Class.class);
|
||||
if ((containerAnnotation != null)
|
||||
&& (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
|
||||
return parseSequence(dataValue, targetType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CHOICE:
|
||||
{
|
||||
Asn1Class containerAnnotation =
|
||||
targetType.getDeclaredAnnotation(Asn1Class.class);
|
||||
if ((containerAnnotation != null)
|
||||
&& (containerAnnotation.type() == Asn1Type.CHOICE)) {
|
||||
return parseChoice(dataValue, targetType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
throw new Asn1DecodingException(
|
||||
"Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Asn1Class {
|
||||
public Asn1Type type();
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
/**
|
||||
* Indicates that input could not be decoded into intended ASN.1 structure.
|
||||
*/
|
||||
public class Asn1DecodingException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public Asn1DecodingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public Asn1DecodingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,596 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
import com.android.apksig.internal.asn1.ber.BerEncoding;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Encoder of ASN.1 structures into DER-encoded form.
|
||||
*
|
||||
* <p>Structure is described to the encoder by providing a class annotated with {@link Asn1Class},
|
||||
* containing fields annotated with {@link Asn1Field}.
|
||||
*/
|
||||
public final class Asn1DerEncoder {
|
||||
private Asn1DerEncoder() {}
|
||||
|
||||
/**
|
||||
* Returns the DER-encoded form of the provided ASN.1 structure.
|
||||
*
|
||||
* @param container container to be encoded. The container's class must meet the following
|
||||
* requirements:
|
||||
* <ul>
|
||||
* <li>The class must be annotated with {@link Asn1Class}.</li>
|
||||
* <li>Member fields of the class which are to be encoded must be annotated with
|
||||
* {@link Asn1Field} and be public.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @throws Asn1EncodingException if the input could not be encoded
|
||||
*/
|
||||
public static byte[] encode(Object container) throws Asn1EncodingException {
|
||||
Class<?> containerClass = container.getClass();
|
||||
Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
|
||||
if (containerAnnotation == null) {
|
||||
throw new Asn1EncodingException(
|
||||
containerClass.getName() + " not annotated with " + Asn1Class.class.getName());
|
||||
}
|
||||
|
||||
Asn1Type containerType = containerAnnotation.type();
|
||||
switch (containerType) {
|
||||
case CHOICE:
|
||||
return toChoice(container);
|
||||
case SEQUENCE:
|
||||
return toSequence(container);
|
||||
case UNENCODED_CONTAINER:
|
||||
return toSequence(container, true);
|
||||
default:
|
||||
throw new Asn1EncodingException("Unsupported container type: " + containerType);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] toChoice(Object container) throws Asn1EncodingException {
|
||||
Class<?> containerClass = container.getClass();
|
||||
List<AnnotatedField> fields = getAnnotatedFields(container);
|
||||
if (fields.isEmpty()) {
|
||||
throw new Asn1EncodingException(
|
||||
"No fields annotated with " + Asn1Field.class.getName()
|
||||
+ " in CHOICE class " + containerClass.getName());
|
||||
}
|
||||
|
||||
AnnotatedField resultField = null;
|
||||
for (AnnotatedField field : fields) {
|
||||
Object fieldValue = getMemberFieldValue(container, field.getField());
|
||||
if (fieldValue != null) {
|
||||
if (resultField != null) {
|
||||
throw new Asn1EncodingException(
|
||||
"Multiple non-null fields in CHOICE class " + containerClass.getName()
|
||||
+ ": " + resultField.getField().getName()
|
||||
+ ", " + field.getField().getName());
|
||||
}
|
||||
resultField = field;
|
||||
}
|
||||
}
|
||||
|
||||
if (resultField == null) {
|
||||
throw new Asn1EncodingException(
|
||||
"No non-null fields in CHOICE class " + containerClass.getName());
|
||||
}
|
||||
|
||||
return resultField.toDer();
|
||||
}
|
||||
|
||||
private static byte[] toSequence(Object container) throws Asn1EncodingException {
|
||||
return toSequence(container, false);
|
||||
}
|
||||
|
||||
private static byte[] toSequence(Object container, boolean omitTag)
|
||||
throws Asn1EncodingException {
|
||||
Class<?> containerClass = container.getClass();
|
||||
List<AnnotatedField> fields = getAnnotatedFields(container);
|
||||
Collections.sort(
|
||||
fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
|
||||
if (fields.size() > 1) {
|
||||
AnnotatedField lastField = null;
|
||||
for (AnnotatedField field : fields) {
|
||||
if ((lastField != null)
|
||||
&& (lastField.getAnnotation().index() == field.getAnnotation().index())) {
|
||||
throw new Asn1EncodingException(
|
||||
"Fields have the same index: " + containerClass.getName()
|
||||
+ "." + lastField.getField().getName()
|
||||
+ " and ." + field.getField().getName());
|
||||
}
|
||||
lastField = field;
|
||||
}
|
||||
}
|
||||
|
||||
List<byte[]> serializedFields = new ArrayList<>(fields.size());
|
||||
int contentLen = 0;
|
||||
for (AnnotatedField field : fields) {
|
||||
byte[] serializedField;
|
||||
try {
|
||||
serializedField = field.toDer();
|
||||
} catch (Asn1EncodingException e) {
|
||||
throw new Asn1EncodingException(
|
||||
"Failed to encode " + containerClass.getName()
|
||||
+ "." + field.getField().getName(),
|
||||
e);
|
||||
}
|
||||
if (serializedField != null) {
|
||||
serializedFields.add(serializedField);
|
||||
contentLen += serializedField.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (omitTag) {
|
||||
byte[] unencodedResult = new byte[contentLen];
|
||||
int index = 0;
|
||||
for (byte[] serializedField : serializedFields) {
|
||||
System.arraycopy(serializedField, 0, unencodedResult, index, serializedField.length);
|
||||
index += serializedField.length;
|
||||
}
|
||||
return unencodedResult;
|
||||
} else {
|
||||
return createTag(
|
||||
BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE,
|
||||
serializedFields.toArray(new byte[0][]));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] toSetOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
|
||||
return toSequenceOrSetOf(values, elementType, true);
|
||||
}
|
||||
|
||||
private static byte[] toSequenceOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
|
||||
return toSequenceOrSetOf(values, elementType, false);
|
||||
}
|
||||
|
||||
private static byte[] toSequenceOrSetOf(Collection<?> values, Asn1Type elementType, boolean toSet)
|
||||
throws Asn1EncodingException {
|
||||
List<byte[]> serializedValues = new ArrayList<>(values.size());
|
||||
for (Object value : values) {
|
||||
serializedValues.add(JavaToDerConverter.toDer(value, elementType, null));
|
||||
}
|
||||
int tagNumber;
|
||||
if (toSet) {
|
||||
if (serializedValues.size() > 1) {
|
||||
Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE);
|
||||
}
|
||||
tagNumber = BerEncoding.TAG_NUMBER_SET;
|
||||
} else {
|
||||
tagNumber = BerEncoding.TAG_NUMBER_SEQUENCE;
|
||||
}
|
||||
return createTag(
|
||||
BerEncoding.TAG_CLASS_UNIVERSAL, true, tagNumber,
|
||||
serializedValues.toArray(new byte[0][]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two bytes arrays based on their lexicographic order. Corresponding elements of the
|
||||
* two arrays are compared in ascending order. Elements at out of range indices are assumed to
|
||||
* be smaller than the smallest possible value for an element.
|
||||
*/
|
||||
private static class ByteArrayLexicographicComparator implements Comparator<byte[]> {
|
||||
private static final ByteArrayLexicographicComparator INSTANCE =
|
||||
new ByteArrayLexicographicComparator();
|
||||
|
||||
@Override
|
||||
public int compare(byte[] arr1, byte[] arr2) {
|
||||
int commonLength = Math.min(arr1.length, arr2.length);
|
||||
for (int i = 0; i < commonLength; i++) {
|
||||
int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff);
|
||||
if (diff != 0) {
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
return arr1.length - arr2.length;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<AnnotatedField> getAnnotatedFields(Object container)
|
||||
throws Asn1EncodingException {
|
||||
Class<?> containerClass = container.getClass();
|
||||
Field[] declaredFields = containerClass.getDeclaredFields();
|
||||
List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
|
||||
for (Field field : declaredFields) {
|
||||
Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
|
||||
if (annotation == null) {
|
||||
continue;
|
||||
}
|
||||
if (Modifier.isStatic(field.getModifiers())) {
|
||||
throw new Asn1EncodingException(
|
||||
Asn1Field.class.getName() + " used on a static field: "
|
||||
+ containerClass.getName() + "." + field.getName());
|
||||
}
|
||||
|
||||
AnnotatedField annotatedField;
|
||||
try {
|
||||
annotatedField = new AnnotatedField(container, field, annotation);
|
||||
} catch (Asn1EncodingException e) {
|
||||
throw new Asn1EncodingException(
|
||||
"Invalid ASN.1 annotation on "
|
||||
+ containerClass.getName() + "." + field.getName(),
|
||||
e);
|
||||
}
|
||||
result.add(annotatedField);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] toInteger(int value) {
|
||||
return toInteger((long) value);
|
||||
}
|
||||
|
||||
private static byte[] toInteger(long value) {
|
||||
return toInteger(BigInteger.valueOf(value));
|
||||
}
|
||||
|
||||
private static byte[] toInteger(BigInteger value) {
|
||||
return createTag(
|
||||
BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER,
|
||||
value.toByteArray());
|
||||
}
|
||||
|
||||
private static byte[] toBoolean(boolean value) {
|
||||
// A boolean should be encoded in a single byte with a value of 0 for false and any non-zero
|
||||
// value for true.
|
||||
byte[] result = new byte[1];
|
||||
if (value == false) {
|
||||
result[0] = 0;
|
||||
} else {
|
||||
result[0] = 1;
|
||||
}
|
||||
return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_BOOLEAN, result);
|
||||
}
|
||||
|
||||
private static byte[] toOid(String oid) throws Asn1EncodingException {
|
||||
ByteArrayOutputStream encodedValue = new ByteArrayOutputStream();
|
||||
String[] nodes = oid.split("\\.");
|
||||
if (nodes.length < 2) {
|
||||
throw new Asn1EncodingException(
|
||||
"OBJECT IDENTIFIER must contain at least two nodes: " + oid);
|
||||
}
|
||||
int firstNode;
|
||||
try {
|
||||
firstNode = Integer.parseInt(nodes[0]);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]);
|
||||
}
|
||||
if ((firstNode > 6) || (firstNode < 0)) {
|
||||
throw new Asn1EncodingException("Invalid value for node #1: " + firstNode);
|
||||
}
|
||||
|
||||
int secondNode;
|
||||
try {
|
||||
secondNode = Integer.parseInt(nodes[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]);
|
||||
}
|
||||
if ((secondNode >= 40) || (secondNode < 0)) {
|
||||
throw new Asn1EncodingException("Invalid value for node #2: " + secondNode);
|
||||
}
|
||||
int firstByte = firstNode * 40 + secondNode;
|
||||
if (firstByte > 0xff) {
|
||||
throw new Asn1EncodingException(
|
||||
"First two nodes out of range: " + firstNode + "." + secondNode);
|
||||
}
|
||||
|
||||
encodedValue.write(firstByte);
|
||||
for (int i = 2; i < nodes.length; i++) {
|
||||
String nodeString = nodes[i];
|
||||
int node;
|
||||
try {
|
||||
node = Integer.parseInt(nodeString);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString);
|
||||
}
|
||||
if (node < 0) {
|
||||
throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node);
|
||||
}
|
||||
if (node <= 0x7f) {
|
||||
encodedValue.write(node);
|
||||
continue;
|
||||
}
|
||||
if (node < 1 << 14) {
|
||||
encodedValue.write(0x80 | (node >> 7));
|
||||
encodedValue.write(node & 0x7f);
|
||||
continue;
|
||||
}
|
||||
if (node < 1 << 21) {
|
||||
encodedValue.write(0x80 | (node >> 14));
|
||||
encodedValue.write(0x80 | ((node >> 7) & 0x7f));
|
||||
encodedValue.write(node & 0x7f);
|
||||
continue;
|
||||
}
|
||||
throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node);
|
||||
}
|
||||
|
||||
return createTag(
|
||||
BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER,
|
||||
encodedValue.toByteArray());
|
||||
}
|
||||
|
||||
private static Object getMemberFieldValue(Object obj, Field field)
|
||||
throws Asn1EncodingException {
|
||||
try {
|
||||
return field.get(obj);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new Asn1EncodingException(
|
||||
"Failed to read " + obj.getClass().getName() + "." + field.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AnnotatedField {
|
||||
private final Field mField;
|
||||
private final Object mObject;
|
||||
private final Asn1Field mAnnotation;
|
||||
private final Asn1Type mDataType;
|
||||
private final Asn1Type mElementDataType;
|
||||
private final Asn1TagClass mTagClass;
|
||||
private final int mDerTagClass;
|
||||
private final int mDerTagNumber;
|
||||
private final Asn1Tagging mTagging;
|
||||
private final boolean mOptional;
|
||||
|
||||
public AnnotatedField(Object obj, Field field, Asn1Field annotation)
|
||||
throws Asn1EncodingException {
|
||||
mObject = obj;
|
||||
mField = field;
|
||||
mAnnotation = annotation;
|
||||
mDataType = annotation.type();
|
||||
mElementDataType = annotation.elementType();
|
||||
|
||||
Asn1TagClass tagClass = annotation.cls();
|
||||
if (tagClass == Asn1TagClass.AUTOMATIC) {
|
||||
if (annotation.tagNumber() != -1) {
|
||||
tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
|
||||
} else {
|
||||
tagClass = Asn1TagClass.UNIVERSAL;
|
||||
}
|
||||
}
|
||||
mTagClass = tagClass;
|
||||
mDerTagClass = BerEncoding.getTagClass(mTagClass);
|
||||
|
||||
int tagNumber;
|
||||
if (annotation.tagNumber() != -1) {
|
||||
tagNumber = annotation.tagNumber();
|
||||
} else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
|
||||
tagNumber = -1;
|
||||
} else {
|
||||
tagNumber = BerEncoding.getTagNumber(mDataType);
|
||||
}
|
||||
mDerTagNumber = tagNumber;
|
||||
|
||||
mTagging = annotation.tagging();
|
||||
if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
|
||||
&& (annotation.tagNumber() == -1)) {
|
||||
throw new Asn1EncodingException(
|
||||
"Tag number must be specified when tagging mode is " + mTagging);
|
||||
}
|
||||
|
||||
mOptional = annotation.optional();
|
||||
}
|
||||
|
||||
public Field getField() {
|
||||
return mField;
|
||||
}
|
||||
|
||||
public Asn1Field getAnnotation() {
|
||||
return mAnnotation;
|
||||
}
|
||||
|
||||
public byte[] toDer() throws Asn1EncodingException {
|
||||
Object fieldValue = getMemberFieldValue(mObject, mField);
|
||||
if (fieldValue == null) {
|
||||
if (mOptional) {
|
||||
return null;
|
||||
}
|
||||
throw new Asn1EncodingException("Required field not set");
|
||||
}
|
||||
|
||||
byte[] encoded = JavaToDerConverter.toDer(fieldValue, mDataType, mElementDataType);
|
||||
switch (mTagging) {
|
||||
case NORMAL:
|
||||
return encoded;
|
||||
case EXPLICIT:
|
||||
return createTag(mDerTagClass, true, mDerTagNumber, encoded);
|
||||
case IMPLICIT:
|
||||
int originalTagNumber = BerEncoding.getTagNumber(encoded[0]);
|
||||
if (originalTagNumber == 0x1f) {
|
||||
throw new Asn1EncodingException("High-tag-number form not supported");
|
||||
}
|
||||
if (mDerTagNumber >= 0x1f) {
|
||||
throw new Asn1EncodingException(
|
||||
"Unsupported high tag number: " + mDerTagNumber);
|
||||
}
|
||||
encoded[0] = BerEncoding.setTagNumber(encoded[0], mDerTagNumber);
|
||||
encoded[0] = BerEncoding.setTagClass(encoded[0], mDerTagClass);
|
||||
return encoded;
|
||||
default:
|
||||
throw new RuntimeException("Unknown tagging mode: " + mTagging);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] createTag(
|
||||
int tagClass, boolean constructed, int tagNumber, byte[]... contents) {
|
||||
if (tagNumber >= 0x1f) {
|
||||
throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber);
|
||||
}
|
||||
// tag class & number fit into the first byte
|
||||
byte firstIdentifierByte =
|
||||
(byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber);
|
||||
|
||||
int contentsLength = 0;
|
||||
for (byte[] c : contents) {
|
||||
contentsLength += c.length;
|
||||
}
|
||||
int contentsPosInResult;
|
||||
byte[] result;
|
||||
if (contentsLength < 0x80) {
|
||||
// Length fits into one byte
|
||||
contentsPosInResult = 2;
|
||||
result = new byte[contentsPosInResult + contentsLength];
|
||||
result[0] = firstIdentifierByte;
|
||||
result[1] = (byte) contentsLength;
|
||||
} else {
|
||||
// Length is represented as multiple bytes
|
||||
// The low 7 bits of the first byte represent the number of length bytes (following the
|
||||
// first byte) in which the length is in big-endian base-256 form
|
||||
if (contentsLength <= 0xff) {
|
||||
contentsPosInResult = 3;
|
||||
result = new byte[contentsPosInResult + contentsLength];
|
||||
result[1] = (byte) 0x81; // 1 length byte
|
||||
result[2] = (byte) contentsLength;
|
||||
} else if (contentsLength <= 0xffff) {
|
||||
contentsPosInResult = 4;
|
||||
result = new byte[contentsPosInResult + contentsLength];
|
||||
result[1] = (byte) 0x82; // 2 length bytes
|
||||
result[2] = (byte) (contentsLength >> 8);
|
||||
result[3] = (byte) (contentsLength & 0xff);
|
||||
} else if (contentsLength <= 0xffffff) {
|
||||
contentsPosInResult = 5;
|
||||
result = new byte[contentsPosInResult + contentsLength];
|
||||
result[1] = (byte) 0x83; // 3 length bytes
|
||||
result[2] = (byte) (contentsLength >> 16);
|
||||
result[3] = (byte) ((contentsLength >> 8) & 0xff);
|
||||
result[4] = (byte) (contentsLength & 0xff);
|
||||
} else {
|
||||
contentsPosInResult = 6;
|
||||
result = new byte[contentsPosInResult + contentsLength];
|
||||
result[1] = (byte) 0x84; // 4 length bytes
|
||||
result[2] = (byte) (contentsLength >> 24);
|
||||
result[3] = (byte) ((contentsLength >> 16) & 0xff);
|
||||
result[4] = (byte) ((contentsLength >> 8) & 0xff);
|
||||
result[5] = (byte) (contentsLength & 0xff);
|
||||
}
|
||||
result[0] = firstIdentifierByte;
|
||||
}
|
||||
for (byte[] c : contents) {
|
||||
System.arraycopy(c, 0, result, contentsPosInResult, c.length);
|
||||
contentsPosInResult += c.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final class JavaToDerConverter {
|
||||
private JavaToDerConverter() {}
|
||||
|
||||
public static byte[] toDer(Object source, Asn1Type targetType, Asn1Type targetElementType)
|
||||
throws Asn1EncodingException {
|
||||
Class<?> sourceType = source.getClass();
|
||||
if (Asn1OpaqueObject.class.equals(sourceType)) {
|
||||
ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded();
|
||||
byte[] result = new byte[buf.remaining()];
|
||||
buf.get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if ((targetType == null) || (targetType == Asn1Type.ANY)) {
|
||||
return encode(source);
|
||||
}
|
||||
|
||||
switch (targetType) {
|
||||
case OCTET_STRING:
|
||||
case BIT_STRING:
|
||||
byte[] value = null;
|
||||
if (source instanceof ByteBuffer) {
|
||||
ByteBuffer buf = (ByteBuffer) source;
|
||||
value = new byte[buf.remaining()];
|
||||
buf.slice().get(value);
|
||||
} else if (source instanceof byte[]) {
|
||||
value = (byte[]) source;
|
||||
}
|
||||
if (value != null) {
|
||||
return createTag(
|
||||
BerEncoding.TAG_CLASS_UNIVERSAL,
|
||||
false,
|
||||
BerEncoding.getTagNumber(targetType),
|
||||
value);
|
||||
}
|
||||
break;
|
||||
case INTEGER:
|
||||
if (source instanceof Integer) {
|
||||
return toInteger((Integer) source);
|
||||
} else if (source instanceof Long) {
|
||||
return toInteger((Long) source);
|
||||
} else if (source instanceof BigInteger) {
|
||||
return toInteger((BigInteger) source);
|
||||
}
|
||||
break;
|
||||
case BOOLEAN:
|
||||
if (source instanceof Boolean) {
|
||||
return toBoolean((Boolean) (source));
|
||||
}
|
||||
break;
|
||||
case UTC_TIME:
|
||||
case GENERALIZED_TIME:
|
||||
if (source instanceof String) {
|
||||
return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false,
|
||||
BerEncoding.getTagNumber(targetType), ((String) source).getBytes());
|
||||
}
|
||||
break;
|
||||
case OBJECT_IDENTIFIER:
|
||||
if (source instanceof String) {
|
||||
return toOid((String) source);
|
||||
}
|
||||
break;
|
||||
case SEQUENCE:
|
||||
{
|
||||
Asn1Class containerAnnotation =
|
||||
sourceType.getDeclaredAnnotation(Asn1Class.class);
|
||||
if ((containerAnnotation != null)
|
||||
&& (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
|
||||
return toSequence(source);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CHOICE:
|
||||
{
|
||||
Asn1Class containerAnnotation =
|
||||
sourceType.getDeclaredAnnotation(Asn1Class.class);
|
||||
if ((containerAnnotation != null)
|
||||
&& (containerAnnotation.type() == Asn1Type.CHOICE)) {
|
||||
return toChoice(source);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SET_OF:
|
||||
return toSetOf((Collection<?>) source, targetElementType);
|
||||
case SEQUENCE_OF:
|
||||
return toSequenceOf((Collection<?>) source, targetElementType);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
throw new Asn1EncodingException(
|
||||
"Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
|
||||
}
|
||||
}
|
||||
/** ASN.1 DER-encoded {@code NULL}. */
|
||||
public static final Asn1OpaqueObject ASN1_DER_NULL =
|
||||
new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
/**
|
||||
* Indicates that an ASN.1 structure could not be encoded.
|
||||
*/
|
||||
public class Asn1EncodingException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public Asn1EncodingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public Asn1EncodingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Asn1Field {
|
||||
/** Index used to order fields in a container. Required for fields of SEQUENCE containers. */
|
||||
public int index() default 0;
|
||||
|
||||
public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC;
|
||||
|
||||
public Asn1Type type();
|
||||
|
||||
/** Tagging mode. Default: NORMAL. */
|
||||
public Asn1Tagging tagging() default Asn1Tagging.NORMAL;
|
||||
|
||||
/** Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.*/
|
||||
public int tagNumber() default -1;
|
||||
|
||||
/** {@code true} if this field is optional. Ignored for fields of CHOICE containers. */
|
||||
public boolean optional() default false;
|
||||
|
||||
/** Type of elements. Used only for SET_OF or SEQUENCE_OF. */
|
||||
public Asn1Type elementType() default Asn1Type.ANY;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Opaque holder of encoded ASN.1 stuff.
|
||||
*/
|
||||
public class Asn1OpaqueObject {
|
||||
private final ByteBuffer mEncoded;
|
||||
|
||||
public Asn1OpaqueObject(ByteBuffer encoded) {
|
||||
mEncoded = encoded.slice();
|
||||
}
|
||||
|
||||
public Asn1OpaqueObject(byte[] encoded) {
|
||||
mEncoded = ByteBuffer.wrap(encoded);
|
||||
}
|
||||
|
||||
public ByteBuffer getEncoded() {
|
||||
return mEncoded.slice();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
public enum Asn1TagClass {
|
||||
UNIVERSAL,
|
||||
APPLICATION,
|
||||
CONTEXT_SPECIFIC,
|
||||
PRIVATE,
|
||||
|
||||
/**
|
||||
* Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class
|
||||
* automatically.
|
||||
*/
|
||||
AUTOMATIC,
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
public enum Asn1Tagging {
|
||||
NORMAL,
|
||||
EXPLICIT,
|
||||
IMPLICIT,
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1;
|
||||
|
||||
public enum Asn1Type {
|
||||
ANY,
|
||||
CHOICE,
|
||||
INTEGER,
|
||||
OBJECT_IDENTIFIER,
|
||||
OCTET_STRING,
|
||||
SEQUENCE,
|
||||
SEQUENCE_OF,
|
||||
SET_OF,
|
||||
BIT_STRING,
|
||||
UTC_TIME,
|
||||
GENERALIZED_TIME,
|
||||
BOOLEAN,
|
||||
// This type can be used to annotate classes that encapsulate ASN.1 structures that are not
|
||||
// classified as a SEQUENCE or SET.
|
||||
UNENCODED_CONTAINER
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.asn1.ber;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
|
||||
*/
|
||||
public class BerDataValue {
|
||||
private final ByteBuffer mEncoded;
|
||||
private final ByteBuffer mEncodedContents;
|
||||
private final int mTagClass;
|
||||
private final boolean mConstructed;
|
||||
private final int mTagNumber;
|
||||
|
||||
BerDataValue(
|
||||
ByteBuffer encoded,
|
||||
ByteBuffer encodedContents,
|
||||
int tagClass,
|
||||
boolean constructed,
|
||||
int tagNumber) {
|
||||
mEncoded = encoded;
|
||||
mEncodedContents = encodedContents;
|
||||
mTagClass = tagClass;
|
||||
mConstructed = constructed;
|
||||
mTagNumber = tagNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS}
|
||||
* constants.
|
||||
*/
|
||||
public int getTagClass() {
|
||||
return mTagClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the content octets of this data value are the complete BER encoding
|
||||
* of one or more data values, {@code false} if the content octets of this data value directly
|
||||
* represent the value.
|
||||
*/
|
||||
public boolean isConstructed() {
|
||||
return mConstructed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER}
|
||||
* constants.
|
||||
*/
|
||||
public int getTagNumber() {
|
||||
return mTagNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encoded form of this data value.
|
||||
*/
|
||||
public ByteBuffer getEncoded() {
|
||||
return mEncoded.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encoded contents of this data value.
|
||||
*/
|
||||
public ByteBuffer getEncodedContents() {
|
||||
return mEncodedContents.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new reader of the contents of this data value.
|
||||
*/
|
||||
public BerDataValueReader contentsReader() {
|
||||
return new ByteBufferBerDataValueReader(getEncodedContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new reader which returns just this data value. This may be useful for re-reading
|
||||
* this value in different contexts.
|
||||
*/
|
||||
public BerDataValueReader dataValueReader() {
|
||||
return new ParsedValueReader(this);
|
||||
}
|
||||
|
||||
private static final class ParsedValueReader implements BerDataValueReader {
|
||||
private final BerDataValue mValue;
|
||||
private boolean mValueOutput;
|
||||
|
||||
public ParsedValueReader(BerDataValue value) {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BerDataValue readDataValue() throws BerDataValueFormatException {
|
||||
if (mValueOutput) {
|
||||
return null;
|
||||
}
|
||||
mValueOutput = true;
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue