mirror of
https://github.com/Maschell/JNUSLib.git
synced 2024-11-21 15:39:18 +01:00
Code clean up, changed to a maven project
This commit is contained in:
parent
b332dc1bcd
commit
14c987456d
22
.classpath
22
.classpath
@ -1,7 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="lib" path="libs/lombok.jar"/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
<classpathentry kind="src" output="target/classes" path="src">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" path="test"/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
/bin/
|
||||
/target/
|
||||
|
6
.project
6
.project
@ -10,8 +10,14 @@
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
|
295
eclipse_code_convention.xml
Normal file
295
eclipse_code_convention.xml
Normal file
@ -0,0 +1,295 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<profiles version="12">
|
||||
<profile kind="CodeFormatterProfile" name="JNUSLib Code Convention" version="12">
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="48"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="160"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.source" value="1.8"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.8"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.8"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="160"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
|
||||
</profile>
|
||||
</profiles>
|
41
pom.xml
Normal file
41
pom.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>de.mas.wiiu.jnus</groupId>
|
||||
<artifactId>library</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<configuration>
|
||||
<source/>
|
||||
<target/>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.16.12</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -1,336 +0,0 @@
|
||||
package de.mas.jnus.lib;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.mas.jnus.lib.entities.TMD;
|
||||
import de.mas.jnus.lib.entities.Ticket;
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.entities.fst.FSTEntry;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProvider;
|
||||
import de.mas.jnus.lib.utils.HashUtil;
|
||||
import de.mas.jnus.lib.utils.StreamUtils;
|
||||
import de.mas.jnus.lib.utils.Utils;
|
||||
import de.mas.jnus.lib.utils.cryptography.NUSDecryption;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class DecryptionService {
|
||||
private static Map<NUSTitle,DecryptionService> instances = new HashMap<>();
|
||||
|
||||
public static DecryptionService getInstance(NUSTitle nustitle) {
|
||||
if(!instances.containsKey(nustitle)){
|
||||
instances.put(nustitle, new DecryptionService(nustitle));
|
||||
}
|
||||
return instances.get(nustitle);
|
||||
}
|
||||
|
||||
@Getter @Setter private NUSTitle NUSTitle = null;
|
||||
|
||||
private DecryptionService(NUSTitle nustitle){
|
||||
setNUSTitle(nustitle);
|
||||
}
|
||||
|
||||
public Ticket getTicket() {
|
||||
return getNUSTitle().getTicket();
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(boolean useFullPath,FSTEntry entry, String outputPath, boolean skipExistingFile) throws IOException {
|
||||
if(entry.isNotInPackage() || entry.getContent() == null){
|
||||
return;
|
||||
}
|
||||
|
||||
String targetFilePath = new StringBuilder().append(outputPath).append("/").append(entry.getFilename()).toString();
|
||||
String fullPath = new StringBuilder().append(outputPath).toString();
|
||||
|
||||
if(useFullPath){
|
||||
targetFilePath = new StringBuilder().append(outputPath).append(entry.getFullPath()).toString();
|
||||
fullPath = new StringBuilder().append(outputPath).append(entry.getPath()).toString();
|
||||
if(entry.isDir()){ //If the entry is a directory. Create it and return.
|
||||
Utils.createDir(targetFilePath);
|
||||
return;
|
||||
}
|
||||
}else if(entry.isDir()){
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Utils.createDir(fullPath)){
|
||||
return;
|
||||
}
|
||||
|
||||
File target = new File(targetFilePath);
|
||||
|
||||
if(skipExistingFile){
|
||||
File targetFile = new File(targetFilePath);
|
||||
if(targetFile.exists()){
|
||||
if(entry.isDir()){
|
||||
return;
|
||||
}
|
||||
if(targetFile.length() == entry.getFileSize()){
|
||||
Content c = entry.getContent();
|
||||
if(!c.isHashed()){
|
||||
if(Arrays.equals(HashUtil.hashSHA1(target,(int) c.getEncryptedFileSize()), c.getSHA2Hash())){
|
||||
System.out.println("File already exists: " + entry.getFilename());
|
||||
return;
|
||||
}else{
|
||||
System.out.println("File already exists with the same filesize, but the hash doesn't match: " + entry.getFilename());
|
||||
}
|
||||
}else{
|
||||
System.out.println("File already exists: " + entry.getFilename());
|
||||
return;
|
||||
}
|
||||
|
||||
}else{
|
||||
System.out.println("File already exists but the filesize doesn't match: " + entry.getFilename());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath));
|
||||
|
||||
System.out.println("Decrypting " + entry.getFilename());
|
||||
decryptFSTEntryToStream(entry, outputStream);
|
||||
}
|
||||
|
||||
public void decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream) throws IOException {
|
||||
if(entry.isNotInPackage() || entry.getContent() == null){
|
||||
return;
|
||||
}
|
||||
|
||||
Content c = entry.getContent();
|
||||
|
||||
long fileSize = entry.getFileSize();
|
||||
long fileOffset = entry.getFileOffset();
|
||||
long fileOffsetBlock = entry.getFileOffsetBlock();
|
||||
|
||||
NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
|
||||
InputStream in = dataProvider.getInputStreamFromContent(c, fileOffsetBlock);
|
||||
|
||||
decryptFSTEntryFromStreams(in,outputStream,fileSize,fileOffset,c);
|
||||
}
|
||||
|
||||
|
||||
private void decryptFSTEntryFromStreams(InputStream inputStream, OutputStream outputStream,long filesize, long fileoffset, Content content) throws IOException{
|
||||
decryptStreams(inputStream, outputStream, filesize, fileoffset, content);
|
||||
}
|
||||
|
||||
private void decryptContentFromStream(InputStream inputStream, OutputStream outputStream,Content content) throws IOException {
|
||||
long filesize = content.getDecryptedFileSize();
|
||||
System.out.println("Decrypting Content " + String.format("%08X", content.getID()));
|
||||
decryptStreams(inputStream, outputStream, filesize, 0L, content);
|
||||
}
|
||||
|
||||
|
||||
private void decryptStreams(InputStream inputStream, OutputStream outputStream,long size, long offset, Content content) throws IOException{
|
||||
NUSDecryption nusdecryption = new NUSDecryption(getTicket());
|
||||
short contentIndex = (short)content.getIndex();
|
||||
|
||||
long encryptedFileSize = content.getEncryptedFileSize();
|
||||
if(!content.isEncrypted()){
|
||||
StreamUtils.saveInputStreamToOutputStreamWithHash(inputStream, outputStream,size,content.getSHA2Hash(),encryptedFileSize);
|
||||
}else{
|
||||
if(content.isHashed()){
|
||||
NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
|
||||
byte[] h3 = dataProvider.getContentH3Hash(content);
|
||||
nusdecryption.decryptFileStreamHashed(inputStream, outputStream, size, offset, (short) contentIndex, h3);
|
||||
}else{
|
||||
nusdecryption.decryptFileStream(inputStream, outputStream, size, (short)contentIndex,content.getSHA2Hash(),encryptedFileSize);
|
||||
}
|
||||
}
|
||||
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
|
||||
public void decryptContentTo(Content content,String outPath,boolean skipExistingFile) throws IOException {
|
||||
String targetFilePath = outPath + File.separator + content.getFilenameDecrypted();
|
||||
if(skipExistingFile){
|
||||
File targetFile = new File(targetFilePath);
|
||||
if(targetFile.exists()){
|
||||
if(targetFile.length() == content.getDecryptedFileSize()){
|
||||
System.out.println("File already exists : " + content.getFilenameDecrypted());
|
||||
return;
|
||||
}else{
|
||||
System.out.println("File already exists but the filesize doesn't match: " +content.getFilenameDecrypted());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!Utils.createDir(outPath)){
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("Decrypting Content " + String.format("%08X", content.getID()));
|
||||
|
||||
FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath));
|
||||
|
||||
decryptContentToStream(content, outputStream);
|
||||
}
|
||||
|
||||
|
||||
public void decryptContentToStream(Content content, OutputStream outputStream) throws IOException {
|
||||
if(content == null){
|
||||
return;
|
||||
}
|
||||
|
||||
NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
|
||||
InputStream inputStream = dataProvider.getInputStreamFromContent(content, 0);
|
||||
|
||||
decryptContentFromStream(inputStream,outputStream,content);
|
||||
}
|
||||
|
||||
public InputStream getDecryptedOutputAsInputStream(FSTEntry fstEntry) throws IOException {
|
||||
PipedInputStream in = new PipedInputStream();
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
|
||||
new Thread(() -> {try {
|
||||
decryptFSTEntryToStream(fstEntry, out);
|
||||
} catch (IOException e) {e.printStackTrace();}}).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
public InputStream getDecryptedContentAsInputStream(Content content) throws IOException {
|
||||
PipedInputStream in = new PipedInputStream();
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
|
||||
new Thread(() -> {try {
|
||||
decryptContentToStream(content, out);
|
||||
} catch (IOException e) {e.printStackTrace();}}).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
//Decrypt FSTEntry to OutputStream
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
public void decryptFSTEntryTo(String entryFullPath,OutputStream outputStream) throws IOException{
|
||||
FSTEntry entry = getNUSTitle().getFSTEntryByFullPath(entryFullPath);
|
||||
if(entry == null){
|
||||
System.out.println("File not found");
|
||||
}
|
||||
|
||||
decryptFSTEntryToStream(entry, outputStream);
|
||||
}
|
||||
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
//Decrypt single FSTEntry to File
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
public void decryptFSTEntryTo(String entryFullPath,String outputFolder) throws IOException{
|
||||
decryptFSTEntryTo(false,entryFullPath,outputFolder);
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(boolean fullPath,String entryFullPath,String outputFolder) throws IOException{
|
||||
decryptFSTEntryTo(fullPath,entryFullPath,outputFolder,getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(String entryFullPath,String outputFolder, boolean skipExistingFiles) throws IOException{
|
||||
decryptFSTEntryTo(false,entryFullPath,outputFolder,getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(boolean fullPath, String entryFullPath,String outputFolder, boolean skipExistingFiles) throws IOException{
|
||||
FSTEntry entry = getNUSTitle().getFSTEntryByFullPath(entryFullPath);
|
||||
if(entry == null){
|
||||
System.out.println("File not found");
|
||||
return;
|
||||
}
|
||||
|
||||
decryptFSTEntryTo(fullPath,entry, outputFolder,skipExistingFiles);
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(FSTEntry entry,String outputFolder) throws IOException{
|
||||
decryptFSTEntryTo(false,entry, outputFolder);
|
||||
}
|
||||
public void decryptFSTEntryTo(boolean fullPath,FSTEntry entry,String outputFolder) throws IOException{
|
||||
decryptFSTEntryTo(fullPath,entry,outputFolder,getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(FSTEntry entry,String outputFolder, boolean skipExistingFiles) throws IOException{
|
||||
decryptFSTEntryTo(false,entry,outputFolder,getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
|
||||
/*
|
||||
public void decryptFSTEntryTo(boolean fullPath, FSTEntry entry,String outputFolder, boolean skipExistingFiles) throws IOException{
|
||||
decryptFSTEntry(fullPath,entry,outputFolder,skipExistingFiles);
|
||||
}*/
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
//Decrypt list of FSTEntry to Files
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
public void decryptAllFSTEntriesTo(String outputFolder) throws IOException {
|
||||
decryptFSTEntriesTo(true, ".*", outputFolder);
|
||||
}
|
||||
|
||||
public void decryptFSTEntriesTo(String regEx, String outputFolder) throws IOException {
|
||||
decryptFSTEntriesTo(true,regEx, outputFolder);
|
||||
}
|
||||
public void decryptFSTEntriesTo(boolean fullPath,String regEx, String outputFolder) throws IOException {
|
||||
List<FSTEntry> files = getNUSTitle().getAllFSTEntriesFlat();
|
||||
Pattern p = Pattern.compile(regEx);
|
||||
|
||||
List<FSTEntry> result = new ArrayList<>();
|
||||
|
||||
for(FSTEntry f :files){
|
||||
String match = f.getFullPath().replaceAll("\\\\", "/");
|
||||
Matcher m = p.matcher(match);
|
||||
if(m.matches()){
|
||||
result.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
decryptFSTEntryListTo(fullPath,result, outputFolder);
|
||||
}
|
||||
public void decryptFSTEntryListTo(List<FSTEntry> list,String outputFolder) throws IOException {
|
||||
decryptFSTEntryListTo(true,list,outputFolder);
|
||||
}
|
||||
public void decryptFSTEntryListTo(boolean fullPath,List<FSTEntry> list,String outputFolder) throws IOException {
|
||||
for(FSTEntry entry: list){
|
||||
decryptFSTEntryTo(fullPath,entry, outputFolder, getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
}
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
//Save decrypted contents
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
public void decryptPlainContentByID(int ID,String outputFolder) throws IOException {
|
||||
decryptPlainContent(getTMDFromNUSTitle().getContentByID(ID),outputFolder);
|
||||
}
|
||||
|
||||
public void decryptPlainContentByIndex(int index,String outputFolder) throws IOException {
|
||||
decryptPlainContent(getTMDFromNUSTitle().getContentByIndex(index),outputFolder);
|
||||
}
|
||||
|
||||
public void decryptPlainContent(Content c,String outputFolder) throws IOException {
|
||||
decryptPlainContents(new ArrayList<Content>(Arrays.asList(c)), outputFolder);
|
||||
}
|
||||
|
||||
public void decryptPlainContents(List<Content> list,String outputFolder) throws IOException {
|
||||
for(Content c : list){
|
||||
decryptContentTo(c,outputFolder,getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
}
|
||||
public void decryptAllPlainContents(String outputFolder) throws IOException {
|
||||
decryptPlainContents(new ArrayList<Content>(getTMDFromNUSTitle().getAllContents().values()),outputFolder);
|
||||
}
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
//Other
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
private TMD getTMDFromNUSTitle(){
|
||||
return getNUSTitle().getTMD();
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package de.mas.jnus.lib;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import de.mas.jnus.lib.entities.TMD;
|
||||
import de.mas.jnus.lib.entities.Ticket;
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.entities.content.ContentFSTInfo;
|
||||
import de.mas.jnus.lib.entities.fst.FST;
|
||||
import de.mas.jnus.lib.entities.fst.FSTEntry;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProvider;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class NUSTitle {
|
||||
@Getter @Setter private String inputPath = "";
|
||||
|
||||
@Getter @Setter private FST FST;
|
||||
@Getter @Setter private TMD TMD;
|
||||
@Getter @Setter private Ticket ticket;
|
||||
|
||||
@Getter @Setter private boolean skipExistingFiles = true;
|
||||
@Getter @Setter private NUSDataProvider dataProvider = null;
|
||||
|
||||
public List<FSTEntry> getAllFSTEntriesFlatByContentID(short ID){
|
||||
return getFSTEntriesFlatByContent(getTMD().getContentByID((int) ID));
|
||||
}
|
||||
|
||||
public List<FSTEntry> getFSTEntriesFlatByContentIndex(int index){
|
||||
return getFSTEntriesFlatByContent(getTMD().getContentByIndex(index));
|
||||
}
|
||||
|
||||
public List<FSTEntry> getFSTEntriesFlatByContent(Content content){
|
||||
return getFSTEntriesFlatByContents(new ArrayList<Content>(Arrays.asList(content)));
|
||||
}
|
||||
|
||||
public List<FSTEntry> getFSTEntriesFlatByContents(List<Content> list){
|
||||
List<FSTEntry> entries = new ArrayList<>();
|
||||
for(Content c : list){
|
||||
for(FSTEntry f : c.getEntries()){
|
||||
entries.add(f);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
public List<FSTEntry> getAllFSTEntriesFlat(){
|
||||
return getFSTEntriesFlatByContents(new ArrayList<Content>(getTMD().getAllContents().values()));
|
||||
}
|
||||
|
||||
public FSTEntry getFSTEntryByFullPath(String givenFullPath) {
|
||||
givenFullPath = givenFullPath.replaceAll("/", "\\\\");
|
||||
if(!givenFullPath.startsWith("\\")) givenFullPath = "\\" +givenFullPath;
|
||||
for(FSTEntry f :getAllFSTEntriesFlat()){
|
||||
if(f.getFullPath().equals(givenFullPath)){
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void printFiles() {
|
||||
getFST().getRoot().printRecursive(0);
|
||||
}
|
||||
|
||||
public void printContentFSTInfos(){
|
||||
for(Entry<Integer,ContentFSTInfo> e : getFST().getContentFSTInfos().entrySet()){
|
||||
System.out.println(String.format("%08X", e.getKey()) + ": " +e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public void printContentInfos(){
|
||||
for(Entry<Integer, Content> e : getTMD().getAllContents().entrySet()){
|
||||
|
||||
System.out.println(String.format("%08X", e.getKey()) + ": " +e.getValue());
|
||||
System.out.println(e.getValue().getContentFSTInfo());
|
||||
for(FSTEntry entry : e.getValue().getEntries()){
|
||||
System.out.println(entry.getFullPath() + String.format(" size: %016X", entry.getFileSize())
|
||||
+ String.format(" offset: %016X", entry.getFileOffset())
|
||||
+ String.format(" flags: %04X", entry.getFlags()));
|
||||
}
|
||||
System.out.println("-");
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanup() throws IOException{
|
||||
if(getDataProvider() != null){
|
||||
getDataProvider().cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public void printDetailedData() {
|
||||
printFiles();
|
||||
printContentFSTInfos();
|
||||
printContentInfos();
|
||||
|
||||
System.out.println();
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package de.mas.jnus.lib;
|
||||
|
||||
import de.mas.jnus.lib.entities.Ticket;
|
||||
import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
|
||||
import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class NUSTitleConfig {
|
||||
private String inputPath = "";
|
||||
private WUDInfo WUDInfo = null;
|
||||
private Ticket ticket = null;
|
||||
|
||||
private int version = Settings.LATEST_TMD_VERSION;
|
||||
private long titleID = 0x0L;
|
||||
|
||||
private WoomyInfo woomyInfo;
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package de.mas.jnus.lib;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.jnus.lib.entities.TMD;
|
||||
import de.mas.jnus.lib.entities.Ticket;
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.entities.fst.FST;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProvider;
|
||||
import de.mas.jnus.lib.utils.StreamUtils;
|
||||
import de.mas.jnus.lib.utils.cryptography.AESDecryption;
|
||||
|
||||
abstract class NUSTitleLoader {
|
||||
protected NUSTitleLoader(){
|
||||
|
||||
}
|
||||
|
||||
public NUSTitle loadNusTitle(NUSTitleConfig config) throws Exception{
|
||||
NUSTitle result = new NUSTitle();
|
||||
|
||||
NUSDataProvider dataProvider = getDataProvider(config);
|
||||
result.setDataProvider(dataProvider);
|
||||
dataProvider.setNUSTitle(result);
|
||||
|
||||
|
||||
TMD tmd = TMD.parseTMD(dataProvider.getRawTMD());
|
||||
result.setTMD(tmd);
|
||||
|
||||
if(tmd == null){
|
||||
System.out.println("TMD not found.");
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
Ticket ticket = config.getTicket();
|
||||
if(ticket == null){
|
||||
ticket = Ticket.parseTicket(dataProvider.getRawTicket());
|
||||
}
|
||||
result.setTicket(ticket);
|
||||
System.out.println(ticket);
|
||||
|
||||
Content fstContent = tmd.getContentByIndex(0);
|
||||
|
||||
|
||||
InputStream fstContentEncryptedStream = dataProvider.getInputStreamFromContent(fstContent, 0);
|
||||
if(fstContentEncryptedStream == null){
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] fstBytes = StreamUtils.getBytesFromStream(fstContentEncryptedStream, (int) fstContent.getEncryptedFileSize());
|
||||
|
||||
if(fstContent.isEncrypted()){
|
||||
AESDecryption aesDecryption = new AESDecryption(ticket.getDecryptedKey(), new byte[0x10]);
|
||||
fstBytes = aesDecryption.decrypt(fstBytes);
|
||||
}
|
||||
|
||||
Map<Integer,Content> contents = tmd.getAllContents();
|
||||
|
||||
FST fst = FST.parseFST(fstBytes, contents);
|
||||
result.setFST(fst);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected abstract NUSDataProvider getDataProvider(NUSTitleConfig config);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package de.mas.jnus.lib;
|
||||
|
||||
import de.mas.jnus.lib.entities.Ticket;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProviderLocal;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProvider;
|
||||
|
||||
public class NUSTitleLoaderLocal extends NUSTitleLoader {
|
||||
|
||||
private NUSTitleLoaderLocal(){
|
||||
super();
|
||||
}
|
||||
public static NUSTitle loadNUSTitle(String inputPath) throws Exception{
|
||||
return loadNUSTitle(inputPath, null);
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(String inputPath, Ticket ticket) throws Exception{
|
||||
NUSTitleLoader loader = new NUSTitleLoaderLocal();
|
||||
NUSTitleConfig config = new NUSTitleConfig();
|
||||
|
||||
if(ticket != null){
|
||||
config.setTicket(ticket);
|
||||
}
|
||||
config.setInputPath(inputPath);
|
||||
|
||||
return loader.loadNusTitle(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
|
||||
NUSDataProviderLocal result = new NUSDataProviderLocal();
|
||||
result.setLocalPath(config.getInputPath());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package de.mas.jnus.lib;
|
||||
|
||||
import de.mas.jnus.lib.entities.Ticket;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProviderRemote;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProvider;
|
||||
|
||||
public class NUSTitleLoaderRemote extends NUSTitleLoader{
|
||||
|
||||
private NUSTitleLoaderRemote(){
|
||||
super();
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(long titleID) throws Exception{
|
||||
return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION ,null);
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(long titleID, int version) throws Exception{
|
||||
return loadNUSTitle(titleID, version,null);
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(long titleID, Ticket ticket) throws Exception{
|
||||
return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION, ticket);
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(long titleID,int version, Ticket ticket) throws Exception{
|
||||
NUSTitleLoader loader = new NUSTitleLoaderRemote();
|
||||
NUSTitleConfig config = new NUSTitleConfig();
|
||||
|
||||
config.setVersion(version);
|
||||
config.setTitleID(titleID);
|
||||
|
||||
return loader.loadNusTitle(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
|
||||
NUSDataProviderRemote result = new NUSDataProviderRemote();
|
||||
result.setVersion(config.getVersion());
|
||||
result.setTitleID(config.getTitleID());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package de.mas.jnus.lib;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import de.mas.jnus.lib.implementations.NUSDataProviderWUD;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProvider;
|
||||
import de.mas.jnus.lib.implementations.wud.WUDImage;
|
||||
import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
|
||||
import de.mas.jnus.lib.implementations.wud.parser.WUDInfoParser;
|
||||
|
||||
public class NUSTitleLoaderWUD extends NUSTitleLoader {
|
||||
|
||||
private NUSTitleLoaderWUD(){
|
||||
super();
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(String WUDPath) throws Exception{
|
||||
return loadNUSTitle(WUDPath, null);
|
||||
}
|
||||
public static NUSTitle loadNUSTitle(String WUDPath, byte[] titleKey) throws Exception{
|
||||
NUSTitleLoader loader = new NUSTitleLoaderWUD();
|
||||
NUSTitleConfig config = new NUSTitleConfig();
|
||||
|
||||
File wudFile = new File(WUDPath);
|
||||
if(!wudFile.exists()){
|
||||
System.out.println(WUDPath + " does not exist.");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
WUDImage image = new WUDImage(wudFile);
|
||||
if(titleKey == null){
|
||||
File keyFile = new File(new File(wudFile.getAbsolutePath()).getParentFile().getPath() + File.separator + Settings.WUD_KEY_FILENAME);
|
||||
if(!keyFile.exists()){
|
||||
System.out.println(keyFile.getAbsolutePath() + " does not exist and no title key was provided.");
|
||||
return null;
|
||||
}
|
||||
titleKey = Files.readAllBytes(keyFile.toPath());
|
||||
}
|
||||
WUDInfo wudInfo = WUDInfoParser.createAndLoad(image.getWUDDiscReader(), titleKey);
|
||||
if(wudInfo == null){
|
||||
return null;
|
||||
}
|
||||
|
||||
config.setWUDInfo(wudInfo);
|
||||
|
||||
return loader.loadNusTitle(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
|
||||
NUSDataProviderWUD result = new NUSDataProviderWUD();
|
||||
result.setWUDInfo(config.getWUDInfo());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package de.mas.jnus.lib;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import de.mas.jnus.lib.implementations.NUSDataProviderWoomy;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProvider;
|
||||
import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
|
||||
import de.mas.jnus.lib.implementations.woomy.WoomyParser;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class NUSTitleLoaderWoomy extends NUSTitleLoader {
|
||||
|
||||
public static NUSTitle loadNUSTitle(String inputFile) throws Exception{
|
||||
NUSTitleLoaderWoomy loader = new NUSTitleLoaderWoomy();
|
||||
NUSTitleConfig config = new NUSTitleConfig();
|
||||
|
||||
WoomyInfo woomyInfo = WoomyParser.createWoomyInfo(new File(inputFile));
|
||||
if(woomyInfo == null){
|
||||
log.info("Created woomy is null.");
|
||||
return null;
|
||||
}
|
||||
config.setWoomyInfo(woomyInfo);
|
||||
return loader.loadNusTitle(config);
|
||||
}
|
||||
@Override
|
||||
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
|
||||
NUSDataProviderWoomy dataProvider = new NUSDataProviderWoomy();
|
||||
dataProvider.setWoomyInfo(config.getWoomyInfo());
|
||||
return dataProvider;
|
||||
}
|
||||
|
||||
}
|
@ -1,184 +0,0 @@
|
||||
package de.mas.jnus.lib;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import de.mas.jnus.lib.implementations.wud.WUDImage;
|
||||
import de.mas.jnus.lib.implementations.wud.WUDImageCompressedInfo;
|
||||
import de.mas.jnus.lib.utils.ByteArrayBuffer;
|
||||
import de.mas.jnus.lib.utils.ByteArrayWrapper;
|
||||
import de.mas.jnus.lib.utils.HashUtil;
|
||||
import de.mas.jnus.lib.utils.StreamUtils;
|
||||
import de.mas.jnus.lib.utils.Utils;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class WUDService {
|
||||
public static File compressWUDToWUX(WUDImage image,String outputFolder) throws IOException{
|
||||
return compressWUDToWUX(image, outputFolder, "game.wux",false);
|
||||
}
|
||||
|
||||
public static File compressWUDToWUX(WUDImage image,String outputFolder,boolean overwrite) throws IOException{
|
||||
return compressWUDToWUX(image, outputFolder, "game.wux",overwrite);
|
||||
}
|
||||
|
||||
public static File compressWUDToWUX(WUDImage image,String outputFolder,String filename,boolean overwrite) throws IOException{
|
||||
if(image.isCompressed()){
|
||||
log.info("Given image is already compressed");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(image.getWUDFileSize() != WUDImage.WUD_FILESIZE)
|
||||
{
|
||||
log.info("Given WUD has not the expected filesize");
|
||||
return null;
|
||||
}
|
||||
|
||||
Utils.createDir(outputFolder);
|
||||
|
||||
String filePath;
|
||||
if(outputFolder == null) outputFolder = "";
|
||||
|
||||
if(!outputFolder.isEmpty()){
|
||||
filePath = outputFolder+ File.separator + filename;
|
||||
}else{
|
||||
filePath = filename;
|
||||
}
|
||||
File outputFile = new File(filePath);
|
||||
|
||||
if(outputFile.exists() && !overwrite){
|
||||
log.info("Couldn't compress wud, target file already exists (" + outputFile.getAbsolutePath() + ")");
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info("Writing compressed file to: " + outputFile.getAbsolutePath() );
|
||||
RandomAccessFile fileOutput = new RandomAccessFile(outputFile, "rw");
|
||||
|
||||
WUDImageCompressedInfo info = WUDImageCompressedInfo.getDefaultCompressedInfo();
|
||||
|
||||
byte[] header = info.getHeaderAsBytes();
|
||||
log.info("Writing header");
|
||||
fileOutput.write(header);
|
||||
|
||||
int sectorTableEntryCount = (int) ((image.getWUDFileSize()+ WUDImageCompressedInfo.SECTOR_SIZE-1) / (long)WUDImageCompressedInfo.SECTOR_SIZE);
|
||||
|
||||
long sectorTableStart = fileOutput.getFilePointer();
|
||||
long sectorTableEnd = Utils.align(sectorTableEntryCount *0x04,WUDImageCompressedInfo.SECTOR_SIZE);
|
||||
byte[] sectorTablePlaceHolder = new byte[(int) (sectorTableEnd-sectorTableStart)];
|
||||
|
||||
fileOutput.write(sectorTablePlaceHolder);
|
||||
|
||||
Map<ByteArrayWrapper,Integer> sectorHashes = new HashMap<>();
|
||||
Map<Integer,Integer> sectorMapping = new TreeMap<>();
|
||||
|
||||
InputStream in = image.getWUDDiscReader().readEncryptedToInputStream(0, image.getWUDFileSize());
|
||||
|
||||
int bufferSize = WUDImageCompressedInfo.SECTOR_SIZE;
|
||||
byte[] blockBuffer = new byte[bufferSize];
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(bufferSize);
|
||||
|
||||
|
||||
long written = 0;
|
||||
int curSector = 0;
|
||||
int realSector = 0;
|
||||
|
||||
log.info("Writing sectors");
|
||||
Integer oldOffset = null;
|
||||
do{
|
||||
int read = StreamUtils.getChunkFromStream(in, blockBuffer, overflow, bufferSize);
|
||||
ByteArrayWrapper hash = new ByteArrayWrapper(HashUtil.hashSHA1(blockBuffer));
|
||||
|
||||
if((oldOffset = sectorHashes.get(hash)) != null){
|
||||
sectorMapping.put(curSector, oldOffset);
|
||||
oldOffset = null;
|
||||
}else{ //its a new sector
|
||||
sectorMapping.put(curSector, realSector);
|
||||
sectorHashes.put(hash, realSector);
|
||||
fileOutput.write(blockBuffer);
|
||||
realSector++;
|
||||
}
|
||||
|
||||
written += read;
|
||||
curSector++;
|
||||
if(curSector % 10 == 0){
|
||||
double readMB = written / 1024.0 / 1024.0;
|
||||
double writtenMB = ((long)realSector * (long)bufferSize) / 1024.0 / 1024.0;
|
||||
double percent = ((double)written / image.getWUDFileSize())*100;
|
||||
double ratio = 1 / (writtenMB / readMB);
|
||||
System.out.print(String.format(Locale.ROOT,"\rCompressing into .wux | Progress %.2f%% | Ratio: 1:%.2f | Read: %.2fMB | Written: %.2fMB\t",percent,ratio,readMB,writtenMB));
|
||||
}
|
||||
}while(written < image.getWUDFileSize());
|
||||
System.out.println();
|
||||
System.out.println("Sectors compressed.");
|
||||
log.info("Writing sector table");
|
||||
fileOutput.seek(sectorTableStart);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(sectorTablePlaceHolder.length);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for(Entry<Integer,Integer> e: sectorMapping.entrySet()){
|
||||
buffer.putInt(e.getValue());
|
||||
}
|
||||
|
||||
fileOutput.write(buffer.array());
|
||||
fileOutput.close();
|
||||
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
public static boolean compareWUDImage(WUDImage firstImage,WUDImage secondImage) throws IOException{
|
||||
if(firstImage.getWUDFileSize() != secondImage.getWUDFileSize()){
|
||||
log.info("Filesize is different");
|
||||
return false;
|
||||
}
|
||||
InputStream in1 = firstImage.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE);
|
||||
InputStream in2 = secondImage.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE);
|
||||
|
||||
boolean result = true;
|
||||
int bufferSize = 1024*1024+19;
|
||||
long totalread = 0;
|
||||
byte[] blockBuffer1 = new byte[bufferSize];
|
||||
byte[] blockBuffer2 = new byte[bufferSize];
|
||||
ByteArrayBuffer overflow1 = new ByteArrayBuffer(bufferSize);
|
||||
ByteArrayBuffer overflow2 = new ByteArrayBuffer(bufferSize);
|
||||
long curSector = 0;
|
||||
do{
|
||||
int read1 = StreamUtils.getChunkFromStream(in1, blockBuffer1, overflow1, bufferSize);
|
||||
int read2 = StreamUtils.getChunkFromStream(in2, blockBuffer2, overflow2, bufferSize);
|
||||
if(read1 != read2){
|
||||
log.info("Verification error");
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if(!Arrays.equals(blockBuffer1,blockBuffer2)){
|
||||
log.info("Verification error");
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
|
||||
totalread += read1;
|
||||
|
||||
curSector++;
|
||||
if(curSector % 1 == 0){
|
||||
double readMB = totalread / 1024.0 / 1024.0;
|
||||
double percent = ((double)totalread / WUDImage.WUD_FILESIZE)*100;
|
||||
System.out.print(String.format("\rVerification: %.2fMB done (%.2f%%)", readMB,percent));
|
||||
}
|
||||
}while(totalread < WUDImage.WUD_FILESIZE);
|
||||
System.out.println();
|
||||
System.out.print("Verfication done!");
|
||||
in1.close();
|
||||
in2.close();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
package de.mas.jnus.lib.entities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.entities.content.ContentInfo;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class TMD {
|
||||
@Getter @Setter private int signatureType; // 0x000
|
||||
@Getter @Setter private byte[] signature = new byte[0x100]; // 0x004
|
||||
@Getter @Setter private byte[] issuer = new byte[0x40]; // 0x140
|
||||
@Getter @Setter private byte version; // 0x180
|
||||
@Getter @Setter private byte CACRLVersion; // 0x181
|
||||
@Getter @Setter private byte signerCRLVersion; // 0x182
|
||||
@Getter @Setter private long systemVersion; // 0x184
|
||||
@Getter @Setter private long titleID; // 0x18C
|
||||
@Getter @Setter private int titleType; // 0x194
|
||||
@Getter @Setter private short groupID; // 0x198
|
||||
@Getter @Setter private byte[] reserved = new byte[62]; // 0x19A
|
||||
@Getter @Setter private int accessRights; // 0x1D8
|
||||
@Getter @Setter private short titleVersion; // 0x1DC
|
||||
@Getter @Setter private short contentCount; // 0x1DE
|
||||
@Getter @Setter private short bootIndex; // 0x1E0
|
||||
@Getter @Setter private byte[] SHA2 = new byte[0x20]; // 0x1E4
|
||||
@Getter @Setter private ContentInfo[] contentInfos = new ContentInfo[0x40];
|
||||
Map<Integer,Content> contentToIndex = new HashMap<>();
|
||||
Map<Integer,Content> contentToID = new HashMap<>();
|
||||
|
||||
@Getter @Setter private byte[] rawTMD = new byte[0];
|
||||
private TMD(){
|
||||
|
||||
}
|
||||
|
||||
public static TMD parseTMD(File tmd) throws IOException {
|
||||
if(tmd == null || !tmd.exists()){
|
||||
System.out.println("TMD input file null or doesn't exist.");
|
||||
return null;
|
||||
}
|
||||
return parseTMD(Files.readAllBytes(tmd.toPath()));
|
||||
}
|
||||
|
||||
public static TMD parseTMD(byte[] input) {
|
||||
|
||||
TMD result = new TMD();
|
||||
result.setRawTMD(Arrays.copyOf(input,input.length));
|
||||
byte[] signature = new byte[0x100];
|
||||
byte[] issuer = new byte[0x40];
|
||||
byte[] reserved = new byte[62];
|
||||
byte[] SHA2 = new byte[0x20];
|
||||
|
||||
ContentInfo[] contentInfos = result.getContentInfos();
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(input.length);
|
||||
buffer.put(input);
|
||||
|
||||
//Get Signature
|
||||
buffer.position(0x00);
|
||||
int signatureType = buffer.getInt();
|
||||
buffer.get(signature, 0, 0x100);
|
||||
|
||||
//Get Issuer
|
||||
buffer.position(0x140);
|
||||
buffer.get(issuer, 0, 0x40);
|
||||
|
||||
//Get CACRLVersion and signerCRLVersion
|
||||
buffer.position(0x180);
|
||||
byte version = buffer.get();
|
||||
byte CACRLVersion = buffer.get();
|
||||
byte signerCRLVersion = buffer.get();
|
||||
|
||||
//Get title information
|
||||
buffer.position(0x184);
|
||||
long systemVersion = buffer.getLong();
|
||||
long titleID = buffer.getLong();
|
||||
int titleType = buffer.getInt();
|
||||
short groupID = buffer.getShort();
|
||||
buffer.position(0x19A);
|
||||
|
||||
//Get title information
|
||||
buffer.get(reserved, 0, 62);
|
||||
|
||||
//Get accessRights,titleVersion,contentCount,bootIndex
|
||||
buffer.position(0x1D8);
|
||||
int accessRights = buffer.getInt();
|
||||
short titleVersion = buffer.getShort();
|
||||
short contentCount = buffer.getShort();
|
||||
short bootIndex = buffer.getShort();
|
||||
|
||||
//Get hash
|
||||
buffer.position(0x1E4);
|
||||
buffer.get(SHA2, 0, 0x20);
|
||||
|
||||
//Get contentInfos
|
||||
buffer.position(0x204);
|
||||
for(int i =0;i<64;i++){
|
||||
byte[] contentInfo = new byte[0x24];
|
||||
buffer.get(contentInfo, 0, 0x24);
|
||||
contentInfos[i] = ContentInfo.parseContentInfo(contentInfo);
|
||||
}
|
||||
|
||||
//Get Contents
|
||||
for(int i =0;i<contentCount;i++){
|
||||
buffer.position(0xB04+(0x30*i));
|
||||
byte[] content = new byte[0x30];
|
||||
buffer.get(content, 0, 0x30);
|
||||
Content c = Content.parseContent(content);
|
||||
result.setContentToIndex(c.getIndex(),c);
|
||||
result.setContentToID(c.getID(), c);
|
||||
}
|
||||
|
||||
result.setSignatureType(signatureType);
|
||||
result.setSignature(signature);
|
||||
result.setVersion(version);
|
||||
result.setCACRLVersion(CACRLVersion);
|
||||
result.setSignerCRLVersion(signerCRLVersion);
|
||||
result.setSystemVersion(systemVersion);
|
||||
result.setTitleID(titleID);
|
||||
result.setTitleType(titleType);
|
||||
result.setGroupID(groupID);
|
||||
result.setAccessRights(accessRights);
|
||||
result.setTitleVersion(titleVersion);
|
||||
result.setContentCount(contentCount);
|
||||
result.setBootIndex(bootIndex);
|
||||
result.setSHA2(SHA2);
|
||||
result.setContentInfos(contentInfos);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Content getContentByIndex(int index) {
|
||||
return contentToIndex.get(index);
|
||||
}
|
||||
|
||||
private void setContentToIndex(int index,Content content) {
|
||||
contentToIndex.put(index, content);
|
||||
}
|
||||
|
||||
public Content getContentByID(int id) {
|
||||
return contentToID.get(id);
|
||||
}
|
||||
|
||||
private void setContentToID(int id,Content content) {
|
||||
contentToID.put(id, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all contents mapped by index
|
||||
* @return Map of Content, index/content pairs
|
||||
*/
|
||||
public Map<Integer, Content> getAllContents() {
|
||||
return contentToIndex;
|
||||
}
|
||||
|
||||
public void printContents() {
|
||||
for(Content c: contentToIndex.values()){
|
||||
System.out.println(c);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
package de.mas.jnus.lib.entities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import de.mas.jnus.lib.utils.Utils;
|
||||
import de.mas.jnus.lib.utils.cryptography.AESDecryption;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class Ticket {
|
||||
@Getter @Setter private byte[] encryptedKey = new byte[0x10];
|
||||
@Getter @Setter private byte[] decryptedKey = new byte[0x10];
|
||||
|
||||
@Getter @Setter private byte[] IV = new byte[0x10];
|
||||
|
||||
@Getter @Setter private byte[] cert0 = new byte[0x300];
|
||||
@Getter @Setter private byte[] cert1 = new byte[0x400];
|
||||
|
||||
@Getter @Setter private byte[] rawTicket = new byte[0];
|
||||
|
||||
private Ticket(){
|
||||
|
||||
}
|
||||
|
||||
public static Ticket parseTicket(File ticket) throws IOException {
|
||||
if(ticket == null || !ticket.exists()){
|
||||
log.warning("Ticket input file null or doesn't exist.");
|
||||
return null;
|
||||
}
|
||||
return parseTicket(Files.readAllBytes(ticket.toPath()));
|
||||
}
|
||||
|
||||
public static Ticket parseTicket(byte[] ticket) throws IOException {
|
||||
if(ticket == null){
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(ticket.length);
|
||||
buffer.put(ticket);
|
||||
|
||||
//read key
|
||||
byte[] encryptedKey = new byte[0x10];
|
||||
buffer.position(0x1BF);
|
||||
buffer.get(encryptedKey,0x00,0x10);
|
||||
|
||||
//read titleID
|
||||
buffer.position(0x1DC);
|
||||
long titleID = buffer.getLong();
|
||||
|
||||
Ticket result = createTicket(encryptedKey,titleID);
|
||||
result.setRawTicket(Arrays.copyOf(ticket, ticket.length));
|
||||
|
||||
//read certs.
|
||||
byte[] cert0 = new byte[0x300];
|
||||
byte[] cert1 = new byte[0x400];
|
||||
|
||||
if(ticket.length >= 0x650){
|
||||
buffer.position(0x350);
|
||||
buffer.get(cert0,0x00,0x300);
|
||||
}
|
||||
if(ticket.length >= 0xA50){
|
||||
buffer.position(0x650);
|
||||
buffer.get(cert1,0x00,0x400);
|
||||
}
|
||||
|
||||
result.setCert0(cert0);
|
||||
result.setCert1(cert1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Ticket createTicket(byte[] encryptedKey, long titleID) {
|
||||
Ticket result = new Ticket();
|
||||
result.encryptedKey = encryptedKey;
|
||||
|
||||
byte[] IV = ByteBuffer.allocate(0x10).putLong(titleID).array();
|
||||
result.decryptedKey = calculateDecryptedKey(result.encryptedKey,IV);
|
||||
result.setIV(IV);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] calculateDecryptedKey(byte[] encryptedKey, byte[] IV) {
|
||||
AESDecryption decryption = new AESDecryption(Settings.commonKey, IV){};
|
||||
return decryption.decrypt(encryptedKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + Arrays.hashCode(encryptedKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
Ticket other = (Ticket) obj;
|
||||
if (!Arrays.equals(encryptedKey, other.encryptedKey))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Ticket [encryptedKey=" + Utils.ByteArrayToString(encryptedKey) + ", decryptedKey="
|
||||
+ Utils.ByteArrayToString(decryptedKey) + "]";
|
||||
}
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
package de.mas.jnus.lib.entities.content;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import de.mas.jnus.lib.entities.fst.FSTEntry;
|
||||
import de.mas.jnus.lib.utils.Utils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Represents a Content
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class Content{
|
||||
public static final short CONTENT_HASHED = 0x0002;
|
||||
public static final short CONTENT_ENCRYPTED = 0x0001;
|
||||
|
||||
@Getter @Setter private int ID = 0x00;
|
||||
@Getter @Setter private short index = 0x00;
|
||||
@Getter @Setter private short type = 0x0000;
|
||||
|
||||
@Getter @Setter private long encryptedFileSize = 0;
|
||||
@Getter @Setter private byte[] SHA2Hash = new byte[0x14];
|
||||
|
||||
@Getter private List<FSTEntry> entries = new ArrayList<>();
|
||||
|
||||
@Getter @Setter private ContentFSTInfo contentFSTInfo = null;
|
||||
|
||||
/**
|
||||
* Creates a new Content object given be the raw byte data
|
||||
* @param input 0x30 byte of data from the TMD (starting at 0xB04)
|
||||
* @return content object
|
||||
*/
|
||||
public static Content parseContent(byte[] input) {
|
||||
if(input == null || input.length != 0x30){
|
||||
System.out.println("Error: invalid Content byte[] input");
|
||||
return null;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate(input.length);
|
||||
buffer.put(input);
|
||||
buffer.position(0);
|
||||
int ID = buffer.getInt(0x00);
|
||||
short index = buffer.getShort(0x04);
|
||||
short type = buffer.getShort(0x06);
|
||||
long encryptedFileSize = buffer.getLong(0x08);
|
||||
buffer.position(0x10);
|
||||
byte[] hash = new byte[0x14];
|
||||
buffer.get(hash, 0x00, 0x14);
|
||||
return new Content(ID, index,type,encryptedFileSize, hash);
|
||||
}
|
||||
|
||||
public Content(int ID, short index, short type, long encryptedFileSize, byte[] hash) {
|
||||
setID(ID);
|
||||
setIndex(index);
|
||||
setType(type);
|
||||
setEncryptedFileSize(encryptedFileSize);
|
||||
setSHA2Hash(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the content is hashed
|
||||
* @return true if hashed
|
||||
*/
|
||||
public boolean isHashed() {
|
||||
return (type & CONTENT_HASHED) == CONTENT_HASHED;
|
||||
}
|
||||
/**
|
||||
* Returns if the content is encrypted
|
||||
* @return true if encrypted
|
||||
*/
|
||||
public boolean isEncrypted() {
|
||||
return (type & CONTENT_ENCRYPTED) == CONTENT_ENCRYPTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filename of the encrypted content.
|
||||
* It's the ID as hex with an extension
|
||||
* For example: 00000000.app
|
||||
* @return filename of the encrypted content
|
||||
*/
|
||||
public String getFilename(){
|
||||
return String.format("%08X%s", getID(),Settings.ENCRYPTED_CONTENT_EXTENTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a content to the internal entry list.
|
||||
* @param entry that will be added to the content list
|
||||
*/
|
||||
public void addEntry(FSTEntry entry) {
|
||||
getEntries().add(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the decrypted content.
|
||||
* @return size of the decrypted content
|
||||
*/
|
||||
public long getDecryptedFileSize() {
|
||||
if(isHashed()){
|
||||
return getEncryptedFileSize()/0x10000*0xFC00;
|
||||
}else{
|
||||
return getEncryptedFileSize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filename of the decrypted content.
|
||||
* It's the ID as hex with an extension
|
||||
* For example: 00000000.dec
|
||||
* @return filename of the decrypted content
|
||||
*/
|
||||
public String getFilenameDecrypted() {
|
||||
return String.format("%08X%s", getID(),Settings.DECRYPTED_CONTENT_EXTENTION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ID;
|
||||
result = prime * result + Arrays.hashCode(SHA2Hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
Content other = (Content) obj;
|
||||
if (ID != other.ID)
|
||||
return false;
|
||||
if (!Arrays.equals(SHA2Hash, other.SHA2Hash))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Content [ID=" + Integer.toHexString(ID) + ", index=" + Integer.toHexString(index) + ", type=" + String.format("%04X", type) + ", encryptedFileSize=" + encryptedFileSize + ", SHA2Hash=" + Utils.ByteArrayToString(SHA2Hash) + "]";
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package de.mas.jnus.lib.entities.content;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@EqualsAndHashCode
|
||||
/**
|
||||
* Representation on an Object of the first section
|
||||
* of an FST.
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class ContentFSTInfo {
|
||||
@Getter @Setter private long offsetSector;
|
||||
@Getter @Setter private long sizeSector;
|
||||
@Getter @Setter private long ownerTitleID;
|
||||
@Getter @Setter private int groupID;
|
||||
@Getter @Setter private byte unkown;
|
||||
|
||||
private static int SECTOR_SIZE = 0x8000;
|
||||
|
||||
private ContentFSTInfo(){
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new ContentFSTInfo object given be the raw byte data
|
||||
* @param input 0x20 byte of data from the FST (starting at 0x20)
|
||||
* @return ContentFSTInfo object
|
||||
*/
|
||||
public static ContentFSTInfo parseContentFST(byte[] input) {
|
||||
if(input == null ||input.length != 0x20){
|
||||
System.out.println("Error: invalid ContentFSTInfo byte[] input");
|
||||
return null;
|
||||
}
|
||||
ContentFSTInfo cFSTInfo = new ContentFSTInfo();
|
||||
ByteBuffer buffer = ByteBuffer.allocate(input.length);
|
||||
buffer.put(input);
|
||||
|
||||
buffer.position(0);
|
||||
int offset = buffer.getInt();
|
||||
int size = buffer.getInt();
|
||||
long ownerTitleID = buffer.getLong();
|
||||
int groupID = buffer.getInt();
|
||||
byte unkown = buffer.get();
|
||||
|
||||
cFSTInfo.setOffsetSector(offset);
|
||||
cFSTInfo.setSizeSector(size);
|
||||
cFSTInfo.setOwnerTitleID(ownerTitleID);
|
||||
cFSTInfo.setGroupID(groupID);
|
||||
cFSTInfo.setUnkown(unkown);
|
||||
|
||||
return cFSTInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of of the Content in the partition
|
||||
* @return offset of the content in the partition in bytes
|
||||
*/
|
||||
public long getOffset() {
|
||||
long result = (getOffsetSector() * SECTOR_SIZE) - SECTOR_SIZE;
|
||||
if(result < 0){
|
||||
return 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size in bytes, not in sectors
|
||||
* @return size in bytes
|
||||
*/
|
||||
public int getSize() {
|
||||
return (int) (getSizeSector() * SECTOR_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ContentFSTInfo [offset=" + String.format("%08X", offsetSector) + ", size=" + String.format("%08X", sizeSector) + ", ownerTitleID=" + String.format("%016X", ownerTitleID) + ", groupID="
|
||||
+ String.format("%08X", groupID) + ", unkown=" + unkown + "]";
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package de.mas.jnus.lib.entities.fst;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.entities.content.ContentFSTInfo;
|
||||
import de.mas.jnus.lib.utils.ByteUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
/**
|
||||
* Represents the FST
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class FST {
|
||||
@Getter @Setter private FSTEntry root = FSTEntry.getRootFSTEntry();
|
||||
|
||||
@Getter @Setter private int unknown;
|
||||
@Getter @Setter private int contentCount = 0;
|
||||
|
||||
@Getter @Setter private Map<Integer,ContentFSTInfo> contentFSTInfos = new HashMap<>();
|
||||
|
||||
private FST(){
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a FST by the given raw byte data
|
||||
* @param fstData raw decrypted FST data
|
||||
* @param contentsMappedByIndex map of index/content
|
||||
* @return
|
||||
*/
|
||||
public static FST parseFST(byte[] fstData,Map<Integer,Content> contentsMappedByIndex){
|
||||
if(!Arrays.equals(Arrays.copyOfRange(fstData, 0, 3), new byte[]{0x46,0x53,0x54})){
|
||||
throw new IllegalArgumentException("Not a FST. Maybe a wrong key?");
|
||||
}
|
||||
FST result = new FST();
|
||||
|
||||
int unknownValue = ByteUtils.getIntFromBytes(fstData, 0x04);
|
||||
int contentCount = ByteUtils.getIntFromBytes(fstData, 0x08);
|
||||
|
||||
int contentfst_offset = 0x20;
|
||||
int contentfst_size = 0x20*contentCount;
|
||||
|
||||
int fst_offset = contentfst_offset+contentfst_size;
|
||||
int fileCount = ByteUtils.getIntFromBytes(fstData, fst_offset + 0x08);
|
||||
int fst_size = fileCount*0x10;
|
||||
|
||||
int nameOff = fst_offset + fst_size;
|
||||
int nameSize = nameOff +1;
|
||||
|
||||
//Get list with null-terminated Strings. Ends with \0\0.
|
||||
for(int i = nameOff;i<fstData.length-1;i++){
|
||||
if(fstData[i] == 0 && fstData[i+1] == 0){
|
||||
nameSize = i - nameOff-1;
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer,ContentFSTInfo> contentFSTInfos = result.getContentFSTInfos();
|
||||
for(int i = 0;i<contentCount;i++){
|
||||
byte contentFST[] = Arrays.copyOfRange(fstData, contentfst_offset + (i*0x20), contentfst_offset + ((i+1)*0x20));
|
||||
contentFSTInfos.put(i,ContentFSTInfo.parseContentFST(contentFST));
|
||||
}
|
||||
|
||||
byte fstSection[] = Arrays.copyOfRange(fstData, fst_offset, fst_offset + fst_size);
|
||||
byte nameSection[] = Arrays.copyOfRange(fstData, nameOff, nameOff + nameSize);
|
||||
|
||||
FSTEntry root = result.getRoot();
|
||||
|
||||
FSTService.parseFST(root,fstSection,nameSection,contentsMappedByIndex,contentFSTInfos);
|
||||
|
||||
result.setContentCount(contentCount);
|
||||
result.setUnknown(unknownValue);
|
||||
result.setContentFSTInfos(contentFSTInfos);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
package de.mas.jnus.lib.entities.fst;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.entities.content.ContentFSTInfo;
|
||||
import de.mas.jnus.lib.utils.ByteUtils;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class FSTService {
|
||||
protected static void parseFST(FSTEntry rootEntry, byte[] fstSection, byte[] namesSection,Map<Integer,Content> contentsByIndex,Map<Integer,ContentFSTInfo> contentsFSTByIndex) {
|
||||
int totalEntries = ByteUtils.getIntFromBytes(fstSection, 0x08);
|
||||
|
||||
int level = 0;
|
||||
int[] LEntry = new int[16];
|
||||
int[] Entry = new int[16];
|
||||
|
||||
HashMap<Integer,FSTEntry> fstEntryToOffsetMap = new HashMap<>();
|
||||
|
||||
rootEntry.setDir(true);
|
||||
Entry[level] = 0;
|
||||
LEntry[level++] = 0;
|
||||
|
||||
fstEntryToOffsetMap.put(0,rootEntry);
|
||||
|
||||
for(int i = 1;i< totalEntries;i++){
|
||||
int entryOffset = i;
|
||||
if( level > 0){
|
||||
while( LEntry[level-1] == i ){level--;}
|
||||
}
|
||||
|
||||
byte[] curEntry = Arrays.copyOfRange(fstSection,i*0x10,(i+1)*0x10);
|
||||
|
||||
FSTEntry entry = new FSTEntry();
|
||||
|
||||
String path = getFullPath(level, fstSection, namesSection, Entry);
|
||||
String filename = getName(curEntry,namesSection);
|
||||
|
||||
long fileOffset = ByteUtils.getIntFromBytes(curEntry, 0x04);
|
||||
long fileSize = ByteUtils.getUnsingedIntFromBytes(curEntry, 0x08);
|
||||
|
||||
short flags = ByteUtils.getShortFromBytes(curEntry, 0x0C);
|
||||
short contentIndex = ByteUtils.getShortFromBytes(curEntry, 0x0E);
|
||||
|
||||
if((curEntry[0] & FSTEntry.FSTEntry_notInNUS) == FSTEntry.FSTEntry_notInNUS){
|
||||
entry.setNotInPackage(true);
|
||||
}
|
||||
|
||||
if((curEntry[0] & FSTEntry.FSTEntry_DIR) == FSTEntry.FSTEntry_DIR){
|
||||
entry.setDir(true);
|
||||
int parentOffset = (int) fileOffset;
|
||||
int nextOffset = (int) fileSize;
|
||||
|
||||
FSTEntry parent = fstEntryToOffsetMap.get(parentOffset);
|
||||
if(parent != null){
|
||||
log.fine("no parent found for a FSTEntry");
|
||||
parent.addChildren(entry);
|
||||
}
|
||||
|
||||
Entry[level] = i;
|
||||
LEntry[level++] = nextOffset ;
|
||||
|
||||
if( level > 15 ){
|
||||
log.warning("level > 15");
|
||||
break;
|
||||
}
|
||||
}else{
|
||||
entry.setFileOffset(fileOffset<<5);
|
||||
entry.setFileSize(fileSize);
|
||||
FSTEntry parent = fstEntryToOffsetMap.get(Entry[level-1]);
|
||||
if(parent != null){
|
||||
parent.addChildren(entry);
|
||||
}else{
|
||||
log.warning(entryOffset +"couldn't find parent @ " + Entry[level-1]);
|
||||
}
|
||||
}
|
||||
|
||||
entry.setFlags(flags);
|
||||
entry.setFilename(filename);
|
||||
entry.setPath(path);
|
||||
|
||||
if(contentsByIndex != null){
|
||||
Content content = contentsByIndex.get((int)contentIndex);
|
||||
if(content == null){
|
||||
log.warning("Content for FST Entry not found");
|
||||
}else{
|
||||
entry.setContent(content);
|
||||
|
||||
ContentFSTInfo contentFSTInfo = contentsFSTByIndex.get((int)contentIndex);
|
||||
if(contentFSTInfo == null){
|
||||
log.warning("ContentFSTInfo for FST Entry not found");
|
||||
}else{
|
||||
content.setContentFSTInfo(contentFSTInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry.setContentFSTID(contentIndex);
|
||||
|
||||
fstEntryToOffsetMap.put(entryOffset, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getNameOffset(byte[] curEntry) {
|
||||
//Its a 24bit number. We overwrite the first byte, then we can read it as an Integer.
|
||||
//But at first we make a copy.
|
||||
byte[] entryData = Arrays.copyOf(curEntry, curEntry.length);
|
||||
entryData[0] = 0;
|
||||
return ByteUtils.getIntFromBytes(entryData, 0);
|
||||
}
|
||||
|
||||
public static String getName(byte[] data, byte[] namesSection){
|
||||
int nameOffset = getNameOffset(data);
|
||||
|
||||
int j = 0;
|
||||
|
||||
while(namesSection[nameOffset + j] != 0){j++;}
|
||||
return(new String(Arrays.copyOfRange(namesSection,nameOffset, nameOffset + j)));
|
||||
}
|
||||
|
||||
|
||||
public static String getFullPath(int level,byte[] fstSection,byte[] namesSection, int[] Entry){
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int i=0; i < level; i++){
|
||||
int entryOffset = Entry[i]*0x10;
|
||||
byte[] entryData = Arrays.copyOfRange(fstSection, entryOffset,entryOffset + 10);
|
||||
String entryName = getName(entryData,namesSection);
|
||||
|
||||
sb.append(entryName).append(File.separator);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import de.mas.jnus.lib.entities.TMD;
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
|
||||
import de.mas.jnus.lib.implementations.wud.parser.WUDPartition;
|
||||
import de.mas.jnus.lib.implementations.wud.parser.WUDPartitionHeader;
|
||||
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
@Log
|
||||
public class NUSDataProviderWUD extends NUSDataProvider {
|
||||
@Getter @Setter private WUDInfo WUDInfo = null;
|
||||
|
||||
@Setter(AccessLevel.PRIVATE) private TMD TMD = null;
|
||||
|
||||
public NUSDataProviderWUD() {
|
||||
super();
|
||||
}
|
||||
|
||||
public long getOffsetInWUD(Content content) {
|
||||
if(content.getContentFSTInfo() == null){
|
||||
return getAbsoluteReadOffset();
|
||||
}else{
|
||||
return getAbsoluteReadOffset() + content.getContentFSTInfo().getOffset();
|
||||
}
|
||||
}
|
||||
|
||||
public long getAbsoluteReadOffset(){
|
||||
return (long)Settings.WIIU_DECRYPTED_AREA_OFFSET + getGamePartition().getPartitionOffset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStreamFromContent(Content content, long fileOffsetBlock) throws IOException {
|
||||
WUDDiscReader discReader = getDiscReader();
|
||||
long offset = getOffsetInWUD(content) + fileOffsetBlock;
|
||||
return discReader.readEncryptedToInputStream(offset, content.getEncryptedFileSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContentH3Hash(Content content) throws IOException {
|
||||
|
||||
if(getGamePartitionHeader() == null){
|
||||
log.warning("GamePartitionHeader is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!getGamePartitionHeader().isCalculatedHashes()){
|
||||
log.info("Calculating h3 hashes");
|
||||
getGamePartitionHeader().calculateHashes(getTMD().getAllContents());
|
||||
|
||||
}
|
||||
return getGamePartitionHeader().getH3Hash(content);
|
||||
|
||||
}
|
||||
|
||||
private TMD getTMD() {
|
||||
if(TMD == null){
|
||||
setTMD(de.mas.jnus.lib.entities.TMD.parseTMD(getRawTMD()));
|
||||
}
|
||||
return TMD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawTMD() {
|
||||
return getWUDInfo().getGamePartition().getRawTMD();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawTicket() {
|
||||
return getWUDInfo().getGamePartition().getRawTicket();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawCert() throws IOException {
|
||||
return getWUDInfo().getGamePartition().getRawCert();
|
||||
}
|
||||
|
||||
|
||||
public WUDPartition getGamePartition(){
|
||||
return getWUDInfo().getGamePartition();
|
||||
}
|
||||
|
||||
public WUDPartitionHeader getGamePartitionHeader(){
|
||||
return getGamePartition().getPartitionHeader();
|
||||
}
|
||||
|
||||
public WUDDiscReader getDiscReader(){
|
||||
return getWUDInfo().getWUDDiscReader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
}
|
||||
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations.woomy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WoomyMeta {
|
||||
private String name;
|
||||
private int icon;
|
||||
private List<WoomyEntry> entries;
|
||||
|
||||
public void addEntry(String name,String folder, int entryCount){
|
||||
WoomyEntry entry = new WoomyEntry(name, folder, entryCount);
|
||||
getEntries().add(entry);
|
||||
}
|
||||
|
||||
public List<WoomyEntry> getEntries(){
|
||||
if(entries == null){
|
||||
setEntries(new ArrayList<>());
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class WoomyEntry {
|
||||
|
||||
public WoomyEntry(String name, String folder, int entryCount) {
|
||||
setName(name);
|
||||
setFolder(folder);
|
||||
setEntryCount(entryCount);
|
||||
}
|
||||
|
||||
private String name;
|
||||
private String folder;
|
||||
private int entryCount;
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations.wud;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
|
||||
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReaderCompressed;
|
||||
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReaderSplitted;
|
||||
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReaderUncompressed;
|
||||
import de.mas.jnus.lib.utils.ByteUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class WUDImage {
|
||||
public static long WUD_FILESIZE = 0x5D3A00000L;
|
||||
|
||||
@Getter @Setter private File fileHandle = null;
|
||||
@Getter @Setter private WUDImageCompressedInfo compressedInfo = null;
|
||||
|
||||
@Getter @Setter private boolean isCompressed = false;
|
||||
@Getter @Setter private boolean isSplitted = false;
|
||||
private long inputFileSize = 0L;
|
||||
@Setter private WUDDiscReader WUDDiscReader = null;
|
||||
|
||||
public WUDImage(File file) throws IOException{
|
||||
if(file == null || !file.exists()){
|
||||
System.out.println("WUD file is null or does not exist");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
RandomAccessFile fileStream = new RandomAccessFile(file,"r");
|
||||
fileStream.seek(0);
|
||||
byte[] wuxheader = new byte[WUDImageCompressedInfo.WUX_HEADER_SIZE];
|
||||
fileStream.read(wuxheader);
|
||||
WUDImageCompressedInfo compressedInfo = new WUDImageCompressedInfo(wuxheader);
|
||||
|
||||
if(compressedInfo.isWUX()){
|
||||
log.info("Image is compressed");
|
||||
setCompressed(true);
|
||||
Map<Integer,Long> indexTable = new HashMap<>();
|
||||
long offsetIndexTable = compressedInfo.getOffsetIndexTable();
|
||||
fileStream.seek(offsetIndexTable);
|
||||
|
||||
byte[] tableData = new byte[(int) (compressedInfo.getIndexTableEntryCount() * 0x04)];
|
||||
fileStream.read(tableData);
|
||||
int cur_offset = 0x00;
|
||||
for(long i = 0;i<compressedInfo.getIndexTableEntryCount();i++){
|
||||
indexTable.put((int) i, ByteUtils.getUnsingedIntFromBytes(tableData, (int) cur_offset,ByteOrder.LITTLE_ENDIAN));
|
||||
cur_offset += 0x04;
|
||||
}
|
||||
compressedInfo.setIndexTable(indexTable);
|
||||
setCompressedInfo(compressedInfo);
|
||||
}else if(file.getName().equals(String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, 1)) &&
|
||||
(file.length() == WUDDiscReaderSplitted.WUD_SPLITTED_FILE_SIZE)){
|
||||
setSplitted(true);
|
||||
log.info("Image is splitted");
|
||||
}
|
||||
|
||||
fileStream.close();
|
||||
setFileHandle(file);
|
||||
}
|
||||
|
||||
public WUDDiscReader getWUDDiscReader() {
|
||||
if(WUDDiscReader == null){
|
||||
if(isCompressed()){
|
||||
setWUDDiscReader(new WUDDiscReaderCompressed(this));
|
||||
}else if(isSplitted()){
|
||||
setWUDDiscReader(new WUDDiscReaderSplitted(this));
|
||||
}else{
|
||||
setWUDDiscReader(new WUDDiscReaderUncompressed(this));
|
||||
}
|
||||
}
|
||||
return WUDDiscReader;
|
||||
}
|
||||
|
||||
public long getWUDFileSize() {
|
||||
if(inputFileSize == 0){
|
||||
if(isSplitted()){
|
||||
inputFileSize = calculateSplittedFileSize();
|
||||
}else if(isCompressed()){
|
||||
inputFileSize = getCompressedInfo().getUncompressedSize();
|
||||
}else{
|
||||
inputFileSize = getFileHandle().length();
|
||||
}
|
||||
}
|
||||
return inputFileSize;
|
||||
}
|
||||
|
||||
private long calculateSplittedFileSize() {
|
||||
long result = 0;
|
||||
File filehandlePart1 = new File(getFileHandle().getAbsolutePath());
|
||||
String pathToFiles = filehandlePart1.getParentFile().getAbsolutePath();
|
||||
for(int i = 1; i<=WUDDiscReaderSplitted.NUMBER_OF_FILES;i++){
|
||||
String filePartPath = pathToFiles + File.separator + String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, i);
|
||||
File part = new File(filePartPath);
|
||||
if(part.exists()){
|
||||
result += part.length();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations.wud;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.jnus.lib.utils.ByteUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class WUDImageCompressedInfo {
|
||||
public static final int WUX_HEADER_SIZE = 0x20;
|
||||
public static final int WUX_MAGIC_0 = 0x30585557;
|
||||
public static final int WUX_MAGIC_1 = 0x1099d02e;
|
||||
public static final int SECTOR_SIZE = 0x8000;
|
||||
|
||||
@Getter @Setter private int magic0;
|
||||
@Getter @Setter private int magic1;
|
||||
@Getter @Setter private int sectorSize;
|
||||
@Getter @Setter private long uncompressedSize;
|
||||
@Getter @Setter private int flags;
|
||||
|
||||
@Getter @Setter private long indexTableEntryCount = 0;
|
||||
@Getter @Setter private long offsetIndexTable = 0;
|
||||
@Getter @Setter private long offsetSectorArray = 0;
|
||||
@Getter @Setter private long indexTableSize = 0;
|
||||
|
||||
@Getter private Map<Integer,Long> indexTable = new HashMap<>();
|
||||
|
||||
public WUDImageCompressedInfo(byte[] headData){
|
||||
if(headData.length < WUX_HEADER_SIZE){
|
||||
System.out.println("WUX header length wrong");
|
||||
System.exit(1);
|
||||
}
|
||||
setMagic0(ByteUtils.getIntFromBytes(headData, 0x00,ByteOrder.LITTLE_ENDIAN));
|
||||
setMagic1(ByteUtils.getIntFromBytes(headData, 0x04,ByteOrder.LITTLE_ENDIAN));
|
||||
setSectorSize(ByteUtils.getIntFromBytes(headData, 0x08,ByteOrder.LITTLE_ENDIAN));
|
||||
setFlags(ByteUtils.getIntFromBytes(headData, 0x0C,ByteOrder.LITTLE_ENDIAN));
|
||||
setUncompressedSize(ByteUtils.getLongFromBytes(headData, 0x10,ByteOrder.LITTLE_ENDIAN));
|
||||
|
||||
calculateOffsets();
|
||||
}
|
||||
|
||||
public static WUDImageCompressedInfo getDefaultCompressedInfo(){
|
||||
return new WUDImageCompressedInfo(SECTOR_SIZE, 0, WUDImage.WUD_FILESIZE);
|
||||
}
|
||||
|
||||
public WUDImageCompressedInfo(int sectorSize,int flags, long uncompressedSize) {
|
||||
setMagic0(WUX_MAGIC_0);
|
||||
setMagic1(WUX_MAGIC_1);
|
||||
setSectorSize(sectorSize);
|
||||
setFlags(flags);
|
||||
setUncompressedSize(uncompressedSize);
|
||||
}
|
||||
|
||||
private void calculateOffsets() {
|
||||
long indexTableEntryCount = (getUncompressedSize()+ getSectorSize()-1) / getSectorSize();
|
||||
setIndexTableEntryCount(indexTableEntryCount);
|
||||
setOffsetIndexTable(0x20);
|
||||
long offsetSectorArray = (getOffsetIndexTable() + ((long)getIndexTableEntryCount() * 0x04L));
|
||||
// align to SECTOR_SIZE
|
||||
offsetSectorArray = (offsetSectorArray + (long)(getSectorSize()-1));
|
||||
offsetSectorArray = offsetSectorArray - (offsetSectorArray%(long)getSectorSize());
|
||||
setOffsetSectorArray(offsetSectorArray);
|
||||
// read index table
|
||||
setIndexTableSize(0x04 * getIndexTableEntryCount());
|
||||
}
|
||||
|
||||
public boolean isWUX() {
|
||||
return (getMagic0() == WUX_MAGIC_0 && getMagic1() == WUX_MAGIC_1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WUDImageCompressedInfo [magic0=" + String.format("0x%08X", magic0) + ", magic1=" + String.format("0x%08X", magic1) + ", sectorSize=" + String.format("0x%08X", sectorSize)
|
||||
+ ", uncompressedSize=" + String.format("0x%016X", uncompressedSize) + ", flags=" + String.format("0x%08X", flags) + ", indexTableEntryCount="
|
||||
+ indexTableEntryCount + ", offsetIndexTable=" + offsetIndexTable + ", offsetSectorArray="
|
||||
+ offsetSectorArray + ", indexTableSize=" + indexTableSize + "]";
|
||||
}
|
||||
|
||||
public long getSectorIndex(int sectorIndex) {
|
||||
return getIndexTable().get(sectorIndex);
|
||||
}
|
||||
|
||||
public void setIndexTable(Map<Integer,Long> indexTable) {
|
||||
this.indexTable = indexTable;
|
||||
}
|
||||
|
||||
public byte[] getHeaderAsBytes() {
|
||||
ByteBuffer result = ByteBuffer.allocate(WUX_HEADER_SIZE);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.putInt(getMagic0());
|
||||
result.putInt(getMagic1());
|
||||
result.putInt(getSectorSize());
|
||||
result.putInt(getFlags());
|
||||
result.putLong(getUncompressedSize());
|
||||
return result.array();
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations.wud.parser;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper=true)
|
||||
public class WUDGamePartition extends WUDPartition {
|
||||
private byte[] rawTMD;
|
||||
private byte[] rawCert;
|
||||
private byte[] rawTicket;
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations.wud.parser;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Data
|
||||
public class WUDInfo {
|
||||
private byte[] titleKey = null;
|
||||
|
||||
private WUDDiscReader WUDDiscReader = null;
|
||||
private Map<String,WUDPartition> partitions = null;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PROTECTED)
|
||||
private String gamePartitionName;
|
||||
|
||||
WUDInfo(){
|
||||
}
|
||||
|
||||
|
||||
private WUDGamePartition cachedGamePartition = null;
|
||||
public WUDGamePartition getGamePartition(){
|
||||
if(cachedGamePartition == null){
|
||||
cachedGamePartition = findGamePartition();
|
||||
}
|
||||
return cachedGamePartition;
|
||||
}
|
||||
private WUDGamePartition findGamePartition() {
|
||||
for(Entry<String,WUDPartition> e: getPartitions().entrySet()){
|
||||
if(e.getKey().equals(getGamePartitionName())){
|
||||
return (WUDGamePartition) e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations.wud.parser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import de.mas.jnus.lib.entities.content.ContentFSTInfo;
|
||||
import de.mas.jnus.lib.entities.fst.FST;
|
||||
import de.mas.jnus.lib.entities.fst.FSTEntry;
|
||||
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
|
||||
import de.mas.jnus.lib.utils.ByteUtils;
|
||||
import de.mas.jnus.lib.utils.FileUtils;
|
||||
import de.mas.jnus.lib.utils.Utils;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class WUDInfoParser {
|
||||
public static byte[] DECRYPTED_AREA_SIGNATURE = new byte[] { (byte) 0xCC, (byte) 0xA6, (byte) 0xE6, 0x7B };
|
||||
public static byte[] PARTITION_FILE_TABLE_SIGNATURE = new byte[] { 0x46, 0x53, 0x54, 0x00 }; // "FST"
|
||||
public final static int PARTITION_TOC_OFFSET = 0x800;
|
||||
public final static int PARTITION_TOC_ENTRY_SIZE = 0x80;
|
||||
|
||||
public static final String WUD_TMD_FILENAME = "title.tmd";
|
||||
public static final String WUD_TICKET_FILENAME = "title.tik";
|
||||
public static final String WUD_CERT_FILENAME = "title.cert";
|
||||
|
||||
public static WUDInfo createAndLoad(WUDDiscReader discReader,byte[] titleKey) throws IOException {
|
||||
WUDInfo result = new WUDInfo();
|
||||
|
||||
result.setTitleKey(titleKey);
|
||||
result.setWUDDiscReader(discReader);
|
||||
|
||||
byte[] PartitionTocBlock = discReader.readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET, 0, 0x8000, titleKey, null);
|
||||
|
||||
// verify DiscKey before proceeding
|
||||
if(!Arrays.equals(Arrays.copyOfRange(PartitionTocBlock, 0, 4),DECRYPTED_AREA_SIGNATURE)){
|
||||
System.out.println("Decryption of PartitionTocBlock failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String,WUDPartition> partitions = readPartitions(result,PartitionTocBlock);
|
||||
result.setPartitions(partitions);
|
||||
//parsePartitions(wudInfo,partitions);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static Map<String, WUDPartition> readPartitions(WUDInfo wudInfo,byte[] partitionTocBlock) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(partitionTocBlock.length);
|
||||
|
||||
buffer.order(ByteOrder.BIG_ENDIAN);
|
||||
buffer.put(partitionTocBlock);
|
||||
buffer.position(0);
|
||||
|
||||
int partitionCount = (int) ByteUtils.getUnsingedIntFromBytes(partitionTocBlock, 0x1C,ByteOrder.BIG_ENDIAN);
|
||||
|
||||
Map<String,WUDPartition> partitions = new HashMap<>();
|
||||
|
||||
WUDGamePartition gamePartition = new WUDGamePartition();
|
||||
|
||||
String realGamePartitionName = null;
|
||||
// populate partition information from decrypted TOC
|
||||
for (int i = 0; i < partitionCount; i++){
|
||||
WUDPartition partition = new WUDPartition();
|
||||
|
||||
int offset = (PARTITION_TOC_OFFSET + (i * PARTITION_TOC_ENTRY_SIZE));
|
||||
byte[] partitionIdentifier = Arrays.copyOfRange(partitionTocBlock, offset, offset+ 0x19);
|
||||
int j = 0;
|
||||
for(j = 0;j<partitionIdentifier.length;j++){
|
||||
if(partitionIdentifier[j] == 0){
|
||||
break;
|
||||
}
|
||||
}
|
||||
String partitionName = new String(Arrays.copyOfRange(partitionIdentifier,0,j));
|
||||
|
||||
// calculate partition offset (relative from WIIU_DECRYPTED_AREA_OFFSET) from decrypted TOC
|
||||
long tmp = ByteUtils.getUnsingedIntFromBytes(partitionTocBlock, (PARTITION_TOC_OFFSET + (i * PARTITION_TOC_ENTRY_SIZE) + 0x20), ByteOrder.BIG_ENDIAN);
|
||||
|
||||
long partitionOffset = ((tmp * (long)0x8000) - 0x10000);
|
||||
|
||||
|
||||
partition.setPartitionName(partitionName);
|
||||
partition.setPartitionOffset(partitionOffset);
|
||||
|
||||
if(partitionName.startsWith("SI")){
|
||||
byte[] fileTableBlock = wudInfo.getWUDDiscReader().readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET + partitionOffset,0, 0x8000, wudInfo.getTitleKey(),null);
|
||||
if(!Arrays.equals(Arrays.copyOfRange(fileTableBlock, 0, 4),PARTITION_FILE_TABLE_SIGNATURE)){
|
||||
log.info("FST Decrpytion failed");
|
||||
continue;
|
||||
}
|
||||
|
||||
FST fst = FST.parseFST(fileTableBlock, null);
|
||||
|
||||
byte[] rawTIK = getFSTEntryAsByte(WUD_TICKET_FILENAME,partition,fst,wudInfo.getWUDDiscReader(),wudInfo.getTitleKey());
|
||||
byte[] rawTMD = getFSTEntryAsByte(WUD_TMD_FILENAME,partition,fst,wudInfo.getWUDDiscReader(),wudInfo.getTitleKey());
|
||||
byte[] rawCert = getFSTEntryAsByte(WUD_CERT_FILENAME,partition,fst,wudInfo.getWUDDiscReader(),wudInfo.getTitleKey());
|
||||
|
||||
gamePartition.setRawTMD(rawTMD);
|
||||
gamePartition.setRawTicket(rawTIK);
|
||||
gamePartition.setRawCert(rawCert);
|
||||
|
||||
//We want to use the real game partition
|
||||
realGamePartitionName = partitionName = "GM" + Utils.ByteArrayToString(Arrays.copyOfRange(rawTIK, 0x1DC, 0x1DC + 0x08));
|
||||
}else if(partitionName.startsWith(realGamePartitionName)){
|
||||
gamePartition.setPartitionOffset(partitionOffset);
|
||||
gamePartition.setPartitionName(partitionName);
|
||||
|
||||
wudInfo.setGamePartitionName(partitionName);
|
||||
partition = gamePartition;
|
||||
}
|
||||
byte [] header = wudInfo.getWUDDiscReader().readEncryptedToByteArray(partition.getPartitionOffset()+0x10000,0,0x8000);
|
||||
WUDPartitionHeader partitionHeader = WUDPartitionHeader.parseHeader(header);
|
||||
partition.setPartitionHeader(partitionHeader);
|
||||
|
||||
partitions.put(partitionName, partition);
|
||||
}
|
||||
|
||||
return partitions;
|
||||
}
|
||||
|
||||
private static byte[] getFSTEntryAsByte(String filename, WUDPartition partition,FST fst,WUDDiscReader discReader,byte[] key) throws IOException{
|
||||
FSTEntry entry = getEntryByName(fst.getRoot(),filename);
|
||||
ContentFSTInfo info = fst.getContentFSTInfos().get((int)entry.getContentFSTID());
|
||||
|
||||
//Calculating the IV
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(0x10);
|
||||
byteBuffer.position(0x08);
|
||||
byte[] IV = byteBuffer.putLong(entry.getFileOffset()>>16).array();
|
||||
|
||||
return discReader.readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET + (long)partition.getPartitionOffset() + (long)info.getOffset(),entry.getFileOffset(), (int) entry.getFileSize(), key, IV);
|
||||
}
|
||||
|
||||
private static FSTEntry getEntryByName(FSTEntry root,String name){
|
||||
for(FSTEntry cur : root.getFileChildren()){
|
||||
if(cur.getFilename().equals(name)){
|
||||
return cur;
|
||||
}
|
||||
}
|
||||
for(FSTEntry cur : root.getDirChildren()){
|
||||
FSTEntry dir_result = getEntryByName(cur,name);
|
||||
if(dir_result != null){
|
||||
return dir_result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations.wud.parser;
|
||||
|
||||
import lombok.Data;
|
||||
@Data
|
||||
public class WUDPartition {
|
||||
private String partitionName = "";
|
||||
private long partitionOffset = 0;
|
||||
|
||||
private WUDPartitionHeader partitionHeader;
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations.wud.parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.utils.ByteUtils;
|
||||
import de.mas.jnus.lib.utils.HashUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class WUDPartitionHeader {
|
||||
@Getter @Setter
|
||||
private boolean calculatedHashes = false;
|
||||
private HashMap<Short,byte[]> h3Hashes;
|
||||
@Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE)
|
||||
private byte[] rawData;
|
||||
|
||||
private WUDPartitionHeader(){
|
||||
}
|
||||
|
||||
//TODO: real processing. Currently we are ignoring everything except the hashes
|
||||
public static WUDPartitionHeader parseHeader(byte[] header) {
|
||||
WUDPartitionHeader result = new WUDPartitionHeader();
|
||||
result.setRawData(header);
|
||||
return result;
|
||||
}
|
||||
|
||||
public HashMap<Short,byte[]> getH3Hashes() {
|
||||
if(h3Hashes == null){
|
||||
h3Hashes = new HashMap<>();
|
||||
}
|
||||
return h3Hashes;
|
||||
}
|
||||
|
||||
public void addH3Hashes(short index, byte[] hash) {
|
||||
getH3Hashes().put(index, hash);
|
||||
}
|
||||
|
||||
public byte[] getH3Hash(Content content) {
|
||||
if(content == null){
|
||||
log.info("Can't find h3 hash, given content is null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return getH3Hashes().get(content.getIndex());
|
||||
}
|
||||
|
||||
public void calculateHashes(Map<Integer, Content> allContents) {
|
||||
byte[] header = getRawData();
|
||||
|
||||
//Calculating offset for the hashes
|
||||
int cnt = ByteUtils.getIntFromBytes(header,0x10);
|
||||
int start_offset = 0x40 + cnt*0x04;
|
||||
|
||||
int offset = 0;
|
||||
|
||||
//We have to make sure, that the list is ordered by index
|
||||
List<Content> contents = new ArrayList<>(allContents.values());
|
||||
Collections.sort( contents, new Comparator<Content>() {
|
||||
@Override
|
||||
public int compare(Content o1, Content o2) {
|
||||
return Short.compare(o1.getIndex(), o2.getIndex());
|
||||
}
|
||||
} );
|
||||
|
||||
for(Content c : allContents.values()){
|
||||
if(!c.isHashed() || !c.isEncrypted()){
|
||||
continue;
|
||||
}
|
||||
|
||||
//The encrypted content are splitted in 0x10000 chunk. For each 0x1000 chunk we need one entry in the h3
|
||||
int cnt_hashes = (int) (c.getEncryptedFileSize()/0x10000/0x1000)+1;
|
||||
|
||||
byte[] hash = Arrays.copyOfRange(header, start_offset+ offset*0x14, start_offset+ (offset+cnt_hashes)*0x14);
|
||||
|
||||
//Checking the hash of the h3 file.
|
||||
if(!Arrays.equals(HashUtil.hashSHA1(hash), c.getSHA2Hash())){
|
||||
log.info("h3 incorrect from WUD");
|
||||
}
|
||||
|
||||
addH3Hashes(c.getIndex(), hash);
|
||||
offset += cnt_hashes;
|
||||
}
|
||||
|
||||
setCalculatedHashes(true);
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
package de.mas.jnus.lib.implementations.wud.reader;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.mas.jnus.lib.implementations.wud.WUDImage;
|
||||
import de.mas.jnus.lib.utils.cryptography.AESDecryption;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
@Log
|
||||
public abstract class WUDDiscReader {
|
||||
@Getter @Setter private WUDImage image = null;
|
||||
|
||||
public WUDDiscReader(WUDImage image){
|
||||
setImage(image);
|
||||
}
|
||||
|
||||
public InputStream readEncryptedToInputStream(long offset,long size) throws IOException {
|
||||
PipedInputStream in = new PipedInputStream();
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
|
||||
new Thread(() -> {try {
|
||||
readEncryptedToOutputStream(out,offset,size);
|
||||
} catch (IOException e) {e.printStackTrace();}}).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
public byte[] readEncryptedToByteArray(long offset,long fileoffset, long size) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
readEncryptedToOutputStream(out,offset,size);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public InputStream readDecryptedToInputStream(long offset,long fileoffset, long size, byte[] key,byte[] iv) throws IOException {
|
||||
PipedInputStream in = new PipedInputStream();
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
|
||||
new Thread(() -> {try {
|
||||
readDecryptedToOutputStream(out,offset,fileoffset,size,key,iv);
|
||||
} catch (IOException e) {e.printStackTrace();}}).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
public byte[] readDecryptedToByteArray(long offset,long fileoffset, long size, byte[] key,byte[] iv) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
readDecryptedToOutputStream(out,offset,fileoffset,size,key,iv);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
protected abstract void readEncryptedToOutputStream(OutputStream out, long offset, long size) throws IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param readOffset Needs to be aligned to 0x8000
|
||||
* @param key
|
||||
* @param IV
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public byte[] readDecryptedChunk(long readOffset,byte[] key, byte[]IV) throws IOException{
|
||||
int chunkSize = 0x8000;
|
||||
|
||||
byte[] encryptedChunk = readEncryptedToByteArray(readOffset, 0, chunkSize);
|
||||
byte[] decryptedChunk = new byte[chunkSize];
|
||||
|
||||
AESDecryption aesDecryption = new AESDecryption(key, IV);
|
||||
decryptedChunk = aesDecryption.decrypt(encryptedChunk);
|
||||
|
||||
return decryptedChunk;
|
||||
}
|
||||
|
||||
public void readDecryptedToOutputStream(OutputStream outputStream,long clusterOffset, long fileOffset, long size,byte[] key,byte[] IV) throws IOException {
|
||||
if(IV == null){
|
||||
IV = new byte[0x10];
|
||||
}
|
||||
|
||||
byte[] buffer;
|
||||
|
||||
long maxCopySize;
|
||||
long copySize;
|
||||
|
||||
long readOffset;
|
||||
|
||||
int blockSize = 0x8000;
|
||||
long totalread = 0;
|
||||
|
||||
do{
|
||||
long blockNumber = (fileOffset / blockSize);
|
||||
long blockOffset = (fileOffset % blockSize);
|
||||
|
||||
readOffset = clusterOffset + (blockNumber * blockSize);
|
||||
// (long)WiiUDisc.WIIU_DECRYPTED_AREA_OFFSET + volumeOffset + clusterOffset + (blockStructure.getBlockNumber() * 0x8000);
|
||||
|
||||
buffer = readDecryptedChunk(readOffset,key, IV);
|
||||
maxCopySize = 0x8000 - blockOffset;
|
||||
copySize = (size > maxCopySize) ? maxCopySize : size;
|
||||
|
||||
outputStream.write(Arrays.copyOfRange(buffer, (int) blockOffset, (int) copySize));
|
||||
totalread += copySize;
|
||||
|
||||
// update counters
|
||||
size -= copySize;
|
||||
fileOffset += copySize;
|
||||
}while(totalread < size);
|
||||
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
public RandomAccessFile getRandomAccessFileStream() throws FileNotFoundException{
|
||||
if(getImage() == null || getImage().getFileHandle() == null){
|
||||
log.warning("No image or image filehandle set.");
|
||||
System.exit(1);
|
||||
}
|
||||
return new RandomAccessFile(getImage().getFileHandle(), "r");
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package de.mas.jnus.lib.utils;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class ByteArrayBuffer{
|
||||
@Getter public byte[] buffer;
|
||||
@Getter @Setter int lengthOfDataInBuffer;
|
||||
|
||||
public ByteArrayBuffer(int length){
|
||||
buffer = new byte[(int) length];
|
||||
}
|
||||
|
||||
public int getSpaceLeft() {
|
||||
return buffer.length - getLengthOfDataInBuffer();
|
||||
}
|
||||
|
||||
public void addLengthOfDataInBuffer(int bytesRead) {
|
||||
lengthOfDataInBuffer += bytesRead;
|
||||
}
|
||||
|
||||
public void resetLengthOfDataInBuffer() {
|
||||
setLengthOfDataInBuffer(0);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package de.mas.jnus.lib.utils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class ByteArrayWrapper
|
||||
{
|
||||
private final byte[] data;
|
||||
|
||||
public ByteArrayWrapper(byte[] data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
throw new NullPointerException();
|
||||
}
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other)
|
||||
{
|
||||
if (!(other instanceof ByteArrayWrapper))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return Arrays.equals(data, ((ByteArrayWrapper)other).data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Arrays.hashCode(data);
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package de.mas.jnus.lib.utils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ByteUtils {
|
||||
public static int getIntFromBytes(byte[] input,int offset){
|
||||
return getIntFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
public static int getIntFromBytes(byte[] input,int offset,ByteOrder bo){
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.order(bo);
|
||||
Arrays.copyOfRange(input,offset, offset+4);
|
||||
buffer.put(Arrays.copyOfRange(input,offset, offset+4));
|
||||
|
||||
return buffer.getInt(0);
|
||||
}
|
||||
|
||||
public static long getUnsingedIntFromBytes(byte[] input,int offset){
|
||||
return getUnsingedIntFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
|
||||
public static long getUnsingedIntFromBytes(byte[] input,int offset,ByteOrder bo){
|
||||
ByteBuffer buffer = ByteBuffer.allocate(8);
|
||||
buffer.order(bo);
|
||||
if(bo.equals(ByteOrder.BIG_ENDIAN)){
|
||||
buffer.position(4);
|
||||
}else{
|
||||
buffer.position(0);
|
||||
}
|
||||
buffer.put(Arrays.copyOfRange(input,offset, offset+4));
|
||||
|
||||
return buffer.getLong(0);
|
||||
}
|
||||
|
||||
public static long getLongFromBytes(byte[] input,int offset){
|
||||
return getLongFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
|
||||
public static long getLongFromBytes(byte[] input,int offset,ByteOrder bo){
|
||||
return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+8)).order(bo).getLong(0);
|
||||
}
|
||||
|
||||
public static short getShortFromBytes(byte[] input,int offset){
|
||||
return getShortFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
public static short getShortFromBytes(byte[] input, int offset,ByteOrder bo) {
|
||||
return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+2)).order(bo).getShort();
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromLong(long value) {
|
||||
return getBytesFromLong(value,ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
public static byte[] getBytesFromLong(long value,ByteOrder bo) {
|
||||
byte[] result = new byte[0x08];
|
||||
ByteBuffer.allocate(8).order(bo).putLong(value).get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromInt(int value) {
|
||||
return getBytesFromInt(value,ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
public static byte[] getBytesFromInt(int value,ByteOrder bo) {
|
||||
byte[] result = new byte[0x04];
|
||||
ByteBuffer.allocate(4).order(bo).putInt(value).get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromShort(short value) {
|
||||
byte[] result = new byte[0x02];
|
||||
ByteBuffer.allocate(2).putShort(value).get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
package de.mas.jnus.lib.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import lombok.extern.java.Log;
|
||||
@Log
|
||||
public class HashUtil {
|
||||
public static byte[] hashSHA256(byte[] data){
|
||||
MessageDigest sha256;
|
||||
try {
|
||||
sha256 = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
return new byte[0x20];
|
||||
}
|
||||
|
||||
return sha256.digest(data);
|
||||
}
|
||||
|
||||
public static byte[] hashSHA256(File file) {
|
||||
return hashSHA256(file, 0);
|
||||
}
|
||||
|
||||
//TODO: testing
|
||||
public static byte[] hashSHA256(File file,int aligmnent) {
|
||||
byte[] hash = new byte[0x20];
|
||||
MessageDigest sha1 = null;
|
||||
try {
|
||||
InputStream in = new FileInputStream(file);
|
||||
sha1 = MessageDigest.getInstance("SHA-256");
|
||||
hash = hash(sha1,in,file.length(),0x8000,aligmnent);
|
||||
} catch (NoSuchAlgorithmException | FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public static byte[] hashSHA1(byte[] data){
|
||||
MessageDigest sha1;
|
||||
try {
|
||||
sha1 = MessageDigest.getInstance("SHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
return new byte[0x14];
|
||||
}
|
||||
|
||||
return sha1.digest(data);
|
||||
}
|
||||
|
||||
public static byte[] hashSHA1(File file) {
|
||||
return hashSHA1(file, 0);
|
||||
}
|
||||
|
||||
public static byte[] hashSHA1(File file,int aligmnent) {
|
||||
byte[] hash = new byte[0x14];
|
||||
MessageDigest sha1 = null;
|
||||
try {
|
||||
InputStream in = new FileInputStream(file);
|
||||
sha1 = MessageDigest.getInstance("SHA1");
|
||||
hash = hash(sha1,in,file.length(),0x8000,aligmnent);
|
||||
} catch (NoSuchAlgorithmException | FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public static byte [] hash(MessageDigest digest, InputStream in,long inputSize1, int bufferSize,int alignment) throws IOException {
|
||||
long target_size = alignment == 0 ? inputSize1: Utils.align(inputSize1, alignment);
|
||||
long cur_position = 0;
|
||||
int inBlockBufferRead = 0;
|
||||
byte[] blockBuffer = new byte[bufferSize];
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(bufferSize);
|
||||
do{
|
||||
if(cur_position + bufferSize > target_size){
|
||||
int expectedSize = (int) (target_size - cur_position);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(expectedSize);
|
||||
buffer.position(0);
|
||||
inBlockBufferRead = StreamUtils.getChunkFromStream(in,blockBuffer,overflow,expectedSize);
|
||||
buffer.put(Arrays.copyOfRange(blockBuffer, 0, inBlockBufferRead));
|
||||
blockBuffer = buffer.array();
|
||||
inBlockBufferRead = blockBuffer.length;
|
||||
}else{
|
||||
int expectedSize = bufferSize;
|
||||
inBlockBufferRead = StreamUtils.getChunkFromStream(in,blockBuffer,overflow,expectedSize);
|
||||
}
|
||||
if(inBlockBufferRead == 0){
|
||||
inBlockBufferRead = (int) (target_size - cur_position);
|
||||
blockBuffer = new byte[inBlockBufferRead];
|
||||
}
|
||||
if(inBlockBufferRead <= 0) break;
|
||||
|
||||
digest.update(blockBuffer, 0, inBlockBufferRead);
|
||||
cur_position += inBlockBufferRead;
|
||||
|
||||
}while(cur_position < target_size);
|
||||
|
||||
in.close();
|
||||
|
||||
return digest.digest();
|
||||
}
|
||||
|
||||
public static void checkFileChunkHashes(byte[] hashes,byte[] h3Hashes, byte[] output,int block) {
|
||||
int H0_start = (block % 16) * 20;
|
||||
int H1_start = (16 + (block / 16) % 16) * 20;
|
||||
int H2_start = (32 + (block / 256) % 16) * 20;
|
||||
int H3_start = ((block / 4096) % 16) * 20;
|
||||
|
||||
byte[] real_h0_hash = HashUtil.hashSHA1(output);
|
||||
byte[] expected_h0_hash = Arrays.copyOfRange(hashes,H0_start,H0_start + 20);
|
||||
|
||||
if(!Arrays.equals(real_h0_hash,expected_h0_hash)){
|
||||
System.out.println("h0 checksum failed");
|
||||
System.out.println("real hash :" + Utils.ByteArrayToString(real_h0_hash));
|
||||
System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h0_hash));
|
||||
System.exit(2);
|
||||
//throw new IllegalArgumentException("h0 checksumfail");
|
||||
}else{
|
||||
log.finest("h1 checksum right!");
|
||||
}
|
||||
|
||||
if ((block % 16) == 0){
|
||||
byte[] expected_h1_hash = Arrays.copyOfRange(hashes,H1_start,H1_start + 20);
|
||||
byte[] real_h1_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes,H0_start,H0_start + (16*20)));
|
||||
|
||||
if(!Arrays.equals(expected_h1_hash, real_h1_hash)){
|
||||
System.out.println("h1 checksum failed");
|
||||
System.out.println("real hash :" + Utils.ByteArrayToString(real_h1_hash));
|
||||
System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h1_hash));
|
||||
System.exit(2);
|
||||
//throw new IllegalArgumentException("h1 checksumfail");
|
||||
}else{
|
||||
log.finer("h1 checksum right!");
|
||||
}
|
||||
}
|
||||
|
||||
if ((block % 256) == 0){
|
||||
byte[] expected_h2_hash = Arrays.copyOfRange(hashes,H2_start,H2_start + 20);
|
||||
byte[] real_h2_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes,H1_start,H1_start + (16*20)));
|
||||
|
||||
if(!Arrays.equals(expected_h2_hash, real_h2_hash)){
|
||||
System.out.println("h2 checksum failed");
|
||||
System.out.println("real hash :" + Utils.ByteArrayToString(real_h2_hash));
|
||||
System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h2_hash));
|
||||
System.exit(2);
|
||||
//throw new IllegalArgumentException("h2 checksumfail");
|
||||
|
||||
}else{
|
||||
log.fine("h2 checksum right!");
|
||||
}
|
||||
}
|
||||
|
||||
if(h3Hashes == null){
|
||||
log.info("didn't check the h3, its missing.");
|
||||
return;
|
||||
}
|
||||
if ((block % 4096) == 0){
|
||||
byte[] expected_h3_hash = Arrays.copyOfRange(h3Hashes,H3_start,H3_start + 20);
|
||||
byte[] real_h3_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes,H2_start,H2_start + (16*20)));
|
||||
|
||||
if(!Arrays.equals(expected_h3_hash, real_h3_hash)){
|
||||
System.out.println("h3 checksum failed");
|
||||
System.out.println("real hash :" + Utils.ByteArrayToString(real_h3_hash));
|
||||
System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h3_hash));
|
||||
System.exit(2);
|
||||
//throw new IllegalArgumentException("h3 checksumfail");
|
||||
}else{
|
||||
log.fine("h3 checksum right!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package de.mas.jnus.lib.utils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class Utils {
|
||||
|
||||
public static long align(long numToRound, int multiple){
|
||||
if((multiple>0) && ((multiple & (multiple -1)) == 0)){
|
||||
return alignPower2(numToRound, multiple);
|
||||
}else{
|
||||
return alignGeneriv(numToRound,multiple);
|
||||
}
|
||||
}
|
||||
|
||||
private static long alignGeneriv(long numToRound,int multiple){
|
||||
int isPositive = 0;
|
||||
if(numToRound >=0){
|
||||
isPositive = 1;
|
||||
}
|
||||
return ((numToRound + isPositive * (multiple - 1)) / multiple) * multiple;
|
||||
}
|
||||
|
||||
private static long alignPower2(long numToRound, int multiple)
|
||||
{
|
||||
if(!((multiple>0) && ((multiple & (multiple -1)) == 0))) return 0L;
|
||||
return (numToRound + (multiple - 1)) & ~(multiple - 1);
|
||||
}
|
||||
|
||||
public static String ByteArrayToString(byte[] ba)
|
||||
{
|
||||
if(ba == null) return null;
|
||||
StringBuilder hex = new StringBuilder(ba.length * 2);
|
||||
for(byte b : ba){
|
||||
hex.append(String.format("%02X", b));
|
||||
}
|
||||
return hex.toString();
|
||||
}
|
||||
|
||||
public static byte[] StringToByteArray(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public static boolean createDir(String path){
|
||||
if(path == null || path.isEmpty()){
|
||||
return false;
|
||||
}
|
||||
File pathFile = new File(path);
|
||||
if(!pathFile.exists()){
|
||||
boolean succes = new File(path).mkdirs();
|
||||
if(!succes){
|
||||
System.out.println("Creating directory \"" +path + "\" failed.");
|
||||
return false;
|
||||
}
|
||||
}else if(!pathFile.isDirectory()){
|
||||
System.out.println("\"" +path + "\" already exists but is no directoy");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package de.mas.jnus.lib.utils.cryptography;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class AESDecryption {
|
||||
private Cipher cipher;
|
||||
|
||||
@Getter @Setter private byte[] AESKey;
|
||||
@Getter @Setter private byte[] IV;
|
||||
|
||||
public AESDecryption(byte[] AESKey, byte[] IV) {
|
||||
try {
|
||||
cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
setAESKey(AESKey);
|
||||
setIV(IV);
|
||||
init();
|
||||
}
|
||||
|
||||
protected void init() {
|
||||
init(getAESKey(),getIV());
|
||||
}
|
||||
|
||||
protected void init(byte[] decryptedKey,byte[] iv){
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES");
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] input){
|
||||
try {
|
||||
return cipher.doFinal(input);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(2);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] input,int len){
|
||||
return decrypt(input,0,len);
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] input,int offset,int len){
|
||||
try {
|
||||
return cipher.doFinal(input, offset, len);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(2);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
package de.mas.jnus.lib.utils.cryptography;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.mas.jnus.lib.entities.Ticket;
|
||||
import de.mas.jnus.lib.utils.ByteArrayBuffer;
|
||||
import de.mas.jnus.lib.utils.HashUtil;
|
||||
import de.mas.jnus.lib.utils.StreamUtils;
|
||||
import de.mas.jnus.lib.utils.Utils;
|
||||
import lombok.extern.java.Log;
|
||||
@Log
|
||||
public class NUSDecryption extends AESDecryption{
|
||||
public NUSDecryption(byte[] AESKey, byte[] IV) {
|
||||
super(AESKey, IV);
|
||||
}
|
||||
|
||||
public NUSDecryption(Ticket ticket) {
|
||||
this(ticket.getDecryptedKey(),ticket.getIV());
|
||||
}
|
||||
|
||||
private byte[] decryptFileChunk(byte[] blockBuffer, int BLOCKSIZE, byte[] IV) {
|
||||
return decryptFileChunk(blockBuffer,0,BLOCKSIZE, IV);
|
||||
}
|
||||
|
||||
private byte[] decryptFileChunk(byte[] blockBuffer, int offset, int BLOCKSIZE, byte[] IV) {
|
||||
if(IV != null){
|
||||
setIV(IV);
|
||||
init();
|
||||
}
|
||||
return decrypt(blockBuffer,offset,BLOCKSIZE);
|
||||
}
|
||||
|
||||
public void decryptFileStream(InputStream inputStream,OutputStream outputStream,long filesize,short contentIndex,byte[] h3hash,long expectedSizeForHash) throws IOException {
|
||||
MessageDigest sha1 = null;
|
||||
if(h3hash != null){
|
||||
try {
|
||||
sha1 = MessageDigest.getInstance("SHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
int BLOCKSIZE = 0x8000;
|
||||
long dlFileLength = filesize;
|
||||
if(dlFileLength > (dlFileLength/BLOCKSIZE)*BLOCKSIZE){
|
||||
dlFileLength = ((dlFileLength/BLOCKSIZE)*BLOCKSIZE) +BLOCKSIZE;
|
||||
}
|
||||
|
||||
byte[] IV = new byte[0x10];
|
||||
|
||||
IV[0] = (byte)((contentIndex >> 8) & 0xFF);
|
||||
IV[1] = (byte)(contentIndex);
|
||||
|
||||
byte[] blockBuffer = new byte[BLOCKSIZE];
|
||||
|
||||
int inBlockBuffer;
|
||||
long written = 0;
|
||||
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
|
||||
|
||||
do{
|
||||
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream,blockBuffer,overflow,BLOCKSIZE);
|
||||
|
||||
byte[] output = decryptFileChunk(blockBuffer,BLOCKSIZE,IV);
|
||||
IV = Arrays.copyOfRange(blockBuffer,BLOCKSIZE-16, BLOCKSIZE);
|
||||
|
||||
if((written + inBlockBuffer) > filesize){
|
||||
inBlockBuffer = (int) (filesize- written);
|
||||
}
|
||||
|
||||
written += inBlockBuffer;
|
||||
|
||||
outputStream.write(output, 0, inBlockBuffer);
|
||||
|
||||
if(sha1 != null){
|
||||
sha1.update(output,0,inBlockBuffer);
|
||||
}
|
||||
}while(inBlockBuffer == BLOCKSIZE);
|
||||
|
||||
if(sha1 != null){
|
||||
long missingInHash = expectedSizeForHash - written;
|
||||
if(missingInHash > 0){
|
||||
sha1.update(new byte[(int) missingInHash]);
|
||||
}
|
||||
|
||||
byte[] calculated_hash = sha1.digest();
|
||||
byte[] expected_hash = h3hash;
|
||||
if(!Arrays.equals(calculated_hash, expected_hash)){
|
||||
log.info(Utils.ByteArrayToString(calculated_hash));
|
||||
log.info(Utils.ByteArrayToString(expected_hash));
|
||||
log.info("Hash doesn't match decrypted content.");
|
||||
}else{
|
||||
//log.warning("###################################################Hash DOES match saves output stream.");
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
}
|
||||
|
||||
public void decryptFileStreamHashed(InputStream inputStream,OutputStream outputStream,long filesize,long fileoffset,short contentIndex,byte[] h3Hash) throws IOException{
|
||||
int BLOCKSIZE = 0x10000;
|
||||
int HASHBLOCKSIZE = 0xFC00;
|
||||
|
||||
long writeSize = HASHBLOCKSIZE;
|
||||
|
||||
long block = (fileoffset / HASHBLOCKSIZE);
|
||||
long soffset = fileoffset - (fileoffset / HASHBLOCKSIZE * HASHBLOCKSIZE);
|
||||
|
||||
if( soffset+filesize > writeSize )
|
||||
writeSize = writeSize - soffset;
|
||||
|
||||
byte[] encryptedBlockBuffer = new byte[BLOCKSIZE];
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
|
||||
|
||||
long wrote = 0;
|
||||
int inBlockBuffer;
|
||||
do{
|
||||
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream,encryptedBlockBuffer,overflow,BLOCKSIZE);
|
||||
|
||||
if( writeSize > filesize )
|
||||
writeSize = filesize;
|
||||
|
||||
byte[] output = decryptFileChunkHash(encryptedBlockBuffer, (int) block,contentIndex,h3Hash);
|
||||
|
||||
if((wrote + writeSize) > filesize){
|
||||
writeSize = (int) (filesize- wrote);
|
||||
}
|
||||
|
||||
outputStream.write(output, (int)(0+soffset), (int)writeSize);
|
||||
|
||||
wrote +=writeSize;
|
||||
|
||||
block++;
|
||||
|
||||
if( soffset > 0){
|
||||
writeSize = HASHBLOCKSIZE;
|
||||
soffset = 0;
|
||||
}
|
||||
}while(wrote < filesize && (inBlockBuffer == BLOCKSIZE));
|
||||
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
}
|
||||
|
||||
private byte[] decryptFileChunkHash(byte[] blockBuffer, int block, int contentIndex,byte[] h3_hashes){
|
||||
int hashSize = 0x400;
|
||||
int blocksize = 0xFC00;
|
||||
byte[] IV = ByteBuffer.allocate(16).putShort((short) contentIndex).array();
|
||||
|
||||
byte[] hashes = decryptFileChunk(blockBuffer,hashSize,IV);
|
||||
|
||||
hashes[0] ^= (byte)((contentIndex >> 8) & 0xFF);
|
||||
hashes[1] ^= (byte)(contentIndex & 0xFF);
|
||||
|
||||
int H0_start = (block % 16) * 20;
|
||||
|
||||
IV = Arrays.copyOfRange(hashes,H0_start,H0_start + 16);
|
||||
byte[] output = decryptFileChunk(blockBuffer,hashSize,blocksize,IV);
|
||||
|
||||
HashUtil.checkFileChunkHashes(hashes,h3_hashes,output,block);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package de.mas.jnus.lib.utils.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import lombok.Setter;
|
||||
|
||||
public class NUSDownloadService extends Downloader{
|
||||
private static NUSDownloadService defaultInstance;
|
||||
|
||||
public static NUSDownloadService getDefaultInstance(){
|
||||
if(defaultInstance == null){
|
||||
defaultInstance = new NUSDownloadService();
|
||||
defaultInstance.setURL_BASE(Settings.URL_BASE);
|
||||
}
|
||||
return defaultInstance;
|
||||
}
|
||||
|
||||
public static NUSDownloadService getInstance(String URL){
|
||||
NUSDownloadService instance = new NUSDownloadService();
|
||||
instance.setURL_BASE(URL);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private NUSDownloadService(){
|
||||
|
||||
}
|
||||
|
||||
@Setter private String URL_BASE = "";
|
||||
|
||||
|
||||
public byte[] downloadTMDToByteArray(long titleID, int version) throws IOException {
|
||||
String version_suf = "";
|
||||
if(version > Settings.LATEST_TMD_VERSION) version_suf = "." + version;
|
||||
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd" +version_suf;
|
||||
return downloadFileToByteArray(URL);
|
||||
}
|
||||
|
||||
public byte[] downloadTicketToByteArray(long titleID) throws IOException {
|
||||
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk";
|
||||
return downloadFileToByteArray(URL);
|
||||
}
|
||||
|
||||
public byte[] downloadToByteArray(String url) throws IOException {
|
||||
String URL = URL_BASE + "/" + url;
|
||||
return downloadFileToByteArray(URL);
|
||||
}
|
||||
|
||||
public InputStream getInputStream(String URL,long offset) throws IOException{
|
||||
URL url_obj = new URL(URL);
|
||||
HttpURLConnection connection = (HttpURLConnection) url_obj.openConnection();
|
||||
connection.setRequestProperty("Range", "bytes=" + offset +"-");
|
||||
try{
|
||||
connection.connect();
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return connection.getInputStream();
|
||||
}
|
||||
|
||||
public InputStream getInputStreamForURL(String url, long offset) throws IOException {
|
||||
String URL = URL_BASE + "/" + url;
|
||||
|
||||
return getInputStream(URL,offset);
|
||||
}
|
||||
}
|
374
src/de/mas/wiiu/jnus/DecryptionService.java
Normal file
374
src/de/mas/wiiu/jnus/DecryptionService.java
Normal file
@ -0,0 +1,374 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.TMD;
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.utils.CheckSumWrongException;
|
||||
import de.mas.wiiu.jnus.utils.HashUtil;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
import de.mas.wiiu.jnus.utils.cryptography.NUSDecryption;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class DecryptionService {
|
||||
private static Map<NUSTitle, DecryptionService> instances = new HashMap<>();
|
||||
@Getter private final NUSTitle NUSTitle;
|
||||
|
||||
public static DecryptionService getInstance(NUSTitle nustitle) {
|
||||
if (!instances.containsKey(nustitle)) {
|
||||
instances.put(nustitle, new DecryptionService(nustitle));
|
||||
}
|
||||
return instances.get(nustitle);
|
||||
}
|
||||
|
||||
private DecryptionService(NUSTitle nustitle) {
|
||||
this.NUSTitle = nustitle;
|
||||
}
|
||||
|
||||
public Ticket getTicket() {
|
||||
return getNUSTitle().getTicket();
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(boolean useFullPath, FSTEntry entry, String outputPath, boolean skipExistingFile) throws IOException, CheckSumWrongException {
|
||||
if (entry.isNotInPackage() || entry.getContent() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//log.info("Decrypting " + entry.getFilename());
|
||||
|
||||
String targetFilePath = new StringBuilder().append(outputPath).append("/").append(entry.getFilename()).toString();
|
||||
String fullPath = new StringBuilder().append(outputPath).toString();
|
||||
|
||||
if (useFullPath) {
|
||||
targetFilePath = new StringBuilder().append(outputPath).append(entry.getFullPath()).toString();
|
||||
fullPath = new StringBuilder().append(outputPath).append(entry.getPath()).toString();
|
||||
if (entry.isDir()) { // If the entry is a directory. Create it and return.
|
||||
Utils.createDir(targetFilePath);
|
||||
return;
|
||||
}
|
||||
} else if (entry.isDir()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Utils.createDir(fullPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
File target = new File(targetFilePath);
|
||||
|
||||
if (skipExistingFile) {
|
||||
File targetFile = new File(targetFilePath);
|
||||
if (targetFile.exists()) {
|
||||
if (entry.isDir()) {
|
||||
return;
|
||||
}
|
||||
if (targetFile.length() == entry.getFileSize()) {
|
||||
Content c = entry.getContent();
|
||||
if (c.isHashed()) {
|
||||
log.info("File already exists: " + entry.getFilename());
|
||||
return;
|
||||
} else {
|
||||
if (Arrays.equals(HashUtil.hashSHA1(target, (int) c.getDecryptedFileSize()), c.getSHA2Hash())) {
|
||||
log.info("File already exists: " + entry.getFilename());
|
||||
return;
|
||||
} else {
|
||||
log.info("File already exists with the same filesize, but the hash doesn't match: " + entry.getFilename());
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.info("File already exists but the filesize doesn't match: " + entry.getFilename());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath));
|
||||
try {
|
||||
decryptFSTEntryToStream(entry, outputStream);
|
||||
} catch (CheckSumWrongException e) {
|
||||
if (entry.getFilename().endsWith(".xml") && Utils.checkXML(new File(targetFilePath))) {
|
||||
log.info("Hash doesn't match, but it's an XML file and it looks okay.");
|
||||
} else {
|
||||
log.info("Hash doesn't match!");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream) throws IOException, CheckSumWrongException {
|
||||
if (entry.isNotInPackage() || entry.getContent() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Content c = entry.getContent();
|
||||
|
||||
long fileSize = entry.getFileSize();
|
||||
long fileOffset = entry.getFileOffset();
|
||||
long fileOffsetBlock = entry.getFileOffsetBlock();
|
||||
|
||||
NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
|
||||
|
||||
InputStream in = dataProvider.getInputStreamFromContent(c, fileOffsetBlock);
|
||||
|
||||
try {
|
||||
decryptFSTEntryFromStreams(in, outputStream, fileSize, fileOffset, c);
|
||||
} catch (CheckSumWrongException e) {
|
||||
log.info("Hash doesn't match");
|
||||
if (entry.getFilename().endsWith(".xml")) {
|
||||
if (outputStream instanceof PipedOutputStream) {
|
||||
log.info("Hash doesn't match. Please check the data for " + entry.getFullPath());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} else if (entry.getContent().isUNKNWNFlag1Set()) {
|
||||
log.info("But file is optional. Don't worry.");
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Detailed info:").append(System.lineSeparator());
|
||||
sb.append(entry).append(System.lineSeparator());
|
||||
sb.append(entry.getContent()).append(System.lineSeparator());
|
||||
sb.append(String.format("%016x", this.NUSTitle.getTMD().getTitleID()));
|
||||
sb.append(e.getMessage() + " Calculated Hash: " + Utils.ByteArrayToString(e.getGivenHash()) + ", expected hash: "
|
||||
+ Utils.ByteArrayToString(e.getExpectedHash()));
|
||||
log.info(sb.toString());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void decryptFSTEntryFromStreams(InputStream inputStream, OutputStream outputStream, long filesize, long fileoffset, Content content)
|
||||
throws IOException, CheckSumWrongException {
|
||||
decryptStreams(inputStream, outputStream, filesize, fileoffset, content);
|
||||
}
|
||||
|
||||
private void decryptContentFromStream(InputStream inputStream, OutputStream outputStream, Content content) throws IOException, CheckSumWrongException {
|
||||
long filesize = content.getDecryptedFileSize();
|
||||
log.info("Decrypting Content " + String.format("%08X", content.getID()));
|
||||
decryptStreams(inputStream, outputStream, filesize, 0L, content);
|
||||
}
|
||||
|
||||
private void decryptStreams(InputStream inputStream, OutputStream outputStream, long size, long offset, Content content)
|
||||
throws IOException, CheckSumWrongException {
|
||||
NUSDecryption nusdecryption = new NUSDecryption(getTicket());
|
||||
short contentIndex = (short) content.getIndex();
|
||||
|
||||
long encryptedFileSize = content.getEncryptedFileSize();
|
||||
|
||||
if (content.isEncrypted()) {
|
||||
if (content.isHashed()) {
|
||||
NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
|
||||
byte[] h3 = dataProvider.getContentH3Hash(content);
|
||||
nusdecryption.decryptFileStreamHashed(inputStream, outputStream, size, offset, (short) contentIndex, h3);
|
||||
} else {
|
||||
nusdecryption.decryptFileStream(inputStream, outputStream, size, (short) contentIndex, content.getSHA2Hash(), encryptedFileSize);
|
||||
}
|
||||
} else {
|
||||
StreamUtils.saveInputStreamToOutputStreamWithHash(inputStream, outputStream, size, content.getSHA2Hash(), encryptedFileSize);
|
||||
}
|
||||
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
public void decryptContentTo(Content content, String outPath, boolean skipExistingFile) throws IOException, CheckSumWrongException {
|
||||
String targetFilePath = outPath + File.separator + content.getFilenameDecrypted();
|
||||
if (skipExistingFile) {
|
||||
File targetFile = new File(targetFilePath);
|
||||
if (targetFile.exists()) {
|
||||
if (targetFile.length() == content.getDecryptedFileSize()) {
|
||||
log.info("File already exists : " + content.getFilenameDecrypted());
|
||||
return;
|
||||
} else {
|
||||
log.info("File already exists but the filesize doesn't match: " + content.getFilenameDecrypted());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Utils.createDir(outPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Decrypting Content " + String.format("%08X", content.getID()));
|
||||
|
||||
FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath));
|
||||
|
||||
decryptContentToStream(content, outputStream);
|
||||
}
|
||||
|
||||
public void decryptContentToStream(Content content, OutputStream outputStream) throws IOException, CheckSumWrongException {
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
|
||||
InputStream inputStream = dataProvider.getInputStreamFromContent(content, 0);
|
||||
|
||||
decryptContentFromStream(inputStream, outputStream, content);
|
||||
}
|
||||
|
||||
public PipedInputStreamWithException getDecryptedOutputAsInputStream(FSTEntry fstEntry) throws IOException {
|
||||
PipedInputStreamWithException in = new PipedInputStreamWithException();
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
|
||||
new Thread(() -> {
|
||||
try { // Throwing it in both cases is EXTREMLY important. Otherwise it'll end in a deadlock
|
||||
decryptFSTEntryToStream(fstEntry, out);
|
||||
in.throwException(null);
|
||||
} catch (Exception e) {
|
||||
in.throwException(e);
|
||||
}
|
||||
|
||||
}).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
public PipedInputStreamWithException getDecryptedContentAsInputStream(Content content) throws IOException, CheckSumWrongException {
|
||||
PipedInputStreamWithException in = new PipedInputStreamWithException();
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
|
||||
new Thread(() -> {
|
||||
try {// Throwing it in both cases is EXTREMLY important. Otherwise it'll end in a deadlock
|
||||
decryptContentToStream(content, out);
|
||||
in.throwException(null);
|
||||
} catch (Exception e) {
|
||||
in.throwException(e);
|
||||
}
|
||||
}).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// Decrypt FSTEntry to OutputStream
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
public void decryptFSTEntryTo(String entryFullPath, OutputStream outputStream) throws IOException, CheckSumWrongException {
|
||||
FSTEntry entry = getNUSTitle().getFSTEntryByFullPath(entryFullPath);
|
||||
if (entry == null) {
|
||||
log.info("File not found");
|
||||
}
|
||||
|
||||
decryptFSTEntryToStream(entry, outputStream);
|
||||
}
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// Decrypt single FSTEntry to File
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
public void decryptFSTEntryTo(String entryFullPath, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptFSTEntryTo(false, entryFullPath, outputFolder);
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(boolean fullPath, String entryFullPath, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptFSTEntryTo(fullPath, entryFullPath, outputFolder, getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(String entryFullPath, String outputFolder, boolean skipExistingFiles) throws IOException, CheckSumWrongException {
|
||||
decryptFSTEntryTo(false, entryFullPath, outputFolder, getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(boolean fullPath, String entryFullPath, String outputFolder, boolean skipExistingFiles)
|
||||
throws IOException, CheckSumWrongException {
|
||||
FSTEntry entry = getNUSTitle().getFSTEntryByFullPath(entryFullPath);
|
||||
if (entry == null) {
|
||||
log.info("File not found");
|
||||
return;
|
||||
}
|
||||
|
||||
decryptFSTEntryTo(fullPath, entry, outputFolder, skipExistingFiles);
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(FSTEntry entry, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptFSTEntryTo(false, entry, outputFolder);
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(boolean fullPath, FSTEntry entry, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptFSTEntryTo(fullPath, entry, outputFolder, getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
|
||||
public void decryptFSTEntryTo(FSTEntry entry, String outputFolder, boolean skipExistingFiles) throws IOException, CheckSumWrongException {
|
||||
decryptFSTEntryTo(false, entry, outputFolder, getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
|
||||
/*
|
||||
* public void decryptFSTEntryTo(boolean fullPath, FSTEntry entry,String outputFolder, boolean skipExistingFiles) throws IOException{
|
||||
* decryptFSTEntry(fullPath,entry,outputFolder,skipExistingFiles); }
|
||||
*/
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// Decrypt list of FSTEntry to Files
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
public void decryptAllFSTEntriesTo(String outputFolder) throws IOException, CheckSumWrongException {
|
||||
Utils.createDir(outputFolder + File.separator + "code");
|
||||
Utils.createDir(outputFolder + File.separator + "content");
|
||||
Utils.createDir(outputFolder + File.separator + "meta");
|
||||
decryptFSTEntriesTo(true, ".*", outputFolder);
|
||||
}
|
||||
|
||||
public void decryptFSTEntriesTo(String regEx, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptFSTEntriesTo(true, regEx, outputFolder);
|
||||
}
|
||||
|
||||
public void decryptFSTEntriesTo(boolean fullPath, String regEx, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptFSTEntryListTo(fullPath, getNUSTitle().getFSTEntriesByRegEx(regEx), outputFolder);
|
||||
}
|
||||
|
||||
public void decryptFSTEntryListTo(List<FSTEntry> list, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptFSTEntryListTo(true, list, outputFolder);
|
||||
}
|
||||
|
||||
public void decryptFSTEntryListTo(boolean fullPath, List<FSTEntry> list, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
for (FSTEntry entry : list) {
|
||||
decryptFSTEntryTo(fullPath, entry, outputFolder, getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
}
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// Save decrypted contents
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
public void decryptPlainContentByID(int ID, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptPlainContent(getTMDFromNUSTitle().getContentByID(ID), outputFolder);
|
||||
}
|
||||
|
||||
public void decryptPlainContentByIndex(int index, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptPlainContent(getTMDFromNUSTitle().getContentByIndex(index), outputFolder);
|
||||
}
|
||||
|
||||
public void decryptPlainContent(Content c, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptPlainContents(new ArrayList<Content>(Arrays.asList(c)), outputFolder);
|
||||
}
|
||||
|
||||
public void decryptPlainContents(List<Content> list, String outputFolder) throws IOException, CheckSumWrongException {
|
||||
for (Content c : list) {
|
||||
decryptContentTo(c, outputFolder, getNUSTitle().isSkipExistingFiles());
|
||||
}
|
||||
}
|
||||
|
||||
public void decryptAllPlainContents(String outputFolder) throws IOException, CheckSumWrongException {
|
||||
decryptPlainContents(new ArrayList<Content>(getTMDFromNUSTitle().getAllContents().values()), outputFolder);
|
||||
}
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// Other
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
private TMD getTMDFromNUSTitle() {
|
||||
return getNUSTitle().getTMD();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.mas.jnus.lib;
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -7,41 +7,42 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.implementations.NUSDataProvider;
|
||||
import de.mas.jnus.lib.utils.FileUtils;
|
||||
import de.mas.jnus.lib.utils.Utils;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.utils.FileUtils;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class ExtractionService {
|
||||
private static Map<NUSTitle, ExtractionService> instances = new HashMap<>();
|
||||
|
||||
@Getter private final NUSTitle NUSTitle;
|
||||
|
||||
public class ExtractionService {
|
||||
private static Map<NUSTitle,ExtractionService> instances = new HashMap<>();
|
||||
|
||||
public static ExtractionService getInstance(NUSTitle nustitle) {
|
||||
if(!instances.containsKey(nustitle)){
|
||||
if (!instances.containsKey(nustitle)) {
|
||||
instances.put(nustitle, new ExtractionService(nustitle));
|
||||
}
|
||||
return instances.get(nustitle);
|
||||
}
|
||||
|
||||
@Getter @Setter private NUSTitle NUSTitle = null;
|
||||
|
||||
private ExtractionService(NUSTitle nustitle){
|
||||
setNUSTitle(nustitle);
|
||||
|
||||
private ExtractionService(NUSTitle nustitle) {
|
||||
this.NUSTitle = nustitle;
|
||||
}
|
||||
|
||||
private NUSDataProvider getDataProvider(){
|
||||
|
||||
private NUSDataProvider getDataProvider() {
|
||||
return getNUSTitle().getDataProvider();
|
||||
}
|
||||
|
||||
|
||||
public void extractAllEncrpytedContentFileHashes(String outputFolder) throws IOException {
|
||||
extractEncryptedContentHashesTo(new ArrayList<Content>(getNUSTitle().getTMD().getAllContents().values()),outputFolder);
|
||||
extractEncryptedContentHashesTo(new ArrayList<Content>(getNUSTitle().getTMD().getAllContents().values()), outputFolder);
|
||||
}
|
||||
|
||||
|
||||
public void extractEncryptedContentHashesTo(List<Content> list, String outputFolder) throws IOException {
|
||||
Utils.createDir(outputFolder);
|
||||
NUSDataProvider dataProvider = getDataProvider();
|
||||
for(Content c : list){
|
||||
for (Content c : list) {
|
||||
dataProvider.saveContentH3Hash(c, outputFolder);
|
||||
}
|
||||
}
|
||||
@ -49,22 +50,23 @@ public class ExtractionService {
|
||||
public void extractAllEncryptedContentFiles(String outputFolder) throws IOException {
|
||||
extractAllEncryptedContentFilesWithHashesTo(outputFolder);
|
||||
}
|
||||
|
||||
|
||||
public void extractAllEncryptedContentFilesWithoutHashesTo(String outputFolder) throws IOException {
|
||||
extractEncryptedContentFilesTo(new ArrayList<Content>(getNUSTitle().getTMD().getAllContents().values()),outputFolder,false);
|
||||
}
|
||||
|
||||
public void extractAllEncryptedContentFilesWithHashesTo(String outputFolder) throws IOException {
|
||||
extractEncryptedContentFilesTo(new ArrayList<Content>(getNUSTitle().getTMD().getAllContents().values()),outputFolder,true);
|
||||
extractEncryptedContentFilesTo(new ArrayList<Content>(getNUSTitle().getTMD().getAllContents().values()), outputFolder, false);
|
||||
}
|
||||
|
||||
public void extractEncryptedContentFilesTo(List<Content> list,String outputFolder,boolean withHashes) throws IOException {
|
||||
public void extractAllEncryptedContentFilesWithHashesTo(String outputFolder) throws IOException {
|
||||
extractEncryptedContentFilesTo(new ArrayList<Content>(getNUSTitle().getTMD().getAllContents().values()), outputFolder, true);
|
||||
}
|
||||
|
||||
public void extractEncryptedContentFilesTo(List<Content> list, String outputFolder, boolean withHashes) throws IOException {
|
||||
Utils.createDir(outputFolder);
|
||||
NUSDataProvider dataProvider = getDataProvider();
|
||||
for(Content c : list){
|
||||
if(withHashes){
|
||||
for (Content c : list) {
|
||||
log.info("Saving " + c.getFilename());
|
||||
if (withHashes) {
|
||||
dataProvider.saveEncryptedContentWithH3Hash(c, outputFolder);
|
||||
}else{
|
||||
} else {
|
||||
dataProvider.saveEncryptedContent(c, outputFolder);
|
||||
}
|
||||
}
|
||||
@ -72,44 +74,44 @@ public class ExtractionService {
|
||||
|
||||
public void extractTMDTo(String output) throws IOException {
|
||||
Utils.createDir(output);
|
||||
|
||||
byte[] rawTMD= getDataProvider().getRawTMD();
|
||||
|
||||
if(rawTMD != null && rawTMD.length == 0){
|
||||
System.out.println("Couldn't write TMD: No TMD loaded");
|
||||
|
||||
byte[] rawTMD = getDataProvider().getRawTMD();
|
||||
|
||||
if (rawTMD == null || rawTMD.length == 0) {
|
||||
log.info("Couldn't write TMD: No TMD loaded");
|
||||
return;
|
||||
}
|
||||
String tmd_path = output + File.separator + Settings.TMD_FILENAME;
|
||||
System.out.println("Extracting TMD to: " + tmd_path);
|
||||
FileUtils.saveByteArrayToFile(tmd_path,rawTMD);
|
||||
log.info("Extracting TMD to: " + tmd_path);
|
||||
FileUtils.saveByteArrayToFile(tmd_path, rawTMD);
|
||||
}
|
||||
|
||||
public void extractTicketTo(String output) throws IOException {
|
||||
Utils.createDir(output);
|
||||
|
||||
byte[] rawTicket= getDataProvider().getRawTicket();
|
||||
|
||||
if(rawTicket != null && rawTicket.length == 0){
|
||||
System.out.println("Couldn't write Ticket: No Ticket loaded");
|
||||
|
||||
byte[] rawTicket = getDataProvider().getRawTicket();
|
||||
|
||||
if (rawTicket == null || rawTicket.length == 0) {
|
||||
log.info("Couldn't write Ticket: No Ticket loaded");
|
||||
return;
|
||||
}
|
||||
String ticket_path = output + File.separator + Settings.TICKET_FILENAME;
|
||||
System.out.println("Extracting Ticket to: " + ticket_path);
|
||||
FileUtils.saveByteArrayToFile(ticket_path,rawTicket);
|
||||
log.info("Extracting Ticket to: " + ticket_path);
|
||||
FileUtils.saveByteArrayToFile(ticket_path, rawTicket);
|
||||
}
|
||||
|
||||
|
||||
public void extractCertTo(String output) throws IOException {
|
||||
Utils.createDir(output);
|
||||
|
||||
byte[] rawCert = getDataProvider().getRawCert();
|
||||
|
||||
if(rawCert != null && rawCert.length == 0){
|
||||
System.out.println("Couldn't write Cert: No Cert loaded");
|
||||
|
||||
byte[] rawCert = getDataProvider().getRawCert();
|
||||
|
||||
if (rawCert == null || rawCert.length == 0) {
|
||||
log.info("Couldn't write Cert: No Cert loaded");
|
||||
return;
|
||||
}
|
||||
String cert_path = output + File.separator + Settings.CERT_FILENAME;
|
||||
System.out.println("Extracting Cert to: " + cert_path);
|
||||
FileUtils.saveByteArrayToFile(cert_path,rawCert);
|
||||
log.info("Extracting Cert to: " + cert_path);
|
||||
FileUtils.saveByteArrayToFile(cert_path, rawCert);
|
||||
}
|
||||
|
||||
public void extractAll(String outputFolder) throws IOException {
|
||||
@ -118,7 +120,7 @@ public class ExtractionService {
|
||||
extractCertTo(outputFolder);
|
||||
extractTMDTo(outputFolder);
|
||||
extractTicketTo(outputFolder);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
7
src/de/mas/wiiu/jnus/InputStreamWithException.java
Normal file
7
src/de/mas/wiiu/jnus/InputStreamWithException.java
Normal file
@ -0,0 +1,7 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface InputStreamWithException extends Closeable {
|
||||
public void checkForException() throws Exception;
|
||||
}
|
123
src/de/mas/wiiu/jnus/NUSTitle.java
Normal file
123
src/de/mas/wiiu/jnus/NUSTitle.java
Normal file
@ -0,0 +1,123 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.TMD;
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
|
||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class NUSTitle {
|
||||
@Getter @Setter private FST FST;
|
||||
@Getter @Setter private TMD TMD;
|
||||
@Getter @Setter private Ticket ticket;
|
||||
|
||||
@Getter @Setter private boolean skipExistingFiles = true;
|
||||
@Getter @Setter private NUSDataProvider dataProvider = null;
|
||||
|
||||
public List<FSTEntry> getAllFSTEntriesFlatByContentID(short ID) {
|
||||
return getFSTEntriesFlatByContent(getTMD().getContentByID((int) ID));
|
||||
}
|
||||
|
||||
public List<FSTEntry> getFSTEntriesFlatByContentIndex(int index) {
|
||||
return getFSTEntriesFlatByContent(getTMD().getContentByIndex(index));
|
||||
}
|
||||
|
||||
public List<FSTEntry> getFSTEntriesFlatByContent(Content content) {
|
||||
return getFSTEntriesFlatByContents(new ArrayList<Content>(Arrays.asList(content)));
|
||||
}
|
||||
|
||||
public List<FSTEntry> getFSTEntriesFlatByContents(List<Content> list) {
|
||||
List<FSTEntry> entries = new ArrayList<>();
|
||||
for (Content c : list) {
|
||||
for (FSTEntry f : c.getEntries()) {
|
||||
entries.add(f);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
public List<FSTEntry> getAllFSTEntriesFlat() {
|
||||
return getFSTEntriesFlatByContents(new ArrayList<Content>(getTMD().getAllContents().values()));
|
||||
}
|
||||
|
||||
public FSTEntry getFSTEntryByFullPath(String givenFullPath) {
|
||||
String fullPath = givenFullPath.replaceAll("/", "\\\\");
|
||||
if (!fullPath.startsWith("\\")) fullPath = "\\" + fullPath;
|
||||
for (FSTEntry f : getAllFSTEntriesFlat()) {
|
||||
if (f.getFullPath().equals(fullPath)) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<FSTEntry> getFSTEntriesByRegEx(String regEx) {
|
||||
List<FSTEntry> files = getAllFSTEntriesFlat();
|
||||
Pattern p = Pattern.compile(regEx);
|
||||
|
||||
List<FSTEntry> result = new ArrayList<>();
|
||||
|
||||
for (FSTEntry f : files) {
|
||||
String match = f.getFullPath().replaceAll("\\\\", "/");
|
||||
Matcher m = p.matcher(match);
|
||||
if (m.matches()) {
|
||||
result.add(f);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void printFiles() {
|
||||
getFST().getRoot().printRecursive(0);
|
||||
}
|
||||
|
||||
public void printContentFSTInfos() {
|
||||
for (Entry<Integer, ContentFSTInfo> e : getFST().getContentFSTInfos().entrySet()) {
|
||||
System.out.println(String.format("%08X", e.getKey()) + ": " + e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public void printContentInfos() {
|
||||
for (Entry<Integer, Content> e : getTMD().getAllContents().entrySet()) {
|
||||
|
||||
System.out.println(String.format("%08X", e.getKey()) + ": " + e.getValue());
|
||||
System.out.println(e.getValue().getContentFSTInfo());
|
||||
for (FSTEntry entry : e.getValue().getEntries()) {
|
||||
System.out.println(entry.getFullPath() + String.format(" size: %016X", entry.getFileSize())
|
||||
+ String.format(" offset: %016X", entry.getFileOffset()) + String.format(" flags: %04X", entry.getFlags()));
|
||||
}
|
||||
System.out.println("-");
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanup() throws IOException {
|
||||
if (getDataProvider() != null) {
|
||||
getDataProvider().cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public void printDetailedData() {
|
||||
printFiles();
|
||||
printContentFSTInfos();
|
||||
printContentInfos();
|
||||
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NUSTitle [dataProvider=" + dataProvider + "]";
|
||||
}
|
||||
}
|
18
src/de/mas/wiiu/jnus/NUSTitleConfig.java
Normal file
18
src/de/mas/wiiu/jnus/NUSTitleConfig.java
Normal file
@ -0,0 +1,18 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
|
||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfo;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class NUSTitleConfig {
|
||||
private String inputPath;
|
||||
private WUDInfo WUDInfo;
|
||||
private Ticket ticket;
|
||||
|
||||
private int version = Settings.LATEST_TMD_VERSION;
|
||||
private long titleID = 0x0L;
|
||||
|
||||
private WoomyInfo woomyInfo;
|
||||
}
|
66
src/de/mas/wiiu/jnus/NUSTitleLoader.java
Normal file
66
src/de/mas/wiiu/jnus/NUSTitleLoader.java
Normal file
@ -0,0 +1,66 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.TMD;
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
abstract class NUSTitleLoader {
|
||||
protected NUSTitleLoader() {
|
||||
// should be empty
|
||||
}
|
||||
|
||||
public NUSTitle loadNusTitle(NUSTitleConfig config) throws Exception {
|
||||
NUSTitle result = new NUSTitle();
|
||||
|
||||
NUSDataProvider dataProvider = getDataProvider(result, config);
|
||||
result.setDataProvider(dataProvider);
|
||||
|
||||
TMD tmd = TMD.parseTMD(dataProvider.getRawTMD());
|
||||
result.setTMD(tmd);
|
||||
|
||||
if (tmd == null) {
|
||||
log.info("TMD not found.");
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
Ticket ticket = config.getTicket();
|
||||
if (ticket == null) {
|
||||
ticket = Ticket.parseTicket(dataProvider.getRawTicket());
|
||||
}
|
||||
result.setTicket(ticket);
|
||||
// System.out.println(ticket);
|
||||
|
||||
Content fstContent = tmd.getContentByIndex(0);
|
||||
|
||||
InputStream fstContentEncryptedStream = dataProvider.getInputStreamFromContent(fstContent, 0);
|
||||
if (fstContentEncryptedStream == null) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] fstBytes = StreamUtils.getBytesFromStream(fstContentEncryptedStream, (int) fstContent.getEncryptedFileSize());
|
||||
|
||||
if (fstContent.isEncrypted()) {
|
||||
AESDecryption aesDecryption = new AESDecryption(ticket.getDecryptedKey(), new byte[0x10]);
|
||||
fstBytes = aesDecryption.decrypt(fstBytes);
|
||||
}
|
||||
|
||||
Map<Integer, Content> contents = tmd.getAllContents();
|
||||
|
||||
FST fst = FST.parseFST(fstBytes, contents);
|
||||
result.setFST(fst);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected abstract NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config);
|
||||
}
|
34
src/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java
Normal file
34
src/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java
Normal file
@ -0,0 +1,34 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocal;
|
||||
|
||||
public final class NUSTitleLoaderLocal extends NUSTitleLoader {
|
||||
|
||||
private NUSTitleLoaderLocal() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(String inputPath) throws Exception {
|
||||
return loadNUSTitle(inputPath, null);
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(String inputPath, Ticket ticket) throws Exception {
|
||||
NUSTitleLoader loader = new NUSTitleLoaderLocal();
|
||||
NUSTitleConfig config = new NUSTitleConfig();
|
||||
|
||||
if (ticket != null) {
|
||||
config.setTicket(ticket);
|
||||
}
|
||||
config.setInputPath(inputPath);
|
||||
|
||||
return loader.loadNusTitle(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) {
|
||||
return new NUSDataProviderLocal(title, config.getInputPath());
|
||||
}
|
||||
|
||||
}
|
41
src/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java
Normal file
41
src/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java
Normal file
@ -0,0 +1,41 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote;
|
||||
|
||||
public final class NUSTitleLoaderRemote extends NUSTitleLoader {
|
||||
|
||||
private NUSTitleLoaderRemote() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(long titleID) throws Exception {
|
||||
return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION, null);
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(long titleID, int version) throws Exception {
|
||||
return loadNUSTitle(titleID, version, null);
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(long titleID, Ticket ticket) throws Exception {
|
||||
return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION, ticket);
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(long titleID, int version, Ticket ticket) throws Exception {
|
||||
NUSTitleLoader loader = new NUSTitleLoaderRemote();
|
||||
NUSTitleConfig config = new NUSTitleConfig();
|
||||
|
||||
config.setVersion(version);
|
||||
config.setTitleID(titleID);
|
||||
config.setTicket(ticket);
|
||||
|
||||
return loader.loadNusTitle(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) {
|
||||
return new NUSDataProviderRemote(title, config.getVersion(), config.getTitleID());
|
||||
}
|
||||
|
||||
}
|
57
src/de/mas/wiiu/jnus/NUSTitleLoaderWUD.java
Normal file
57
src/de/mas/wiiu/jnus/NUSTitleLoaderWUD.java
Normal file
@ -0,0 +1,57 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD;
|
||||
import de.mas.wiiu.jnus.implementations.wud.WUDImage;
|
||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfo;
|
||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfoParser;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class NUSTitleLoaderWUD extends NUSTitleLoader {
|
||||
|
||||
private NUSTitleLoaderWUD() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(String WUDPath) throws Exception {
|
||||
return loadNUSTitle(WUDPath, null);
|
||||
}
|
||||
|
||||
public static NUSTitle loadNUSTitle(String WUDPath, byte[] titleKey) throws Exception {
|
||||
NUSTitleLoader loader = new NUSTitleLoaderWUD();
|
||||
NUSTitleConfig config = new NUSTitleConfig();
|
||||
byte[] usedTitleKey = titleKey;
|
||||
File wudFile = new File(WUDPath);
|
||||
if (!wudFile.exists()) {
|
||||
log.info(WUDPath + " does not exist.");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
WUDImage image = new WUDImage(wudFile);
|
||||
if (usedTitleKey == null) {
|
||||
File keyFile = new File(wudFile.getParentFile().getPath() + File.separator + Settings.WUD_KEY_FILENAME);
|
||||
if (!keyFile.exists()) {
|
||||
log.info(keyFile.getAbsolutePath() + " does not exist and no title key was provided.");
|
||||
return null;
|
||||
}
|
||||
usedTitleKey = Files.readAllBytes(keyFile.toPath());
|
||||
}
|
||||
WUDInfo wudInfo = WUDInfoParser.createAndLoad(image.getWUDDiscReader(), usedTitleKey);
|
||||
if (wudInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
config.setWUDInfo(wudInfo);
|
||||
|
||||
return loader.loadNusTitle(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) {
|
||||
return new NUSDataProviderWUD(title, config.getWUDInfo());
|
||||
}
|
||||
}
|
32
src/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java
Normal file
32
src/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java
Normal file
@ -0,0 +1,32 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderWoomy;
|
||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
|
||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyParser;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class NUSTitleLoaderWoomy extends NUSTitleLoader {
|
||||
|
||||
public static NUSTitle loadNUSTitle(String inputFile) throws Exception {
|
||||
NUSTitleLoaderWoomy loader = new NUSTitleLoaderWoomy();
|
||||
NUSTitleConfig config = new NUSTitleConfig();
|
||||
|
||||
WoomyInfo woomyInfo = WoomyParser.createWoomyInfo(new File(inputFile));
|
||||
if (woomyInfo == null) {
|
||||
log.info("Created woomy is null.");
|
||||
return null;
|
||||
}
|
||||
config.setWoomyInfo(woomyInfo);
|
||||
return loader.loadNusTitle(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) {
|
||||
return new NUSDataProviderWoomy(title, config.getWoomyInfo());
|
||||
}
|
||||
|
||||
}
|
64
src/de/mas/wiiu/jnus/PipedInputStreamWithException.java
Normal file
64
src/de/mas/wiiu/jnus/PipedInputStreamWithException.java
Normal file
@ -0,0 +1,64 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PipedInputStream;
|
||||
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
|
||||
public class PipedInputStreamWithException extends PipedInputStream implements InputStreamWithException {
|
||||
private Exception e = null;
|
||||
private boolean exceptionSet = false;
|
||||
private boolean closed = false;
|
||||
private Object lock = new Object();
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
synchronized (lock) {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void throwException(Exception e) {
|
||||
synchronized (lock) {
|
||||
exceptionSet = true;
|
||||
this.e = e;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
boolean isClosed = false;
|
||||
synchronized (lock) {
|
||||
isClosed = closed;
|
||||
}
|
||||
return isClosed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkForException() throws Exception {
|
||||
if (isClosed()) {
|
||||
boolean waiting = true;
|
||||
int tries = 0;
|
||||
while (waiting) {
|
||||
synchronized (lock) {
|
||||
waiting = !exceptionSet;
|
||||
}
|
||||
if (waiting) {
|
||||
Utils.sleep(10);
|
||||
}
|
||||
if (tries > 100) {
|
||||
// TODO: warning?
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
synchronized (lock) {
|
||||
if (e != null) {
|
||||
Exception tmp = e;
|
||||
e = null;
|
||||
exceptionSet = true;
|
||||
throw tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package de.mas.jnus.lib;
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
public class Settings {
|
||||
public static String URL_BASE = "http://ccs.cdn.wup.shop.nintendo.net/ccs/download";
|
||||
public static String URL_BASE = "http://ccs.cdn.c.shop.nintendowifi.net/ccs/download";
|
||||
public static final int LATEST_TMD_VERSION = 0;
|
||||
public static final String TMD_FILENAME = "title.tmd";
|
||||
public static final String TICKET_FILENAME = "title.tik";
|
251
src/de/mas/wiiu/jnus/WUDService.java
Normal file
251
src/de/mas/wiiu/jnus/WUDService.java
Normal file
@ -0,0 +1,251 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TreeMap;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Checksum;
|
||||
|
||||
import de.mas.wiiu.jnus.implementations.wud.WUDImage;
|
||||
import de.mas.wiiu.jnus.implementations.wud.WUDImageCompressedInfo;
|
||||
import de.mas.wiiu.jnus.utils.ByteArrayBuffer;
|
||||
import de.mas.wiiu.jnus.utils.ByteArrayWrapper;
|
||||
import de.mas.wiiu.jnus.utils.HashResult;
|
||||
import de.mas.wiiu.jnus.utils.HashUtil;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class WUDService {
|
||||
private WUDService() {
|
||||
// Just an utility class
|
||||
}
|
||||
|
||||
public static File compressWUDToWUX(WUDImage image, String outputFolder) throws IOException {
|
||||
return compressWUDToWUX(image, outputFolder, "game.wux", false);
|
||||
}
|
||||
|
||||
public static File compressWUDToWUX(WUDImage image, String outputFolder, boolean overwrite) throws IOException {
|
||||
return compressWUDToWUX(image, outputFolder, "game.wux", overwrite);
|
||||
}
|
||||
|
||||
public static File compressWUDToWUX(WUDImage image, String outputFolder, String filename, boolean overwrite) throws IOException {
|
||||
if (image.isCompressed()) {
|
||||
log.info("Given image is already compressed");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (image.getWUDFileSize() != WUDImage.WUD_FILESIZE) {
|
||||
log.info("Given WUD has not the expected filesize");
|
||||
return null;
|
||||
}
|
||||
|
||||
String usedOutputFolder = outputFolder;
|
||||
if (usedOutputFolder == null) usedOutputFolder = "";
|
||||
Utils.createDir(usedOutputFolder);
|
||||
|
||||
String filePath;
|
||||
if (usedOutputFolder.isEmpty()) {
|
||||
filePath = filename;
|
||||
} else {
|
||||
filePath = usedOutputFolder + File.separator + filename;
|
||||
}
|
||||
|
||||
File outputFile = new File(filePath);
|
||||
|
||||
if (outputFile.exists() && !overwrite) {
|
||||
log.info("Couldn't compress wud, target file already exists (" + outputFile.getAbsolutePath() + ")");
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info("Writing compressed file to: " + outputFile.getAbsolutePath());
|
||||
RandomAccessFile fileOutput = new RandomAccessFile(outputFile, "rw");
|
||||
|
||||
WUDImageCompressedInfo info = WUDImageCompressedInfo.getDefaultCompressedInfo();
|
||||
|
||||
byte[] header = info.getHeaderAsBytes();
|
||||
log.info("Writing header");
|
||||
fileOutput.write(header);
|
||||
|
||||
int sectorTableEntryCount = (int) ((image.getWUDFileSize() + WUDImageCompressedInfo.SECTOR_SIZE - 1) / (long) WUDImageCompressedInfo.SECTOR_SIZE);
|
||||
|
||||
long sectorTableStart = fileOutput.getFilePointer();
|
||||
long sectorTableEnd = Utils.align(sectorTableEntryCount * 0x04, WUDImageCompressedInfo.SECTOR_SIZE);
|
||||
byte[] sectorTablePlaceHolder = new byte[(int) (sectorTableEnd - sectorTableStart)];
|
||||
|
||||
fileOutput.write(sectorTablePlaceHolder);
|
||||
|
||||
Map<ByteArrayWrapper, Integer> sectorHashes = new HashMap<>();
|
||||
Map<Integer, Integer> sectorMapping = new TreeMap<>();
|
||||
|
||||
InputStream in = image.getWUDDiscReader().readEncryptedToInputStream(0, image.getWUDFileSize());
|
||||
|
||||
int bufferSize = WUDImageCompressedInfo.SECTOR_SIZE;
|
||||
byte[] blockBuffer = new byte[bufferSize];
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(bufferSize);
|
||||
|
||||
long written = 0;
|
||||
int curSector = 0;
|
||||
int realSector = 0;
|
||||
|
||||
log.info("Writing sectors");
|
||||
Integer oldOffset = null;
|
||||
do {
|
||||
int read = StreamUtils.getChunkFromStream(in, blockBuffer, overflow, bufferSize);
|
||||
ByteArrayWrapper hash = new ByteArrayWrapper(HashUtil.hashSHA1(blockBuffer));
|
||||
|
||||
if ((oldOffset = sectorHashes.get(hash)) == null) {
|
||||
sectorMapping.put(curSector, realSector);
|
||||
sectorHashes.put(hash, realSector);
|
||||
fileOutput.write(blockBuffer);
|
||||
realSector++;
|
||||
} else {
|
||||
sectorMapping.put(curSector, oldOffset);
|
||||
oldOffset = null;
|
||||
}
|
||||
|
||||
written += read;
|
||||
curSector++;
|
||||
if (curSector % 10 == 0) {
|
||||
double readMB = written / 1024.0 / 1024.0;
|
||||
double writtenMB = ((long) realSector * (long) bufferSize) / 1024.0 / 1024.0;
|
||||
double percent = ((double) written / image.getWUDFileSize()) * 100;
|
||||
double ratio = 1 / (writtenMB / readMB);
|
||||
System.out.print(String.format(Locale.ROOT, "\rCompressing into .wux | Progress %.2f%% | Ratio: 1:%.2f | Read: %.2fMB | Written: %.2fMB\t",
|
||||
percent, ratio, readMB, writtenMB));
|
||||
}
|
||||
} while (written < image.getWUDFileSize());
|
||||
System.out.println();
|
||||
System.out.println("Sectors compressed.");
|
||||
log.info("Writing sector table");
|
||||
fileOutput.seek(sectorTableStart);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(sectorTablePlaceHolder.length);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (Entry<Integer, Integer> e : sectorMapping.entrySet()) {
|
||||
buffer.putInt(e.getValue());
|
||||
}
|
||||
|
||||
fileOutput.write(buffer.array());
|
||||
fileOutput.close();
|
||||
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
public static boolean compareWUDImage(WUDImage firstImage, WUDImage secondImage) throws IOException {
|
||||
if (firstImage.getWUDFileSize() != secondImage.getWUDFileSize()) {
|
||||
log.info("Filesize is different");
|
||||
return false;
|
||||
}
|
||||
InputStream in1 = firstImage.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE);
|
||||
InputStream in2 = secondImage.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE);
|
||||
|
||||
boolean result = true;
|
||||
int bufferSize = 1024 * 1024 + 19;
|
||||
long totalread = 0;
|
||||
byte[] blockBuffer1 = new byte[bufferSize];
|
||||
byte[] blockBuffer2 = new byte[bufferSize];
|
||||
ByteArrayBuffer overflow1 = new ByteArrayBuffer(bufferSize);
|
||||
ByteArrayBuffer overflow2 = new ByteArrayBuffer(bufferSize);
|
||||
long curSector = 0;
|
||||
do {
|
||||
int read1 = StreamUtils.getChunkFromStream(in1, blockBuffer1, overflow1, bufferSize);
|
||||
int read2 = StreamUtils.getChunkFromStream(in2, blockBuffer2, overflow2, bufferSize);
|
||||
if (read1 != read2) {
|
||||
log.info("Verification error");
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!Arrays.equals(blockBuffer1, blockBuffer2)) {
|
||||
log.info("Verification error");
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
|
||||
totalread += read1;
|
||||
|
||||
curSector++;
|
||||
if (curSector % 1 == 0) {
|
||||
double readMB = totalread / 1024.0 / 1024.0;
|
||||
double percent = ((double) totalread / WUDImage.WUD_FILESIZE) * 100;
|
||||
System.out.print(String.format("\rVerification: %.2fMB done (%.2f%%)", readMB, percent));
|
||||
}
|
||||
} while (totalread < WUDImage.WUD_FILESIZE);
|
||||
System.out.println();
|
||||
System.out.print("Verfication done!");
|
||||
in1.close();
|
||||
in2.close();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static HashResult hashWUDImage(WUDImage image) throws IOException {
|
||||
if (image == null) {
|
||||
log.info("Failed to calculate the hash of the given image: input was null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (image.isCompressed()) {
|
||||
log.info("The input file is compressed. The calculated hash is the hash of the corresponding .wud file, not this .wux!");
|
||||
} else if (image.isSplitted()) {
|
||||
log.info("The input file is splitted. The calculated hash is the hash of the corresponding .wud file, not this splitted .wud");
|
||||
}
|
||||
|
||||
InputStream in = image.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE);
|
||||
|
||||
int bufferSize = 1024 * 1024 * 10;
|
||||
long totalread = 0;
|
||||
byte[] blockBuffer1 = new byte[bufferSize];
|
||||
ByteArrayBuffer overflow1 = new ByteArrayBuffer(bufferSize);
|
||||
long curSector = 0;
|
||||
|
||||
MessageDigest sha1 = null;
|
||||
MessageDigest md5 = null;
|
||||
Checksum checksumEngine = new CRC32();
|
||||
|
||||
try {
|
||||
sha1 = MessageDigest.getInstance("SHA1");
|
||||
md5 = MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
do {
|
||||
int read1 = StreamUtils.getChunkFromStream(in, blockBuffer1, overflow1, bufferSize);
|
||||
sha1.update(blockBuffer1, 0, read1);
|
||||
md5.update(blockBuffer1, 0, read1);
|
||||
checksumEngine.update(blockBuffer1, 0, read1);
|
||||
|
||||
totalread += read1;
|
||||
|
||||
curSector++;
|
||||
if (curSector % 10 == 0) {
|
||||
double readMB = totalread / 1024.0 / 1024.0;
|
||||
double percent = ((double) totalread / WUDImage.WUD_FILESIZE) * 100;
|
||||
System.out.print(String.format("\rHashing: %.2fMB done (%.2f%%)", readMB, percent));
|
||||
}
|
||||
} while (totalread < WUDImage.WUD_FILESIZE);
|
||||
double readMB = totalread / 1024.0 / 1024.0;
|
||||
double percent = ((double) totalread / WUDImage.WUD_FILESIZE) * 100;
|
||||
|
||||
System.out.println(String.format("\rHashing: %.2fMB done (%.2f%%)", readMB, percent));
|
||||
|
||||
HashResult result = new HashResult(sha1.digest(), md5.digest(), Utils.StringToByteArray(Long.toHexString(checksumEngine.getValue())));
|
||||
|
||||
in.close();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
226
src/de/mas/wiiu/jnus/entities/TMD.java
Normal file
226
src/de/mas/wiiu/jnus/entities/TMD.java
Normal file
@ -0,0 +1,226 @@
|
||||
package de.mas.wiiu.jnus.entities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.content.ContentInfo;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class TMD {
|
||||
private static final int SIGNATURE_LENGTH = 0x100;
|
||||
private static final int ISSUER_LENGTH = 0x40;
|
||||
private static final int RESERVED_LENGTH = 0x3E;
|
||||
private static final int SHA2_LENGTH = 0x20;
|
||||
|
||||
private static final int POSITION_SIGNATURE = 0x04;
|
||||
private static final int POSITION_ISSUER = 0x140;
|
||||
private static final int POSITION_RESERVED = 0x19A;
|
||||
private static final int POSITION_SHA2 = 0x1E4;
|
||||
|
||||
private static final int CONTENT_INFO_ARRAY_SIZE = 0x40;
|
||||
|
||||
private static final int CONTENT_INFO_OFFSET = 0x204;
|
||||
private static final int CONTENT_OFFSET = 0xB04;
|
||||
|
||||
@Getter private final int signatureType; // 0x000
|
||||
@Getter private final byte[] signature; // 0x004
|
||||
@Getter private final byte[] issuer; // 0x140
|
||||
@Getter private final byte version; // 0x180
|
||||
@Getter private final byte CACRLVersion; // 0x181
|
||||
@Getter private final byte signerCRLVersion; // 0x182
|
||||
@Getter private final long systemVersion; // 0x184
|
||||
@Getter private final long titleID; // 0x18C
|
||||
@Getter private final int titleType; // 0x194
|
||||
@Getter private final short groupID; // 0x198
|
||||
@Getter private final byte[] reserved; // 0x19A
|
||||
@Getter private final int accessRights; // 0x1D8
|
||||
@Getter private final short titleVersion; // 0x1DC
|
||||
@Getter private final short contentCount; // 0x1DE
|
||||
@Getter private final short bootIndex; // 0x1E0
|
||||
@Getter private final byte[] SHA2; // 0x1E4
|
||||
@Getter private final ContentInfo[] contentInfos;
|
||||
private final Map<Integer, Content> contentToIndex = new HashMap<>();
|
||||
private final Map<Integer, Content> contentToID = new HashMap<>();
|
||||
|
||||
private TMD(TMDParam param) {
|
||||
super();
|
||||
this.signatureType = param.getSignatureType();
|
||||
this.signature = param.getSignature();
|
||||
this.issuer = param.getIssuer();
|
||||
this.version = param.getVersion();
|
||||
this.CACRLVersion = param.getCACRLVersion();
|
||||
this.signerCRLVersion = param.getSignerCRLVersion();
|
||||
this.systemVersion = param.getSystemVersion();
|
||||
this.titleID = param.getTitleID();
|
||||
this.titleType = param.getTitleType();
|
||||
this.groupID = param.getGroupID();
|
||||
this.reserved = param.getReserved();
|
||||
this.accessRights = param.getAccessRights();
|
||||
this.titleVersion = param.getTitleVersion();
|
||||
this.contentCount = param.getContentCount();
|
||||
this.bootIndex = param.getBootIndex();
|
||||
this.SHA2 = param.getSHA2();
|
||||
this.contentInfos = param.getContentInfos();
|
||||
}
|
||||
|
||||
public static TMD parseTMD(File tmd) throws IOException {
|
||||
if (tmd == null || !tmd.exists()) {
|
||||
log.info("TMD input file null or doesn't exist.");
|
||||
return null;
|
||||
}
|
||||
return parseTMD(Files.readAllBytes(tmd.toPath()));
|
||||
}
|
||||
|
||||
public static TMD parseTMD(byte[] input) {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
byte[] issuer = new byte[ISSUER_LENGTH];
|
||||
byte[] reserved = new byte[RESERVED_LENGTH];
|
||||
byte[] SHA2 = new byte[SHA2_LENGTH];
|
||||
|
||||
ContentInfo[] contentInfos = new ContentInfo[CONTENT_INFO_ARRAY_SIZE];
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(input.length);
|
||||
buffer.put(input);
|
||||
|
||||
// Get Signature
|
||||
buffer.position(0);
|
||||
int signatureType = buffer.getInt();
|
||||
buffer.position(POSITION_SIGNATURE);
|
||||
buffer.get(signature, 0, SIGNATURE_LENGTH);
|
||||
|
||||
// Get Issuer
|
||||
buffer.position(POSITION_ISSUER);
|
||||
buffer.get(issuer, 0, ISSUER_LENGTH);
|
||||
|
||||
// Get CACRLVersion and signerCRLVersion
|
||||
buffer.position(0x180);
|
||||
byte version = buffer.get();
|
||||
byte CACRLVersion = buffer.get();
|
||||
byte signerCRLVersion = buffer.get();
|
||||
|
||||
// Get title information
|
||||
buffer.position(0x184);
|
||||
long systemVersion = buffer.getLong();
|
||||
long titleID = buffer.getLong();
|
||||
int titleType = buffer.getInt();
|
||||
short groupID = buffer.getShort();
|
||||
|
||||
// Get other information
|
||||
buffer.position(POSITION_RESERVED);
|
||||
buffer.get(reserved, 0, RESERVED_LENGTH);
|
||||
|
||||
// Get accessRights,titleVersion,contentCount,bootIndex
|
||||
buffer.position(0x1D8);
|
||||
int accessRights = buffer.getInt();
|
||||
short titleVersion = buffer.getShort();
|
||||
short contentCount = buffer.getShort();
|
||||
short bootIndex = buffer.getShort();
|
||||
|
||||
// Get hash
|
||||
buffer.position(POSITION_SHA2);
|
||||
buffer.get(SHA2, 0, SHA2_LENGTH);
|
||||
|
||||
// Get contentInfos
|
||||
buffer.position(CONTENT_INFO_OFFSET);
|
||||
for (int i = 0; i < CONTENT_INFO_ARRAY_SIZE; i++) {
|
||||
byte[] contentInfo = new byte[ContentInfo.CONTENT_INFO_SIZE];
|
||||
buffer.get(contentInfo, 0, ContentInfo.CONTENT_INFO_SIZE);
|
||||
contentInfos[i] = ContentInfo.parseContentInfo(contentInfo);
|
||||
}
|
||||
|
||||
TMDParam param = new TMDParam();
|
||||
param.setSignatureType(signatureType);
|
||||
param.setSignature(signature);
|
||||
param.setVersion(version);
|
||||
param.setCACRLVersion(CACRLVersion);
|
||||
param.setSignerCRLVersion(signerCRLVersion);
|
||||
param.setSystemVersion(systemVersion);
|
||||
param.setTitleID(titleID);
|
||||
param.setTitleType(titleType);
|
||||
param.setGroupID(groupID);
|
||||
param.setAccessRights(accessRights);
|
||||
param.setTitleVersion(titleVersion);
|
||||
param.setContentCount(contentCount);
|
||||
param.setBootIndex(bootIndex);
|
||||
param.setSHA2(SHA2);
|
||||
param.setContentInfos(contentInfos);
|
||||
|
||||
TMD result = new TMD(param);
|
||||
|
||||
// Get Contents
|
||||
for (int i = 0; i < contentCount; i++) {
|
||||
buffer.position(CONTENT_OFFSET + (Content.CONTENT_SIZE * i));
|
||||
byte[] content = new byte[Content.CONTENT_SIZE];
|
||||
buffer.get(content, 0, Content.CONTENT_SIZE);
|
||||
Content c = Content.parseContent(content);
|
||||
result.setContentToIndex(c.getIndex(), c);
|
||||
result.setContentToID(c.getID(), c);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Content getContentByIndex(int index) {
|
||||
return contentToIndex.get(index);
|
||||
}
|
||||
|
||||
private void setContentToIndex(int index, Content content) {
|
||||
contentToIndex.put(index, content);
|
||||
}
|
||||
|
||||
public Content getContentByID(int id) {
|
||||
return contentToID.get(id);
|
||||
}
|
||||
|
||||
private void setContentToID(int id, Content content) {
|
||||
contentToID.put(id, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all contents mapped by index
|
||||
*
|
||||
* @return Map of Content, index/content pairs
|
||||
*/
|
||||
public Map<Integer, Content> getAllContents() {
|
||||
return contentToIndex;
|
||||
}
|
||||
|
||||
public void printContents() {
|
||||
long totalSize = 0;
|
||||
for (Content c : contentToIndex.values()) {
|
||||
totalSize += c.getEncryptedFileSize();
|
||||
System.out.println(c);
|
||||
}
|
||||
System.out.println("Total size: " + totalSize);
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TMDParam {
|
||||
private int signatureType; // 0x000
|
||||
private byte[] signature; // 0x004
|
||||
private byte[] issuer; // 0x140
|
||||
private byte version; // 0x180
|
||||
private byte CACRLVersion; // 0x181
|
||||
private byte signerCRLVersion; // 0x182
|
||||
private long systemVersion; // 0x184
|
||||
private long titleID; // 0x18C
|
||||
private int titleType; // 0x194
|
||||
private short groupID; // 0x198
|
||||
private byte[] reserved; // 0x19A
|
||||
private int accessRights; // 0x1D8
|
||||
private short titleVersion; // 0x1DC
|
||||
private short contentCount; // 0x1DE
|
||||
private short bootIndex; // 0x1E0
|
||||
private byte[] SHA2; // 0x1E4
|
||||
private ContentInfo[] contentInfos; //
|
||||
}
|
||||
}
|
95
src/de/mas/wiiu/jnus/entities/Ticket.java
Normal file
95
src/de/mas/wiiu/jnus/entities/Ticket.java
Normal file
@ -0,0 +1,95 @@
|
||||
package de.mas.wiiu.jnus.entities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class Ticket {
|
||||
private static final int POSITION_KEY = 0x1BF;
|
||||
private static final int POSITION_TITLEID = 0x1DC;
|
||||
|
||||
@Getter private final byte[] encryptedKey;
|
||||
@Getter private final byte[] decryptedKey;
|
||||
|
||||
@Getter private final byte[] IV;
|
||||
|
||||
private Ticket(byte[] encryptedKey, byte[] decryptedKey, byte[] IV) {
|
||||
this.encryptedKey = encryptedKey;
|
||||
this.decryptedKey = decryptedKey;
|
||||
this.IV = IV;
|
||||
}
|
||||
|
||||
public static Ticket parseTicket(File ticket) throws IOException {
|
||||
if (ticket == null || !ticket.exists()) {
|
||||
log.warning("Ticket input file null or doesn't exist.");
|
||||
return null;
|
||||
}
|
||||
return parseTicket(Files.readAllBytes(ticket.toPath()));
|
||||
}
|
||||
|
||||
public static Ticket parseTicket(byte[] ticket) throws IOException {
|
||||
if (ticket == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(ticket.length);
|
||||
buffer.put(ticket);
|
||||
|
||||
// read key
|
||||
byte[] encryptedKey = new byte[0x10];
|
||||
buffer.position(POSITION_KEY);
|
||||
buffer.get(encryptedKey, 0x00, 0x10);
|
||||
|
||||
// read titleID
|
||||
buffer.position(POSITION_TITLEID);
|
||||
long titleID = buffer.getLong();
|
||||
|
||||
Ticket result = createTicket(encryptedKey, titleID);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Ticket createTicket(byte[] encryptedKey, long titleID) {
|
||||
byte[] IV = ByteBuffer.allocate(0x10).putLong(titleID).array();
|
||||
byte[] decryptedKey = calculateDecryptedKey(encryptedKey, IV);
|
||||
|
||||
return new Ticket(encryptedKey, decryptedKey, IV);
|
||||
}
|
||||
|
||||
private static byte[] calculateDecryptedKey(byte[] encryptedKey, byte[] IV) {
|
||||
AESDecryption decryption = new AESDecryption(Settings.commonKey, IV) {
|
||||
};
|
||||
return decryption.decrypt(encryptedKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + Arrays.hashCode(encryptedKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
Ticket other = (Ticket) obj;
|
||||
return Arrays.equals(encryptedKey, other.encryptedKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Ticket [encryptedKey=" + Utils.ByteArrayToString(encryptedKey) + ", decryptedKey=" + Utils.ByteArrayToString(decryptedKey) + "]";
|
||||
}
|
||||
}
|
192
src/de/mas/wiiu/jnus/entities/content/Content.java
Normal file
192
src/de/mas/wiiu/jnus/entities/content/Content.java
Normal file
@ -0,0 +1,192 @@
|
||||
package de.mas.wiiu.jnus.entities.content;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
/**
|
||||
* Represents a Content
|
||||
*
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
@Log
|
||||
public class Content {
|
||||
public static final short CONTENT_FLAG_UNKWN1 = 0x4000;
|
||||
public static final short CONTENT_HASHED = 0x0002;
|
||||
public static final short CONTENT_ENCRYPTED = 0x0001;
|
||||
public static final int CONTENT_SIZE = 0x30;
|
||||
|
||||
@Getter private final int ID;
|
||||
@Getter private final short index;
|
||||
@Getter private final short type;
|
||||
|
||||
@Getter private final long encryptedFileSize;
|
||||
@Getter private final byte[] SHA2Hash;
|
||||
|
||||
@Getter private final List<FSTEntry> entries = new ArrayList<>();
|
||||
|
||||
@Getter @Setter private ContentFSTInfo contentFSTInfo;
|
||||
|
||||
private Content(ContentParam param) {
|
||||
this.ID = param.getID();
|
||||
this.index = param.getIndex();
|
||||
this.type = param.getType();
|
||||
this.encryptedFileSize = param.getEncryptedFileSize();
|
||||
this.SHA2Hash = param.getSHA2Hash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Content object given be the raw byte data
|
||||
*
|
||||
* @param input
|
||||
* 0x30 byte of data from the TMD (starting at 0xB04)
|
||||
* @return content object
|
||||
*/
|
||||
public static Content parseContent(byte[] input) {
|
||||
if (input == null || input.length != CONTENT_SIZE) {
|
||||
log.info("Error: invalid Content byte[] input");
|
||||
return null;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate(input.length);
|
||||
buffer.put(input);
|
||||
buffer.position(0);
|
||||
|
||||
int ID = buffer.getInt(0x00);
|
||||
short index = buffer.getShort(0x04);
|
||||
short type = buffer.getShort(0x06);
|
||||
long encryptedFileSize = buffer.getLong(0x08);
|
||||
buffer.position(0x10);
|
||||
byte[] hash = new byte[0x14];
|
||||
buffer.get(hash, 0x00, 0x14);
|
||||
byte[] hash2 = new byte[0x06];
|
||||
buffer.get(hash2, 0x00, 0x06);
|
||||
|
||||
ContentParam param = new ContentParam();
|
||||
param.setID(ID);
|
||||
param.setIndex(index);
|
||||
param.setType(type);
|
||||
param.setEncryptedFileSize(encryptedFileSize);
|
||||
param.setSHA2Hash(hash);
|
||||
|
||||
return new Content(param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the content is hashed
|
||||
*
|
||||
* @return true if hashed
|
||||
*/
|
||||
public boolean isHashed() {
|
||||
return (type & CONTENT_HASHED) == CONTENT_HASHED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the content is encrypted
|
||||
*
|
||||
* @return true if encrypted
|
||||
*/
|
||||
public boolean isEncrypted() {
|
||||
return (type & CONTENT_ENCRYPTED) == CONTENT_ENCRYPTED;
|
||||
}
|
||||
|
||||
public boolean isUNKNWNFlag1Set() {
|
||||
return (type & CONTENT_FLAG_UNKWN1) == CONTENT_FLAG_UNKWN1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filename of the encrypted content.
|
||||
* It's the ID as hex with an extension
|
||||
* For example: 00000000.app
|
||||
*
|
||||
* @return filename of the encrypted content
|
||||
*/
|
||||
public String getFilename() {
|
||||
return String.format("%08X%s", getID(), Settings.ENCRYPTED_CONTENT_EXTENTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a content to the internal entry list.
|
||||
*
|
||||
* @param entry
|
||||
* that will be added to the content list
|
||||
*/
|
||||
public void addEntry(FSTEntry entry) {
|
||||
getEntries().add(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the decrypted content.
|
||||
*
|
||||
* @return size of the decrypted content
|
||||
*/
|
||||
public long getDecryptedFileSize() {
|
||||
if (isHashed()) {
|
||||
return getEncryptedFileSize() / 0x10000 * 0xFC00;
|
||||
} else {
|
||||
return getEncryptedFileSize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filename of the decrypted content.
|
||||
* It's the ID as hex with an extension
|
||||
* For example: 00000000.dec
|
||||
*
|
||||
* @return filename of the decrypted content
|
||||
*/
|
||||
public String getFilenameDecrypted() {
|
||||
return String.format("%08X%s", getID(), Settings.DECRYPTED_CONTENT_EXTENTION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ID;
|
||||
result = prime * result + Arrays.hashCode(SHA2Hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
Content other = (Content) obj;
|
||||
if (ID != other.ID) return false;
|
||||
return Arrays.equals(SHA2Hash, other.SHA2Hash);
|
||||
}
|
||||
|
||||
public long getEncryptedFileSizeAligned() {
|
||||
return Utils.align(encryptedFileSize, 16);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Content [ID=" + Integer.toHexString(ID) + ", index=" + Integer.toHexString(index) + ", type=" + String.format("%04X", type)
|
||||
+ ", encryptedFileSize=" + encryptedFileSize + ", SHA2Hash=" + Utils.ByteArrayToString(SHA2Hash) + "]";
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ContentParam {
|
||||
private int ID;
|
||||
private short index;
|
||||
private short type;
|
||||
|
||||
private long encryptedFileSize;
|
||||
private byte[] SHA2Hash;
|
||||
|
||||
private ContentFSTInfo contentFSTInfo;
|
||||
}
|
||||
|
||||
}
|
105
src/de/mas/wiiu/jnus/entities/content/ContentFSTInfo.java
Normal file
105
src/de/mas/wiiu/jnus/entities/content/ContentFSTInfo.java
Normal file
@ -0,0 +1,105 @@
|
||||
package de.mas.wiiu.jnus.entities.content;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@EqualsAndHashCode
|
||||
/**
|
||||
* Representation on an Object of the first section
|
||||
* of an FST.
|
||||
*
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
@Log
|
||||
public final class ContentFSTInfo {
|
||||
@Getter private final long offsetSector;
|
||||
@Getter private final long sizeSector;
|
||||
@Getter private final long ownerTitleID;
|
||||
@Getter private final int groupID;
|
||||
@Getter private final byte unkown;
|
||||
|
||||
private static int SECTOR_SIZE = 0x8000;
|
||||
|
||||
private ContentFSTInfo(ContentFSTInfoParam param) {
|
||||
this.offsetSector = param.getOffsetSector();
|
||||
this.sizeSector = param.getSizeSector();
|
||||
this.ownerTitleID = param.getOwnerTitleID();
|
||||
this.groupID = param.getGroupID();
|
||||
this.unkown = param.getUnkown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ContentFSTInfo object given be the raw byte data
|
||||
*
|
||||
* @param input
|
||||
* 0x20 byte of data from the FST (starting at 0x20)
|
||||
* @return ContentFSTInfo object
|
||||
*/
|
||||
public static ContentFSTInfo parseContentFST(byte[] input) {
|
||||
if (input == null || input.length != 0x20) {
|
||||
log.info("Error: invalid ContentFSTInfo byte[] input");
|
||||
return null;
|
||||
}
|
||||
ContentFSTInfoParam param = new ContentFSTInfoParam();
|
||||
ByteBuffer buffer = ByteBuffer.allocate(input.length);
|
||||
buffer.put(input);
|
||||
|
||||
buffer.position(0);
|
||||
int offset = buffer.getInt();
|
||||
int size = buffer.getInt();
|
||||
long ownerTitleID = buffer.getLong();
|
||||
int groupID = buffer.getInt();
|
||||
byte unkown = buffer.get();
|
||||
|
||||
param.setOffsetSector(offset);
|
||||
param.setSizeSector(size);
|
||||
param.setOwnerTitleID(ownerTitleID);
|
||||
param.setGroupID(groupID);
|
||||
param.setUnkown(unkown);
|
||||
|
||||
return new ContentFSTInfo(param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of of the Content in the partition
|
||||
*
|
||||
* @return offset of the content in the partition in bytes
|
||||
*/
|
||||
public long getOffset() {
|
||||
long result = (getOffsetSector() * SECTOR_SIZE) - SECTOR_SIZE;
|
||||
if (result < 0) {
|
||||
return 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size in bytes, not in sectors
|
||||
*
|
||||
* @return size in bytes
|
||||
*/
|
||||
public int getSize() {
|
||||
return (int) (getSizeSector() * SECTOR_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ContentFSTInfo [offset=" + String.format("%08X", offsetSector) + ", size=" + String.format("%08X", sizeSector) + ", ownerTitleID="
|
||||
+ String.format("%016X", ownerTitleID) + ", groupID=" + String.format("%08X", groupID) + ", unkown=" + unkown + "]";
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ContentFSTInfoParam {
|
||||
private long offsetSector;
|
||||
private long sizeSector;
|
||||
private long ownerTitleID;
|
||||
private int groupID;
|
||||
private byte unkown;
|
||||
}
|
||||
|
||||
}
|
@ -1,63 +1,71 @@
|
||||
package de.mas.jnus.lib.entities.content;
|
||||
package de.mas.wiiu.jnus.entities.content;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@EqualsAndHashCode
|
||||
/**
|
||||
* Represents a Object from the TMD before the actual Content Section.
|
||||
*
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class ContentInfo{
|
||||
@Getter @Setter private short indexOffset = 0x00;
|
||||
@Getter @Setter private short commandCount = 0x00;
|
||||
@Getter @Setter private byte[] SHA2Hash = new byte[0x20];
|
||||
|
||||
@Log
|
||||
public class ContentInfo {
|
||||
public static final int CONTENT_INFO_SIZE = 0x24;
|
||||
|
||||
@Getter private final short indexOffset;
|
||||
@Getter private final short commandCount;
|
||||
@Getter private final byte[] SHA2Hash;
|
||||
|
||||
public ContentInfo() {
|
||||
this((short) 0);
|
||||
}
|
||||
|
||||
|
||||
public ContentInfo(short contentCount) {
|
||||
this((short) 0,contentCount);
|
||||
this((short) 0, contentCount);
|
||||
}
|
||||
public ContentInfo(short indexOffset,short commandCount) {
|
||||
this(indexOffset,commandCount,null);
|
||||
|
||||
public ContentInfo(short indexOffset, short commandCount) {
|
||||
this(indexOffset, commandCount, null);
|
||||
}
|
||||
public ContentInfo(short indexOffset,short commandCount,byte[] SHA2Hash) {
|
||||
setIndexOffset(indexOffset);
|
||||
setCommandCount(commandCount);
|
||||
setSHA2Hash(SHA2Hash);
|
||||
|
||||
public ContentInfo(short indexOffset, short commandCount, byte[] SHA2Hash) {
|
||||
this.indexOffset = indexOffset;
|
||||
this.commandCount = commandCount;
|
||||
this.SHA2Hash = SHA2Hash;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new ContentInfo object given be the raw byte data
|
||||
* @param input 0x24 byte of data from the TMD (starting at 0x208)
|
||||
*
|
||||
* @param input
|
||||
* 0x24 byte of data from the TMD (starting at 0x208)
|
||||
* @return ContentFSTInfo object
|
||||
*/
|
||||
public static ContentInfo parseContentInfo(byte[] input){
|
||||
if(input == null || input.length != 0x24){
|
||||
System.out.println("Error: invalid ContentInfo byte[] input");
|
||||
public static ContentInfo parseContentInfo(byte[] input) {
|
||||
if (input == null || input.length != CONTENT_INFO_SIZE) {
|
||||
log.info("Error: invalid ContentInfo byte[] input");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(input.length);
|
||||
buffer.put(input);
|
||||
buffer.position(0);
|
||||
short indexOffset = buffer.getShort(0x00);
|
||||
short commandCount = buffer.getShort(0x02);
|
||||
|
||||
byte[] sha2hash = new byte[0x20];
|
||||
byte[] sha2hash = new byte[0x20];
|
||||
buffer.position(0x04);
|
||||
buffer.get(sha2hash, 0x00, 0x20);
|
||||
|
||||
return new ContentInfo(indexOffset, commandCount, sha2hash);
|
||||
|
||||
return new ContentInfo(indexOffset, commandCount, sha2hash);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ContentInfo [indexOffset=" + indexOffset + ", commandCount=" + commandCount + ", SHA2Hash=" + Arrays.toString(SHA2Hash) + "]";
|
85
src/de/mas/wiiu/jnus/entities/fst/FST.java
Normal file
85
src/de/mas/wiiu/jnus/entities/fst/FST.java
Normal file
@ -0,0 +1,85 @@
|
||||
package de.mas.wiiu.jnus.entities.fst;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
|
||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Represents the FST
|
||||
*
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public final class FST {
|
||||
@Getter private final FSTEntry root = FSTEntry.getRootFSTEntry();
|
||||
|
||||
@Getter private final int unknown;
|
||||
@Getter private final int contentCount;
|
||||
|
||||
@Getter private final Map<Integer, ContentFSTInfo> contentFSTInfos = new HashMap<>();
|
||||
|
||||
private FST(int unknown, int contentCount) {
|
||||
this.unknown = unknown;
|
||||
this.contentCount = contentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a FST by the given raw byte data
|
||||
*
|
||||
* @param fstData
|
||||
* raw decrypted FST data
|
||||
* @param contentsMappedByIndex
|
||||
* map of index/content
|
||||
* @return
|
||||
*/
|
||||
public static FST parseFST(byte[] fstData, Map<Integer, Content> contentsMappedByIndex) {
|
||||
if (!Arrays.equals(Arrays.copyOfRange(fstData, 0, 3), new byte[] { 0x46, 0x53, 0x54 })) {
|
||||
throw new NullPointerException();
|
||||
// return null;
|
||||
// throw new IllegalArgumentException("Not a FST. Maybe a wrong key?");
|
||||
}
|
||||
|
||||
int unknownValue = ByteUtils.getIntFromBytes(fstData, 0x04);
|
||||
int contentCount = ByteUtils.getIntFromBytes(fstData, 0x08);
|
||||
|
||||
FST result = new FST(unknownValue, contentCount);
|
||||
|
||||
int contentfst_offset = 0x20;
|
||||
int contentfst_size = 0x20 * contentCount;
|
||||
|
||||
int fst_offset = contentfst_offset + contentfst_size;
|
||||
|
||||
int fileCount = ByteUtils.getIntFromBytes(fstData, fst_offset + 0x08);
|
||||
int fst_size = fileCount * 0x10;
|
||||
|
||||
int nameOff = fst_offset + fst_size;
|
||||
int nameSize = nameOff + 1;
|
||||
|
||||
// Get list with null-terminated Strings. Ends with \0\0.
|
||||
for (int i = nameOff; i < fstData.length - 1; i++) {
|
||||
if (fstData[i] == 0 && fstData[i + 1] == 0) {
|
||||
nameSize = i - nameOff;
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer, ContentFSTInfo> contentFSTInfos = result.getContentFSTInfos();
|
||||
for (int i = 0; i < contentCount; i++) {
|
||||
byte contentFST[] = Arrays.copyOfRange(fstData, contentfst_offset + (i * 0x20), contentfst_offset + ((i + 1) * 0x20));
|
||||
contentFSTInfos.put(i, ContentFSTInfo.parseContentFST(contentFST));
|
||||
}
|
||||
|
||||
byte fstSection[] = Arrays.copyOfRange(fstData, fst_offset, fst_offset + fst_size);
|
||||
byte nameSection[] = Arrays.copyOfRange(fstData, nameOff, nameOff + nameSize);
|
||||
|
||||
FSTEntry root = result.getRoot();
|
||||
|
||||
FSTService.parseFST(root, fstSection, nameSection, contentsMappedByIndex, contentFSTInfos);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package de.mas.jnus.lib.entities.fst;
|
||||
package de.mas.wiiu.jnus.entities.fst;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
@ -18,29 +18,43 @@ public class FSTEntry{
|
||||
public static final byte FSTEntry_DIR = (byte)0x01;
|
||||
public static final byte FSTEntry_notInNUS = (byte)0x80;
|
||||
|
||||
@Getter @Setter private String filename = "";
|
||||
@Getter @Setter private String path = "";
|
||||
@Getter @Setter private FSTEntry parent = null;
|
||||
@Getter private final String filename;
|
||||
@Getter private final String path;
|
||||
@Getter private final FSTEntry parent;
|
||||
|
||||
private List<FSTEntry> children = null;
|
||||
@Getter private final List<FSTEntry> children = new ArrayList<>();
|
||||
|
||||
@Getter @Setter private short flags;
|
||||
@Getter private final short flags;
|
||||
|
||||
@Getter @Setter private long fileSize = 0;
|
||||
@Getter @Setter private long fileOffset = 0;
|
||||
@Getter private final long fileSize;
|
||||
@Getter private final long fileOffset;
|
||||
|
||||
@Getter private Content content = null;
|
||||
@Getter private final Content content;
|
||||
|
||||
@Getter @Setter private byte[] hash = new byte[0x14];
|
||||
|
||||
@Getter @Setter private boolean isDir = false;
|
||||
@Getter @Setter private boolean isRoot = false;
|
||||
@Getter @Setter private boolean notInPackage = false;
|
||||
@Getter private final boolean isDir;
|
||||
@Getter private final boolean isRoot;
|
||||
@Getter private final boolean isNotInPackage;
|
||||
|
||||
@Getter @Setter private short contentFSTID = 0;
|
||||
@Getter private final short contentFSTID;
|
||||
|
||||
public FSTEntry(){
|
||||
|
||||
protected FSTEntry(FSTEntryParam fstParam){
|
||||
this.filename = fstParam.getFilename();
|
||||
this.path = fstParam.getPath();
|
||||
this.flags = fstParam.getFlags();
|
||||
this.parent = fstParam.getParent();
|
||||
if(parent != null){
|
||||
parent.children.add(this);
|
||||
}
|
||||
this.fileSize = fstParam.getFileSize();
|
||||
this.fileOffset = fstParam.getFileOffset();
|
||||
this.content = fstParam.getContent();
|
||||
if(content != null){
|
||||
content.addEntry(this);
|
||||
}
|
||||
this.isDir = fstParam.isDir();
|
||||
this.isRoot = fstParam.isRoot();
|
||||
this.isNotInPackage = fstParam.isNotInPackage();
|
||||
this.contentFSTID = fstParam.getContentFSTID();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,9 +62,10 @@ public class FSTEntry{
|
||||
* @return
|
||||
*/
|
||||
public static FSTEntry getRootFSTEntry(){
|
||||
FSTEntry entry = new FSTEntry();
|
||||
entry.setRoot(true);
|
||||
return entry;
|
||||
FSTEntryParam param = new FSTEntryParam();
|
||||
param.setRoot(true);
|
||||
param.setDir(true);
|
||||
return new FSTEntry(param);
|
||||
}
|
||||
|
||||
public String getFullPath() {
|
||||
@ -65,18 +80,6 @@ public class FSTEntry{
|
||||
return count;
|
||||
}
|
||||
|
||||
public void addChildren(FSTEntry fstEntry) {
|
||||
getChildren().add(fstEntry);
|
||||
fstEntry.setParent(this);
|
||||
}
|
||||
|
||||
public List<FSTEntry> getChildren() {
|
||||
if(children == null){
|
||||
children = new ArrayList<>();
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
public List<FSTEntry> getDirChildren(){
|
||||
return getDirChildren(false);
|
||||
}
|
||||
@ -120,16 +123,6 @@ public class FSTEntry{
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void setContent(Content content) {
|
||||
if(content == null){
|
||||
log.warning("Can't set content for "+ getFilename() + ": Content it null");
|
||||
System.out.println();
|
||||
return;
|
||||
}
|
||||
this.content = content;
|
||||
content.addEntry(this);
|
||||
}
|
||||
|
||||
public long getFileOffsetBlock() {
|
||||
if(getContent().isHashed()){
|
||||
@ -160,6 +153,28 @@ public class FSTEntry{
|
||||
public String toString() {
|
||||
return "FSTEntry [filename=" + filename + ", path=" + path + ", flags=" + flags + ", filesize=" + fileSize
|
||||
+ ", fileoffset=" + fileOffset + ", content=" + content + ", isDir=" + isDir + ", isRoot=" + isRoot
|
||||
+ ", notInPackage=" + notInPackage + "]";
|
||||
+ ", notInPackage=" + isNotInPackage + "]";
|
||||
}
|
||||
|
||||
@Data
|
||||
protected static class FSTEntryParam {
|
||||
private String filename = "";
|
||||
private String path = "";
|
||||
|
||||
private FSTEntry parent = null;
|
||||
|
||||
private short flags;
|
||||
|
||||
private long fileSize = 0;
|
||||
private long fileOffset = 0;
|
||||
|
||||
private Content content = null;
|
||||
|
||||
private boolean isDir = false;
|
||||
private boolean isRoot = false;
|
||||
private boolean notInPackage = false;
|
||||
|
||||
private short contentFSTID = 0;
|
||||
}
|
||||
|
||||
}
|
157
src/de/mas/wiiu/jnus/entities/fst/FSTService.java
Normal file
157
src/de/mas/wiiu/jnus/entities/fst/FSTService.java
Normal file
@ -0,0 +1,157 @@
|
||||
package de.mas.wiiu.jnus.entities.fst;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry.FSTEntryParam;
|
||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class FSTService {
|
||||
private FSTService() {
|
||||
}
|
||||
|
||||
public static void parseFST(FSTEntry rootEntry, byte[] fstSection, byte[] namesSection, Map<Integer, Content> contentsByIndex,
|
||||
Map<Integer, ContentFSTInfo> contentsFSTByIndex) {
|
||||
int totalEntries = ByteUtils.getIntFromBytes(fstSection, 0x08);
|
||||
|
||||
int level = 0;
|
||||
int[] LEntry = new int[16];
|
||||
int[] Entry = new int[16];
|
||||
String[] pathStrings = new String[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
pathStrings[i] = "";
|
||||
}
|
||||
|
||||
HashMap<Integer, FSTEntry> fstEntryToOffsetMap = new HashMap<>();
|
||||
Entry[level] = 0;
|
||||
LEntry[level++] = 0;
|
||||
|
||||
fstEntryToOffsetMap.put(0, rootEntry);
|
||||
|
||||
int lastlevel = level;
|
||||
String path = "\\";
|
||||
|
||||
FSTEntry last = null;
|
||||
for (int i = 1; i < totalEntries; i++) {
|
||||
|
||||
int entryOffset = i;
|
||||
if (level > 0) {
|
||||
while (LEntry[level - 1] == i) {
|
||||
level--;
|
||||
}
|
||||
}
|
||||
|
||||
byte[] curEntry = Arrays.copyOfRange(fstSection, i * 0x10, (i + 1) * 0x10);
|
||||
|
||||
FSTEntryParam entryParam = new FSTEntry.FSTEntryParam();
|
||||
|
||||
if (lastlevel != level) {
|
||||
path = pathStrings[level] + getFullPath(level - 1, level, fstSection, namesSection, Entry);
|
||||
lastlevel = level;
|
||||
}
|
||||
|
||||
String filename = getName(curEntry, namesSection);
|
||||
|
||||
long fileOffset = ByteUtils.getIntFromBytes(curEntry, 0x04);
|
||||
long fileSize = ByteUtils.getUnsingedIntFromBytes(curEntry, 0x08);
|
||||
|
||||
short flags = ByteUtils.getShortFromBytes(curEntry, 0x0C);
|
||||
short contentIndex = ByteUtils.getShortFromBytes(curEntry, 0x0E);
|
||||
|
||||
if ((curEntry[0] & FSTEntry.FSTEntry_notInNUS) == FSTEntry.FSTEntry_notInNUS) {
|
||||
entryParam.setNotInPackage(true);
|
||||
}
|
||||
FSTEntry parent = null;
|
||||
if ((curEntry[0] & FSTEntry.FSTEntry_DIR) == FSTEntry.FSTEntry_DIR) {
|
||||
entryParam.setDir(true);
|
||||
int parentOffset = (int) fileOffset;
|
||||
int nextOffset = (int) fileSize;
|
||||
|
||||
parent = fstEntryToOffsetMap.get(parentOffset);
|
||||
Entry[level] = i;
|
||||
LEntry[level++] = nextOffset;
|
||||
pathStrings[level] = path;
|
||||
|
||||
if (level > 15) {
|
||||
log.warning("level > 15");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
entryParam.setFileOffset(fileOffset << 5);
|
||||
|
||||
entryParam.setFileSize(fileSize);
|
||||
parent = fstEntryToOffsetMap.get(Entry[level - 1]);
|
||||
}
|
||||
|
||||
entryParam.setFlags(flags);
|
||||
entryParam.setFilename(filename);
|
||||
entryParam.setPath(path);
|
||||
|
||||
if (contentsByIndex != null) {
|
||||
Content content = contentsByIndex.get((int) contentIndex);
|
||||
if (content == null) {
|
||||
log.warning("Content for FST Entry not found");
|
||||
} else {
|
||||
|
||||
if (content.isHashed() && (content.getDecryptedFileSize() < (fileOffset << 5))) { // TODO: Figure out how this works...
|
||||
entryParam.setFileOffset(fileOffset);
|
||||
}
|
||||
|
||||
entryParam.setContent(content);
|
||||
|
||||
ContentFSTInfo contentFSTInfo = contentsFSTByIndex.get((int) contentIndex);
|
||||
if (contentFSTInfo == null) {
|
||||
log.warning("ContentFSTInfo for FST Entry not found");
|
||||
} else {
|
||||
content.setContentFSTInfo(contentFSTInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entryParam.setContentFSTID(contentIndex);
|
||||
entryParam.setParent(parent);
|
||||
|
||||
FSTEntry entry = new FSTEntry(entryParam);
|
||||
last = entry;
|
||||
fstEntryToOffsetMap.put(entryOffset, entry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static int getNameOffset(byte[] curEntry) {
|
||||
// Its a 24bit number. We overwrite the first byte, then we can read it as an Integer.
|
||||
// But at first we make a copy.
|
||||
byte[] entryData = Arrays.copyOf(curEntry, curEntry.length);
|
||||
entryData[0] = 0;
|
||||
return ByteUtils.getIntFromBytes(entryData, 0);
|
||||
}
|
||||
|
||||
public static String getName(byte[] data, byte[] namesSection) {
|
||||
int nameOffset = getNameOffset(data);
|
||||
int j = 0;
|
||||
|
||||
while ((nameOffset + j) < namesSection.length && namesSection[nameOffset + j] != 0) {
|
||||
j++;
|
||||
}
|
||||
|
||||
return (new String(Arrays.copyOfRange(namesSection, nameOffset, nameOffset + j)));
|
||||
}
|
||||
|
||||
public static String getFullPath(int startlevel, int endlevel, byte[] fstSection, byte[] namesSection, int[] Entry) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = startlevel; i < endlevel; i++) {
|
||||
int entryOffset = Entry[i] * 0x10;
|
||||
byte[] entryData = Arrays.copyOfRange(fstSection, entryOffset, entryOffset + 10);
|
||||
String entryName = getName(entryData, namesSection);
|
||||
|
||||
sb.append(entryName).append(File.separator);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -1,103 +1,108 @@
|
||||
package de.mas.jnus.lib.implementations;
|
||||
package de.mas.wiiu.jnus.implementations;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
|
||||
import com.sun.istack.internal.NotNull;
|
||||
|
||||
import de.mas.jnus.lib.NUSTitle;
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.utils.FileUtils;
|
||||
import de.mas.jnus.lib.utils.Utils;
|
||||
import de.mas.wiiu.jnus.NUSTitle;
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.utils.FileUtils;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
/**
|
||||
* Service Methods for loading NUS/Content data from
|
||||
* different sources
|
||||
*
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public abstract class NUSDataProvider {
|
||||
|
||||
@Getter @Setter private NUSTitle NUSTitle = null;
|
||||
|
||||
public NUSDataProvider () {
|
||||
}
|
||||
|
||||
@Getter private final NUSTitle NUSTitle;
|
||||
|
||||
public NUSDataProvider(NUSTitle title) {
|
||||
this.NUSTitle = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given content encrypted with his .h3 file in the given directory.
|
||||
* The Target directory will be created if it's missing.
|
||||
* If the content is not hashed, no .h3 will be saved
|
||||
* @param content Content that should be saved
|
||||
* @param outputFolder Target directory where the files will be stored in.
|
||||
*
|
||||
* @param content
|
||||
* Content that should be saved
|
||||
* @param outputFolder
|
||||
* Target directory where the files will be stored in.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void saveEncryptedContentWithH3Hash(@NotNull Content content,@NotNull String outputFolder) throws IOException {
|
||||
public void saveEncryptedContentWithH3Hash(@NonNull Content content, @NonNull String outputFolder) throws IOException {
|
||||
saveContentH3Hash(content, outputFolder);
|
||||
saveEncryptedContent(content, outputFolder);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves the .h3 file of the given content into the given directory.
|
||||
* The Target directory will be created if it's missing.
|
||||
* If the content is not hashed, no .h3 will be saved
|
||||
* @param content The content of which the h3 hashes should be saved
|
||||
*
|
||||
* @param content
|
||||
* The content of which the h3 hashes should be saved
|
||||
* @param outputFolder
|
||||
* @throws IOException
|
||||
*/
|
||||
public void saveContentH3Hash(@NotNull Content content,@NotNull String outputFolder) throws IOException {
|
||||
if(!content.isHashed()){
|
||||
public void saveContentH3Hash(@NonNull Content content, @NonNull String outputFolder) throws IOException {
|
||||
if (!content.isHashed()) {
|
||||
return;
|
||||
}
|
||||
byte[] hash = getContentH3Hash(content);
|
||||
if(hash == null){
|
||||
if (hash == null || hash.length == 0) {
|
||||
return;
|
||||
}
|
||||
String h3Filename = String.format("%08X%s", content.getID(),Settings.H3_EXTENTION);
|
||||
String h3Filename = String.format("%08X%s", content.getID(), Settings.H3_EXTENTION);
|
||||
File output = new File(outputFolder + File.separator + h3Filename);
|
||||
if(output.exists() && output.length() == hash.length){
|
||||
System.out.println(h3Filename + " already exists");
|
||||
if (output.exists() && output.length() == hash.length) {
|
||||
log.info(h3Filename + " already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("Saving " + h3Filename +" ");
|
||||
|
||||
log.info("Saving " + h3Filename + " ");
|
||||
|
||||
FileUtils.saveByteArrayToFile(output, hash);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves the given content encrypted in the given directory.
|
||||
* The Target directory will be created if it's missing.
|
||||
* If the content is not encrypted at all, it will be just saved anyway.
|
||||
* @param content Content that should be saved
|
||||
* @param outputFolder Target directory where the files will be stored in.
|
||||
*
|
||||
* @param content
|
||||
* Content that should be saved
|
||||
* @param outputFolder
|
||||
* Target directory where the files will be stored in.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void saveEncryptedContent(@NotNull Content content,@NotNull String outputFolder) throws IOException {
|
||||
public void saveEncryptedContent(@NonNull Content content, @NonNull String outputFolder) throws IOException {
|
||||
Utils.createDir(outputFolder);
|
||||
InputStream inputStream = getInputStreamFromContent(content, 0);
|
||||
if(inputStream == null){
|
||||
if (inputStream == null) {
|
||||
log.info("Couldn't save encrypted content. Input stream was null");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
File output = new File(outputFolder + File.separator + content.getFilename());
|
||||
if(output.exists()){
|
||||
if(output.length() == content.getEncryptedFileSize()){
|
||||
if (output.exists()) {
|
||||
if (output.length() == content.getEncryptedFileSizeAligned()) {
|
||||
log.info("Encrypted content alreadys exists, skipped");
|
||||
return;
|
||||
}else{
|
||||
} else {
|
||||
log.info("Encrypted content alreadys exists, but the length is not as expected. Saving it again");
|
||||
}
|
||||
}
|
||||
System.out.println("Saving " + content.getFilename());
|
||||
FileUtils.saveInputStreamToFile(output,inputStream,content.getEncryptedFileSize());
|
||||
FileUtils.saveInputStreamToFile(output, inputStream, content.getEncryptedFileSizeAligned());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -107,12 +112,17 @@ public abstract class NUSDataProvider {
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public abstract InputStream getInputStreamFromContent(Content content,long offset) throws IOException ;
|
||||
public abstract InputStream getInputStreamFromContent(Content content, long offset) throws IOException;
|
||||
|
||||
// TODO: JavaDocs
|
||||
public abstract byte[] getContentH3Hash(Content content) throws IOException;
|
||||
|
||||
public abstract byte[] getRawTMD() throws IOException;
|
||||
|
||||
public abstract byte[] getRawTicket() throws IOException;
|
||||
|
||||
public abstract byte[] getRawCert() throws IOException;
|
||||
|
||||
|
||||
public abstract void cleanup() throws IOException;
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.mas.jnus.lib.implementations;
|
||||
package de.mas.wiiu.jnus.implementations;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@ -6,30 +6,33 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.NUSTitle;
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class NUSDataProviderLocal extends NUSDataProvider {
|
||||
@Getter @Setter private String localPath = "";
|
||||
|
||||
public NUSDataProviderLocal() {
|
||||
public final class NUSDataProviderLocal extends NUSDataProvider {
|
||||
@Getter private final String localPath;
|
||||
|
||||
public NUSDataProviderLocal(NUSTitle nustitle, String localPath) {
|
||||
super(nustitle);
|
||||
this.localPath = localPath;
|
||||
}
|
||||
|
||||
|
||||
public String getFilePathOnDisk(Content c) {
|
||||
return getLocalPath() + File.separator + c.getFilename();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public InputStream getInputStreamFromContent(Content content, long offset) throws IOException {
|
||||
File filepath = new File(getFilePathOnDisk(content));
|
||||
if(!filepath.exists()){
|
||||
|
||||
if (!filepath.exists()) {
|
||||
|
||||
return null;
|
||||
}
|
||||
InputStream in = new FileInputStream(filepath);
|
||||
in.skip(offset);
|
||||
StreamUtils.skipExactly(in, offset);
|
||||
return in;
|
||||
}
|
||||
|
||||
@ -37,8 +40,8 @@ public class NUSDataProviderLocal extends NUSDataProvider {
|
||||
public byte[] getContentH3Hash(Content content) throws IOException {
|
||||
String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID());
|
||||
File h3File = new File(h3Path);
|
||||
if(!h3File.exists()){
|
||||
return null;
|
||||
if (!h3File.exists()) {
|
||||
return new byte[0];
|
||||
}
|
||||
return Files.readAllBytes(h3File.toPath());
|
||||
}
|
||||
@ -52,22 +55,28 @@ public class NUSDataProviderLocal extends NUSDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawTicket() throws IOException {
|
||||
public byte[] getRawTicket() throws IOException {
|
||||
String inputPath = getLocalPath();
|
||||
String ticketPath = inputPath + File.separator + Settings.TICKET_FILENAME;
|
||||
File ticketFile = new File(ticketPath);
|
||||
File ticketFile = new File(ticketPath);
|
||||
return Files.readAllBytes(ticketFile.toPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawCert() throws IOException {
|
||||
String inputPath = getLocalPath();
|
||||
String certPath = inputPath + File.separator + Settings.CERT_FILENAME;
|
||||
File certFile = new File(certPath);
|
||||
File certFile = new File(certPath);
|
||||
return Files.readAllBytes(certFile.toPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() throws IOException {
|
||||
// We don't need this
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NUSDataProviderLocal [localPath=" + localPath + "]";
|
||||
}
|
||||
}
|
@ -1,27 +1,31 @@
|
||||
package de.mas.jnus.lib.implementations;
|
||||
package de.mas.wiiu.jnus.implementations;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.utils.download.NUSDownloadService;
|
||||
import de.mas.wiiu.jnus.NUSTitle;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.utils.download.NUSDownloadService;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class NUSDataProviderRemote extends NUSDataProvider {
|
||||
@Getter @Setter private int version = Settings.LATEST_TMD_VERSION;
|
||||
@Getter @Setter private long titleID = 0L;
|
||||
|
||||
@Getter private final int version;
|
||||
@Getter private final long titleID;
|
||||
|
||||
public NUSDataProviderRemote(NUSTitle title, int version, long titleID) {
|
||||
super(title);
|
||||
this.version = version;
|
||||
this.titleID = titleID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStreamFromContent(Content content, long fileOffsetBlock) throws IOException {
|
||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||
InputStream in = downloadService.getInputStreamForURL(getRemoteURL(content),fileOffsetBlock);
|
||||
return in;
|
||||
return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock);
|
||||
}
|
||||
|
||||
private String getRemoteURL(Content content) {
|
||||
return String.format("%016x/%08X", getNUSTitle().getTMD().getTitleID(),content.getID());
|
||||
private String getRemoteURL(Content content) {
|
||||
return String.format("%016x/%08X", getNUSTitle().getTMD().getTitleID(), content.getID());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -34,29 +38,29 @@ public class NUSDataProviderRemote extends NUSDataProvider {
|
||||
@Override
|
||||
public byte[] getRawTMD() throws IOException {
|
||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||
|
||||
|
||||
long titleID = getTitleID();
|
||||
int version = getVersion();
|
||||
|
||||
|
||||
return downloadService.downloadTMDToByteArray(titleID, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawTicket() throws IOException {
|
||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||
|
||||
|
||||
long titleID = getNUSTitle().getTMD().getTitleID();
|
||||
|
||||
|
||||
return downloadService.downloadTicketToByteArray(titleID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() throws IOException {
|
||||
// TODO Auto-generated method stub
|
||||
public byte[] getRawCert() throws IOException {
|
||||
return new byte[0]; // TODO: needs to be implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawCert() throws IOException {
|
||||
return null;
|
||||
public void cleanup() throws IOException {
|
||||
// We don't need this
|
||||
}
|
||||
}
|
104
src/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java
Normal file
104
src/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java
Normal file
@ -0,0 +1,104 @@
|
||||
package de.mas.wiiu.jnus.implementations;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import de.mas.wiiu.jnus.NUSTitle;
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
import de.mas.wiiu.jnus.entities.TMD;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDGamePartition;
|
||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfo;
|
||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDPartitionHeader;
|
||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class NUSDataProviderWUD extends NUSDataProvider {
|
||||
@Getter private final WUDInfo WUDInfo;
|
||||
|
||||
private final TMD tmd;
|
||||
|
||||
public NUSDataProviderWUD(NUSTitle title, WUDInfo wudinfo) {
|
||||
super(title);
|
||||
this.WUDInfo = wudinfo;
|
||||
this.tmd = TMD.parseTMD(getRawTMD());
|
||||
}
|
||||
|
||||
public long getOffsetInWUD(Content content) {
|
||||
if (content.getContentFSTInfo() == null) {
|
||||
return getAbsoluteReadOffset();
|
||||
} else {
|
||||
return getAbsoluteReadOffset() + content.getContentFSTInfo().getOffset();
|
||||
}
|
||||
}
|
||||
|
||||
public long getAbsoluteReadOffset() {
|
||||
return (long) Settings.WIIU_DECRYPTED_AREA_OFFSET + getGamePartition().getPartitionOffset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStreamFromContent(Content content, long fileOffsetBlock) throws IOException {
|
||||
WUDDiscReader discReader = getDiscReader();
|
||||
long offset = getOffsetInWUD(content) + fileOffsetBlock;
|
||||
return discReader.readEncryptedToInputStream(offset, content.getEncryptedFileSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContentH3Hash(Content content) throws IOException {
|
||||
|
||||
if (getGamePartitionHeader() == null) {
|
||||
log.warning("GamePartitionHeader is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!getGamePartitionHeader().isCalculatedHashes()) {
|
||||
log.info("Calculating h3 hashes");
|
||||
getGamePartitionHeader().calculateHashes(getTMD().getAllContents());
|
||||
|
||||
}
|
||||
return getGamePartitionHeader().getH3Hash(content);
|
||||
}
|
||||
|
||||
public TMD getTMD() {
|
||||
return tmd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawTMD() {
|
||||
return getGamePartition().getRawTMD();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawTicket() {
|
||||
return getGamePartition().getRawTicket();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawCert() throws IOException {
|
||||
return getGamePartition().getRawCert();
|
||||
}
|
||||
|
||||
public WUDGamePartition getGamePartition() {
|
||||
return getWUDInfo().getGamePartition();
|
||||
}
|
||||
|
||||
public WUDPartitionHeader getGamePartitionHeader() {
|
||||
return getGamePartition().getPartitionHeader();
|
||||
}
|
||||
|
||||
public WUDDiscReader getDiscReader() {
|
||||
return getWUDInfo().getWUDDiscReader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// We don't need it
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NUSDataProviderWUD [WUDInfo=" + WUDInfo + "]";
|
||||
}
|
||||
}
|
@ -1,95 +1,102 @@
|
||||
package de.mas.jnus.lib.implementations;
|
||||
package de.mas.wiiu.jnus.implementations;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import de.mas.jnus.lib.entities.content.Content;
|
||||
import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
|
||||
import de.mas.jnus.lib.implementations.woomy.WoomyZipFile;
|
||||
import de.mas.wiiu.jnus.NUSTitle;
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
|
||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyZipFile;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class NUSDataProviderWoomy extends NUSDataProvider{
|
||||
@Getter @Setter private WoomyInfo woomyInfo;
|
||||
@Setter private WoomyZipFile woomyZipFile;
|
||||
|
||||
public class NUSDataProviderWoomy extends NUSDataProvider {
|
||||
@Getter private final WoomyInfo woomyInfo;
|
||||
@Setter(AccessLevel.PRIVATE) private WoomyZipFile woomyZipFile;
|
||||
|
||||
public NUSDataProviderWoomy(NUSTitle title, WoomyInfo woomyInfo) {
|
||||
super(title);
|
||||
this.woomyInfo = woomyInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStreamFromContent(@NonNull Content content, long fileOffsetBlock) throws IOException {
|
||||
WoomyZipFile zipFile = getSharedWoomyZipFile();
|
||||
ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
|
||||
if(entry == null){
|
||||
if (entry == null) {
|
||||
log.warning("Inputstream for " + content.getFilename() + " not found");
|
||||
System.exit(1);
|
||||
throw new FileNotFoundException("Inputstream for " + content.getFilename() + " not found");
|
||||
}
|
||||
return zipFile.getInputStream(entry);
|
||||
return zipFile.getInputStream(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContentH3Hash(Content content) throws IOException {
|
||||
ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
|
||||
if(entry != null){
|
||||
WoomyZipFile zipFile = getNewWoomyZipFile();
|
||||
if (entry != null) {
|
||||
WoomyZipFile zipFile = getNewWoomyZipFile();
|
||||
byte[] result = zipFile.getEntryAsByte(entry);
|
||||
zipFile.close();
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawTMD() throws IOException {
|
||||
ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TMD_FILENAME);
|
||||
if(entry == null){
|
||||
if (entry == null) {
|
||||
log.warning(Settings.TMD_FILENAME + " not found in woomy file");
|
||||
System.exit(1);
|
||||
throw new FileNotFoundException(Settings.TMD_FILENAME + " not found in woomy file");
|
||||
}
|
||||
WoomyZipFile zipFile = getNewWoomyZipFile();
|
||||
WoomyZipFile zipFile = getNewWoomyZipFile();
|
||||
byte[] result = zipFile.getEntryAsByte(entry);
|
||||
zipFile.close();
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawTicket() throws IOException {
|
||||
ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TICKET_FILENAME);
|
||||
if(entry == null){
|
||||
if (entry == null) {
|
||||
log.warning(Settings.TICKET_FILENAME + " not found in woomy file");
|
||||
System.exit(1);
|
||||
throw new FileNotFoundException(Settings.TICKET_FILENAME + " not found in woomy file");
|
||||
}
|
||||
|
||||
WoomyZipFile zipFile = getNewWoomyZipFile();
|
||||
|
||||
WoomyZipFile zipFile = getNewWoomyZipFile();
|
||||
byte[] result = zipFile.getEntryAsByte(entry);
|
||||
zipFile.close();
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
public WoomyZipFile getSharedWoomyZipFile() throws ZipException, IOException {
|
||||
if(woomyZipFile == null || woomyZipFile.isClosed()){
|
||||
woomyZipFile = getNewWoomyZipFile();
|
||||
if (this.woomyZipFile == null || this.woomyZipFile.isClosed()) {
|
||||
this.woomyZipFile = getNewWoomyZipFile();
|
||||
}
|
||||
return woomyZipFile;
|
||||
return this.woomyZipFile;
|
||||
}
|
||||
|
||||
private WoomyZipFile getNewWoomyZipFile() throws ZipException, IOException {
|
||||
return new WoomyZipFile(getWoomyInfo().getWoomyFile());
|
||||
return new WoomyZipFile(getWoomyInfo().getWoomyFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() throws IOException {
|
||||
if(woomyZipFile != null && woomyZipFile.isClosed()){
|
||||
woomyZipFile.close();
|
||||
if (this.woomyZipFile != null && this.woomyZipFile.isClosed()) {
|
||||
this.woomyZipFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getRawCert() throws IOException {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.mas.jnus.lib.implementations.woomy;
|
||||
package de.mas.wiiu.jnus.implementations.woomy;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
@ -10,5 +10,5 @@ import lombok.Data;
|
||||
public class WoomyInfo {
|
||||
private String name;
|
||||
private File woomyFile;
|
||||
private Map<String,ZipEntry> contentFiles;
|
||||
private Map<String, ZipEntry> contentFiles;
|
||||
}
|
25
src/de/mas/wiiu/jnus/implementations/woomy/WoomyMeta.java
Normal file
25
src/de/mas/wiiu/jnus/implementations/woomy/WoomyMeta.java
Normal file
@ -0,0 +1,25 @@
|
||||
package de.mas.wiiu.jnus.implementations.woomy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WoomyMeta {
|
||||
private final String name;
|
||||
private final int icon;
|
||||
private final List<WoomyEntry> entries = new ArrayList<>();
|
||||
|
||||
public void addEntry(String name, String folder, int entryCount) {
|
||||
WoomyEntry entry = new WoomyEntry(name, folder, entryCount);
|
||||
getEntries().add(entry);
|
||||
}
|
||||
|
||||
@Data
|
||||
public class WoomyEntry {
|
||||
private final String name;
|
||||
private final String folder;
|
||||
private final int entryCount;
|
||||
}
|
||||
}
|
@ -1,61 +1,66 @@
|
||||
package de.mas.jnus.lib.implementations.woomy;
|
||||
package de.mas.wiiu.jnus.implementations.woomy;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import de.mas.jnus.lib.utils.XMLParser;
|
||||
import de.mas.wiiu.jnus.utils.XMLParser;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class WoomyMetaParser extends XMLParser{
|
||||
public final class WoomyMetaParser extends XMLParser {
|
||||
private static final String WOOMY_METADATA_NAME = "name";
|
||||
private static final String WOOMY_METADATA_ICON = "icon";
|
||||
|
||||
|
||||
private static final String WOOMY_METADATA_ENTRIES = "entries";
|
||||
private static final String WOOMY_METADATA_ENTRY_NAME = "name";
|
||||
private static final String WOOMY_METADATA_ENTRY_FOLDER = "folder";
|
||||
private static final String WOOMY_METADATA_ENTRY_ENTRIES = "entries";
|
||||
|
||||
/**
|
||||
* Overwrite the default constructor to force the user to use the factory.
|
||||
*/
|
||||
private WoomyMetaParser(){
|
||||
private WoomyMetaParser() {
|
||||
}
|
||||
|
||||
public static WoomyMeta parseMeta(InputStream data){
|
||||
XMLParser parser = new WoomyMetaParser();
|
||||
WoomyMeta result = new WoomyMeta();
|
||||
|
||||
public static WoomyMeta parseMeta(InputStream data) {
|
||||
XMLParser parser = new WoomyMetaParser();
|
||||
String resultName = "";
|
||||
int resultIcon = 0;
|
||||
try {
|
||||
parser.loadDocument(data);
|
||||
} catch(Exception e){
|
||||
} catch (Exception e) {
|
||||
log.info("Error while loading the data into the WoomyMetaParser");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
String name = parser.getValueOfElement(WOOMY_METADATA_NAME);
|
||||
if(name != null && !name.isEmpty()){
|
||||
result.setName(name);
|
||||
if (name != null && !name.isEmpty()) {
|
||||
resultName = name;
|
||||
}
|
||||
|
||||
|
||||
String icon = parser.getValueOfElement(WOOMY_METADATA_ICON);
|
||||
if(icon != null && !icon.isEmpty()){
|
||||
int icon_val = Integer.parseInt(icon);
|
||||
result.setIcon(icon_val);
|
||||
if (icon != null && !icon.isEmpty()) {
|
||||
int icon_val = Integer.parseInt(icon);
|
||||
resultIcon = icon_val;
|
||||
}
|
||||
|
||||
WoomyMeta result = new WoomyMeta(resultName, resultIcon);
|
||||
|
||||
Node entries_node = parser.getNodeByValue(WOOMY_METADATA_ENTRIES);
|
||||
|
||||
|
||||
NodeList entry_list = entries_node.getChildNodes();
|
||||
for(int i = 0;i<entry_list.getLength();i++){
|
||||
for (int i = 0; i < entry_list.getLength(); i++) {
|
||||
Node node = entry_list.item(i);
|
||||
|
||||
|
||||
String folder = getAttributeValueFromNode(node, WOOMY_METADATA_ENTRY_FOLDER);
|
||||
String entry_name = getAttributeValueFromNode(node, WOOMY_METADATA_ENTRY_NAME);
|
||||
String entry_count = getAttributeValueFromNode(node, WOOMY_METADATA_ENTRY_ENTRIES);
|
||||
int entry_count_val = Integer.parseInt(entry_count);
|
||||
result.addEntry(entry_name, folder, entry_count_val);
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
package de.mas.jnus.lib.implementations.woomy;
|
||||
package de.mas.wiiu.jnus.implementations.woomy;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@ -15,10 +16,9 @@ import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import com.sun.istack.internal.NotNull;
|
||||
|
||||
import de.mas.jnus.lib.Settings;
|
||||
import de.mas.jnus.lib.implementations.woomy.WoomyMeta.WoomyEntry;
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyMeta.WoomyEntry;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
/**
|
||||
@ -27,55 +27,59 @@ import lombok.extern.java.Log;
|
||||
*
|
||||
*/
|
||||
@Log
|
||||
public class WoomyParser {
|
||||
public static WoomyInfo createWoomyInfo(File woomyFile) throws IOException, ParserConfigurationException, SAXException{
|
||||
public final class WoomyParser {
|
||||
private WoomyParser() {
|
||||
//
|
||||
}
|
||||
|
||||
public static WoomyInfo createWoomyInfo(File woomyFile) throws IOException, ParserConfigurationException, SAXException {
|
||||
WoomyInfo result = new WoomyInfo();
|
||||
if(!woomyFile.exists()){
|
||||
if (!woomyFile.exists()) {
|
||||
log.info("File does not exist." + woomyFile.getAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
try (ZipFile zipFile = new ZipFile(woomyFile)) {
|
||||
result.setWoomyFile(woomyFile);
|
||||
ZipEntry metaFile = zipFile.getEntry(Settings.WOOMY_METADATA_FILENAME);
|
||||
if(metaFile == null){
|
||||
if (metaFile == null) {
|
||||
log.info("No meta ");
|
||||
return null;
|
||||
}
|
||||
WoomyMeta meta = WoomyMetaParser.parseMeta(zipFile.getInputStream(metaFile));
|
||||
|
||||
|
||||
/**
|
||||
* Currently we will only use the first entry in the metadata.xml
|
||||
*/
|
||||
if(meta.getEntries().isEmpty()){
|
||||
if (meta.getEntries().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
WoomyEntry entry = meta.getEntries().get(0);
|
||||
String regEx = entry.getFolder() + ".*"; //We want all files in the entry fodler
|
||||
Map<String,ZipEntry> contentFiles = loadFileList(zipFile,regEx);
|
||||
String regEx = entry.getFolder() + ".*"; // We want all files in the entry fodler
|
||||
Map<String, ZipEntry> contentFiles = loadFileList(zipFile, regEx);
|
||||
result.setContentFiles(contentFiles);
|
||||
|
||||
}catch(ZipException e){
|
||||
|
||||
|
||||
} catch (ZipException e) {
|
||||
log.info("Caught Execption : " + e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, ZipEntry> loadFileList(@NotNull ZipFile zipFile, @NotNull String regEx) {
|
||||
private static Map<String, ZipEntry> loadFileList(@NonNull ZipFile zipFile, @NonNull String regEx) {
|
||||
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
|
||||
Map<String,ZipEntry> result = new HashMap<>();
|
||||
Map<String, ZipEntry> result = new HashMap<>();
|
||||
Pattern pattern = Pattern.compile(regEx);
|
||||
while (zipEntries.hasMoreElements()) {
|
||||
ZipEntry entry = (ZipEntry) zipEntries.nextElement();
|
||||
if(!entry.isDirectory()){
|
||||
if (!entry.isDirectory()) {
|
||||
String entryName = entry.getName();
|
||||
Matcher matcher = pattern.matcher(entryName);
|
||||
if(matcher.matches()){
|
||||
String[] tokens = entryName.split("[\\\\|/]"); //We only want the filename!
|
||||
if (matcher.matches()) {
|
||||
String[] tokens = entryName.split("[\\\\|/]"); // We only want the filename!
|
||||
String filename = tokens[tokens.length - 1];
|
||||
result.put(filename.toLowerCase(), entry);
|
||||
result.put(filename.toLowerCase(Locale.ENGLISH), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.mas.jnus.lib.implementations.woomy;
|
||||
package de.mas.wiiu.jnus.implementations.woomy;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -6,22 +6,24 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipException;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import de.mas.jnus.lib.utils.StreamUtils;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Woomy files are just zip files. This class is just the
|
||||
* normal ZipFile class extended by an "isClosed" attribute.
|
||||
*
|
||||
* @author Maschell
|
||||
*
|
||||
*/
|
||||
public class WoomyZipFile extends ZipFile {
|
||||
@Getter @Setter boolean isClosed;
|
||||
|
||||
public WoomyZipFile(File file) throws ZipException, IOException {
|
||||
super(file);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
@ -29,7 +31,6 @@ public class WoomyZipFile extends ZipFile {
|
||||
}
|
||||
|
||||
public byte[] getEntryAsByte(ZipEntry entry) throws IOException {
|
||||
return StreamUtils.getBytesFromStream(getInputStream(entry),(int) entry.getSize());
|
||||
return StreamUtils.getBytesFromStream(getInputStream(entry), (int) entry.getSize());
|
||||
}
|
||||
|
||||
}
|
110
src/de/mas/wiiu/jnus/implementations/wud/WUDImage.java
Normal file
110
src/de/mas/wiiu/jnus/implementations/wud/WUDImage.java
Normal file
@ -0,0 +1,110 @@
|
||||
package de.mas.wiiu.jnus.implementations.wud;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderCompressed;
|
||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderSplitted;
|
||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderUncompressed;
|
||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class WUDImage {
|
||||
public static long WUD_FILESIZE = 0x5D3A00000L;
|
||||
|
||||
@Getter private final File fileHandle;
|
||||
@Getter @Setter private WUDImageCompressedInfo compressedInfo = null;
|
||||
|
||||
@Getter private final boolean isCompressed;
|
||||
@Getter private final boolean isSplitted;
|
||||
|
||||
private long inputFileSize = 0L;
|
||||
@Getter private final WUDDiscReader WUDDiscReader;
|
||||
|
||||
public WUDImage(File file) throws IOException {
|
||||
if (file == null || !file.exists()) {
|
||||
log.info("WUD file is null or does not exist");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
RandomAccessFile fileStream = new RandomAccessFile(file, "r");
|
||||
fileStream.seek(0);
|
||||
byte[] wuxheader = new byte[WUDImageCompressedInfo.WUX_HEADER_SIZE];
|
||||
fileStream.read(wuxheader);
|
||||
WUDImageCompressedInfo compressedInfo = new WUDImageCompressedInfo(wuxheader);
|
||||
|
||||
if (compressedInfo.isWUX()) {
|
||||
log.info("Image is compressed");
|
||||
this.isCompressed = true;
|
||||
this.isSplitted = false;
|
||||
Map<Integer, Long> indexTable = new HashMap<>();
|
||||
long offsetIndexTable = compressedInfo.getOffsetIndexTable();
|
||||
fileStream.seek(offsetIndexTable);
|
||||
|
||||
byte[] tableData = new byte[(int) (compressedInfo.getIndexTableEntryCount() * 0x04)];
|
||||
fileStream.read(tableData);
|
||||
int cur_offset = 0x00;
|
||||
for (long i = 0; i < compressedInfo.getIndexTableEntryCount(); i++) {
|
||||
indexTable.put((int) i, ByteUtils.getUnsingedIntFromBytes(tableData, (int) cur_offset, ByteOrder.LITTLE_ENDIAN));
|
||||
cur_offset += 0x04;
|
||||
}
|
||||
compressedInfo.setIndexTable(indexTable);
|
||||
setCompressedInfo(compressedInfo);
|
||||
} else {
|
||||
this.isCompressed = false;
|
||||
if (file.getName().equals(String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, 1))
|
||||
&& (file.length() == WUDDiscReaderSplitted.WUD_SPLITTED_FILE_SIZE)) {
|
||||
this.isSplitted = true;
|
||||
log.info("Image is splitted");
|
||||
} else {
|
||||
this.isSplitted = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCompressed()) {
|
||||
this.WUDDiscReader = new WUDDiscReaderCompressed(this);
|
||||
} else if (isSplitted()) {
|
||||
this.WUDDiscReader = new WUDDiscReaderSplitted(this);
|
||||
} else {
|
||||
this.WUDDiscReader = new WUDDiscReaderUncompressed(this);
|
||||
}
|
||||
|
||||
fileStream.close();
|
||||
this.fileHandle = file;
|
||||
}
|
||||
|
||||
public long getWUDFileSize() {
|
||||
if (inputFileSize == 0) {
|
||||
if (isSplitted()) {
|
||||
inputFileSize = calculateSplittedFileSize();
|
||||
} else if (isCompressed()) {
|
||||
inputFileSize = getCompressedInfo().getUncompressedSize();
|
||||
} else {
|
||||
inputFileSize = getFileHandle().length();
|
||||
}
|
||||
}
|
||||
return inputFileSize;
|
||||
}
|
||||
|
||||
private long calculateSplittedFileSize() {
|
||||
long result = 0;
|
||||
File filehandlePart1 = getFileHandle();
|
||||
String pathToFiles = filehandlePart1.getParentFile().getAbsolutePath();
|
||||
for (int i = 1; i <= WUDDiscReaderSplitted.NUMBER_OF_FILES; i++) {
|
||||
String filePartPath = pathToFiles + File.separator + String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, i);
|
||||
File part = new File(filePartPath);
|
||||
if (part.exists()) {
|
||||
result += part.length();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package de.mas.wiiu.jnus.implementations.wud;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class WUDImageCompressedInfo {
|
||||
public static final int WUX_HEADER_SIZE = 0x20;
|
||||
public static final int WUX_MAGIC_0 = 0x30585557;
|
||||
public static final int WUX_MAGIC_1 = 0x1099d02e;
|
||||
public static final int SECTOR_SIZE = 0x8000;
|
||||
|
||||
@Getter private final int sectorSize;
|
||||
@Getter private final long uncompressedSize;
|
||||
@Getter private final int flags;
|
||||
|
||||
@Getter @Setter private long indexTableEntryCount;
|
||||
@Getter private final long offsetIndexTable = WUX_HEADER_SIZE;
|
||||
@Getter @Setter private long offsetSectorArray;
|
||||
@Getter @Setter private long indexTableSize;
|
||||
|
||||
private final boolean valid;
|
||||
|
||||
@Getter private Map<Integer, Long> indexTable = new HashMap<>();
|
||||
|
||||
public WUDImageCompressedInfo(byte[] headData) {
|
||||
if (headData.length < WUX_HEADER_SIZE) {
|
||||
log.info("WUX header length wrong");
|
||||
System.exit(1);
|
||||
}
|
||||
int magic0 = ByteUtils.getIntFromBytes(headData, 0x00, ByteOrder.LITTLE_ENDIAN);
|
||||
int magic1 = ByteUtils.getIntFromBytes(headData, 0x04, ByteOrder.LITTLE_ENDIAN);
|
||||
if (magic0 == WUX_MAGIC_0 && magic1 == WUX_MAGIC_1) {
|
||||
valid = true;
|
||||
} else {
|
||||
valid = false;
|
||||
}
|
||||
this.sectorSize = ByteUtils.getIntFromBytes(headData, 0x08, ByteOrder.LITTLE_ENDIAN);
|
||||
this.flags = ByteUtils.getIntFromBytes(headData, 0x0C, ByteOrder.LITTLE_ENDIAN);
|
||||
this.uncompressedSize = ByteUtils.getLongFromBytes(headData, 0x10, ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
calculateOffsets();
|
||||
}
|
||||
|
||||
public static WUDImageCompressedInfo getDefaultCompressedInfo() {
|
||||
return new WUDImageCompressedInfo(SECTOR_SIZE, 0, WUDImage.WUD_FILESIZE);
|
||||
}
|
||||
|
||||
public WUDImageCompressedInfo(int sectorSize, int flags, long uncompressedSize) {
|
||||
this.sectorSize = sectorSize;
|
||||
this.flags = flags;
|
||||
this.uncompressedSize = uncompressedSize;
|
||||
valid = true;
|
||||
calculateOffsets();
|
||||
}
|
||||
|
||||
private void calculateOffsets() {
|
||||
long indexTableEntryCount = (getUncompressedSize() + getSectorSize() - 1) / getSectorSize();
|
||||
setIndexTableEntryCount(indexTableEntryCount);
|
||||
long offsetSectorArray = (getOffsetIndexTable() + ((long) getIndexTableEntryCount() * 0x04L));
|
||||
// align to SECTOR_SIZE
|
||||
offsetSectorArray = (offsetSectorArray + (long) (getSectorSize() - 1));
|
||||
offsetSectorArray = offsetSectorArray - (offsetSectorArray % (long) getSectorSize());
|
||||
setOffsetSectorArray(offsetSectorArray);
|
||||
// read index table
|
||||
setIndexTableSize(0x04 * getIndexTableEntryCount());
|
||||
}
|
||||
|
||||
public boolean isWUX() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public long getSectorIndex(int sectorIndex) {
|
||||
return getIndexTable().get(sectorIndex);
|
||||
}
|
||||
|
||||
public void setIndexTable(Map<Integer, Long> indexTable) {
|
||||
this.indexTable = indexTable;
|
||||
}
|
||||
|
||||
public byte[] getHeaderAsBytes() {
|
||||
ByteBuffer result = ByteBuffer.allocate(WUX_HEADER_SIZE);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.putInt(WUX_MAGIC_0);
|
||||
result.putInt(WUX_MAGIC_1);
|
||||
result.putInt(getSectorSize());
|
||||
result.putInt(getFlags());
|
||||
result.putLong(getUncompressedSize());
|
||||
return result.array();
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class WUDGamePartition extends WUDPartition {
|
||||
private final byte[] rawTMD;
|
||||
private final byte[] rawCert;
|
||||
private final byte[] rawTicket;
|
||||
|
||||
public WUDGamePartition(String partitionName, long partitionOffset, byte[] rawTMD, byte[] rawCert, byte[] rawTicket) {
|
||||
super(partitionName, partitionOffset);
|
||||
this.rawTMD = rawTMD;
|
||||
this.rawCert = rawCert;
|
||||
this.rawTicket = rawTicket;
|
||||
}
|
||||
}
|
43
src/de/mas/wiiu/jnus/implementations/wud/parser/WUDInfo.java
Normal file
43
src/de/mas/wiiu/jnus/implementations/wud/parser/WUDInfo.java
Normal file
@ -0,0 +1,43 @@
|
||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Data
|
||||
public class WUDInfo {
|
||||
private final byte[] titleKey;
|
||||
|
||||
private final WUDDiscReader WUDDiscReader;
|
||||
private final Map<String, WUDPartition> partitions = new HashMap<>();
|
||||
|
||||
@Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PROTECTED) private String gamePartitionName;
|
||||
|
||||
private WUDGamePartition cachedGamePartition = null;
|
||||
|
||||
public void addPartion(String partitionName, WUDGamePartition partition) {
|
||||
getPartitions().put(partitionName, partition);
|
||||
}
|
||||
|
||||
public WUDGamePartition getGamePartition() {
|
||||
if (cachedGamePartition == null) {
|
||||
cachedGamePartition = findGamePartition();
|
||||
}
|
||||
return cachedGamePartition;
|
||||
}
|
||||
|
||||
private WUDGamePartition findGamePartition() {
|
||||
for (Entry<String, WUDPartition> e : getPartitions().entrySet()) {
|
||||
if (e.getKey().equals(getGamePartitionName())) {
|
||||
return (WUDGamePartition) e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
import de.mas.wiiu.jnus.entities.content.ContentFSTInfo;
|
||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
||||
import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader;
|
||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
// TODO: reduce magic numbers
|
||||
public final class WUDInfoParser {
|
||||
public static byte[] DECRYPTED_AREA_SIGNATURE = new byte[] { (byte) 0xCC, (byte) 0xA6, (byte) 0xE6, 0x7B };
|
||||
public static byte[] PARTITION_FILE_TABLE_SIGNATURE = new byte[] { 0x46, 0x53, 0x54, 0x00 }; // "FST"
|
||||
public final static int PARTITION_TOC_OFFSET = 0x800;
|
||||
public final static int PARTITION_TOC_ENTRY_SIZE = 0x80;
|
||||
|
||||
public static final String WUD_TMD_FILENAME = "title.tmd";
|
||||
public static final String WUD_TICKET_FILENAME = "title.tik";
|
||||
public static final String WUD_CERT_FILENAME = "title.cert";
|
||||
|
||||
private WUDInfoParser() {
|
||||
//
|
||||
}
|
||||
|
||||
public static WUDInfo createAndLoad(WUDDiscReader discReader, byte[] titleKey) throws IOException {
|
||||
WUDInfo result = new WUDInfo(titleKey, discReader);
|
||||
|
||||
byte[] PartitionTocBlock = discReader.readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET, 0, 0x8000, titleKey, null);
|
||||
|
||||
// verify DiscKey before proceeding
|
||||
if (!Arrays.equals(Arrays.copyOfRange(PartitionTocBlock, 0, 4), DECRYPTED_AREA_SIGNATURE)) {
|
||||
log.info("Decryption of PartitionTocBlock failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, WUDPartition> partitions = readPartitions(result, PartitionTocBlock);
|
||||
result.getPartitions().clear();
|
||||
result.getPartitions().putAll(partitions);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, WUDPartition> readPartitions(WUDInfo wudInfo, byte[] partitionTocBlock) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(partitionTocBlock.length);
|
||||
|
||||
buffer.order(ByteOrder.BIG_ENDIAN);
|
||||
buffer.put(partitionTocBlock);
|
||||
buffer.position(0);
|
||||
|
||||
int partitionCount = (int) ByteUtils.getUnsingedIntFromBytes(partitionTocBlock, 0x1C, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
Map<String, WUDPartition> partitions = new HashMap<>();
|
||||
|
||||
byte[] gamePartitionTMD = new byte[0];
|
||||
byte[] gamePartitionTicket = new byte[0];
|
||||
byte[] gamePartitionCert = new byte[0];
|
||||
|
||||
String realGamePartitionName = null;
|
||||
// populate partition information from decrypted TOC
|
||||
for (int i = 0; i < partitionCount; i++) {
|
||||
|
||||
int offset = (PARTITION_TOC_OFFSET + (i * PARTITION_TOC_ENTRY_SIZE));
|
||||
byte[] partitionIdentifier = Arrays.copyOfRange(partitionTocBlock, offset, offset + 0x19);
|
||||
int j = 0;
|
||||
for (j = 0; j < partitionIdentifier.length; j++) {
|
||||
if (partitionIdentifier[j] == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String partitionName = new String(Arrays.copyOfRange(partitionIdentifier, 0, j));
|
||||
|
||||
// calculate partition offset (relative from WIIU_DECRYPTED_AREA_OFFSET) from decrypted TOC
|
||||
long tmp = ByteUtils.getUnsingedIntFromBytes(partitionTocBlock, (PARTITION_TOC_OFFSET + (i * PARTITION_TOC_ENTRY_SIZE) + 0x20),
|
||||
ByteOrder.BIG_ENDIAN);
|
||||
|
||||
long partitionOffset = ((tmp * (long) 0x8000) - 0x10000);
|
||||
|
||||
WUDPartition partition = new WUDPartition(partitionName, partitionOffset);
|
||||
|
||||
if (partitionName.startsWith("SI")) {
|
||||
byte[] fileTableBlock = wudInfo.getWUDDiscReader().readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET + partitionOffset, 0, 0x8000,
|
||||
wudInfo.getTitleKey(), null);
|
||||
if (!Arrays.equals(Arrays.copyOfRange(fileTableBlock, 0, 4), PARTITION_FILE_TABLE_SIGNATURE)) {
|
||||
log.info("FST Decrpytion failed");
|
||||
continue;
|
||||
}
|
||||
|
||||
FST fst = FST.parseFST(fileTableBlock, null);
|
||||
|
||||
byte[] rawTIK = getFSTEntryAsByte(WUD_TICKET_FILENAME, partition, fst, wudInfo.getWUDDiscReader(), wudInfo.getTitleKey());
|
||||
byte[] rawTMD = getFSTEntryAsByte(WUD_TMD_FILENAME, partition, fst, wudInfo.getWUDDiscReader(), wudInfo.getTitleKey());
|
||||
byte[] rawCert = getFSTEntryAsByte(WUD_CERT_FILENAME, partition, fst, wudInfo.getWUDDiscReader(), wudInfo.getTitleKey());
|
||||
|
||||
gamePartitionTMD = rawTMD;
|
||||
gamePartitionTicket = rawTIK;
|
||||
gamePartitionCert = rawCert;
|
||||
|
||||
// We want to use the real game partition
|
||||
realGamePartitionName = partitionName = "GM" + Utils.ByteArrayToString(Arrays.copyOfRange(rawTIK, 0x1DC, 0x1DC + 0x08));
|
||||
} else if (partitionName.startsWith(realGamePartitionName)) {
|
||||
wudInfo.setGamePartitionName(partitionName);
|
||||
partition = new WUDGamePartition(partitionName, partitionOffset, gamePartitionTMD, gamePartitionCert, gamePartitionTicket);
|
||||
}
|
||||
byte[] header = wudInfo.getWUDDiscReader().readEncryptedToByteArray(partition.getPartitionOffset() + 0x10000, 0, 0x8000);
|
||||
WUDPartitionHeader partitionHeader = WUDPartitionHeader.parseHeader(header);
|
||||
partition.setPartitionHeader(partitionHeader);
|
||||
|
||||
partitions.put(partitionName, partition);
|
||||
}
|
||||
|
||||
return partitions;
|
||||
}
|
||||
|
||||
private static byte[] getFSTEntryAsByte(String filename, WUDPartition partition, FST fst, WUDDiscReader discReader, byte[] key) throws IOException {
|
||||
FSTEntry entry = getEntryByName(fst.getRoot(), filename);
|
||||
ContentFSTInfo info = fst.getContentFSTInfos().get((int) entry.getContentFSTID());
|
||||
|
||||
// Calculating the IV
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(0x10);
|
||||
byteBuffer.position(0x08);
|
||||
byte[] IV = byteBuffer.putLong(entry.getFileOffset() >> 16).array();
|
||||
|
||||
return discReader.readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET + (long) partition.getPartitionOffset() + (long) info.getOffset(),
|
||||
entry.getFileOffset(), (int) entry.getFileSize(), key, IV);
|
||||
}
|
||||
|
||||
private static FSTEntry getEntryByName(FSTEntry root, String name) {
|
||||
for (FSTEntry cur : root.getFileChildren()) {
|
||||
if (cur.getFilename().equals(name)) {
|
||||
return cur;
|
||||
}
|
||||
}
|
||||
for (FSTEntry cur : root.getDirChildren()) {
|
||||
FSTEntry dir_result = getEntryByName(cur, name);
|
||||
if (dir_result != null) {
|
||||
return dir_result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WUDPartition {
|
||||
private final String partitionName;
|
||||
private final long partitionOffset;
|
||||
|
||||
private WUDPartitionHeader partitionHeader;
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package de.mas.wiiu.jnus.implementations.wud.parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.utils.ByteUtils;
|
||||
import de.mas.wiiu.jnus.utils.HashUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class WUDPartitionHeader {
|
||||
@Getter @Setter private boolean calculatedHashes = false;
|
||||
@Getter private final HashMap<Short, byte[]> h3Hashes = new HashMap<>();
|
||||
@Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE) private byte[] rawData;
|
||||
|
||||
private WUDPartitionHeader() {
|
||||
}
|
||||
|
||||
// TODO: real processing. Currently we are ignoring everything except the hashes
|
||||
public static WUDPartitionHeader parseHeader(byte[] header) {
|
||||
WUDPartitionHeader result = new WUDPartitionHeader();
|
||||
result.setRawData(header);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void addH3Hashes(short index, byte[] hash) {
|
||||
getH3Hashes().put(index, hash);
|
||||
}
|
||||
|
||||
public byte[] getH3Hash(Content content) {
|
||||
if (content == null) {
|
||||
log.info("Can't find h3 hash, given content is null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return getH3Hashes().get(content.getIndex());
|
||||
}
|
||||
|
||||
public void calculateHashes(Map<Integer, Content> allContents) {
|
||||
byte[] header = getRawData();
|
||||
|
||||
// Calculating offset for the hashes
|
||||
int cnt = ByteUtils.getIntFromBytes(header, 0x10);
|
||||
int start_offset = 0x40 + cnt * 0x04;
|
||||
|
||||
int offset = 0;
|
||||
|
||||
// We have to make sure, that the list is ordered by index
|
||||
List<Content> contents = new ArrayList<>(allContents.values());
|
||||
Collections.sort(contents, new Comparator<Content>() {
|
||||
@Override
|
||||
public int compare(Content o1, Content o2) {
|
||||
return Short.compare(o1.getIndex(), o2.getIndex());
|
||||
}
|
||||
});
|
||||
|
||||
for (Content c : allContents.values()) {
|
||||
if (!c.isHashed() || !c.isEncrypted()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The encrypted content are splitted in 0x10000 chunk. For each 0x1000 chunk we need one entry in the h3
|
||||
int cnt_hashes = (int) (c.getEncryptedFileSize() / 0x10000 / 0x1000) + 1;
|
||||
|
||||
byte[] hash = Arrays.copyOfRange(header, start_offset + offset * 0x14, start_offset + (offset + cnt_hashes) * 0x14);
|
||||
|
||||
// Checking the hash of the h3 file.
|
||||
if (!Arrays.equals(HashUtil.hashSHA1(hash), c.getSHA2Hash())) {
|
||||
log.info("h3 incorrect from WUD");
|
||||
}
|
||||
|
||||
addH3Hashes(c.getIndex(), hash);
|
||||
offset += cnt_hashes;
|
||||
}
|
||||
|
||||
setCalculatedHashes(true);
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
package de.mas.wiiu.jnus.implementations.wud.reader;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.mas.wiiu.jnus.implementations.wud.WUDImage;
|
||||
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public abstract class WUDDiscReader {
|
||||
@Getter private final WUDImage image;
|
||||
|
||||
public WUDDiscReader(WUDImage image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public InputStream readEncryptedToInputStream(long offset, long size) throws IOException {
|
||||
PipedInputStream in = new PipedInputStream();
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
readEncryptedToOutputStream(out, offset, size);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
},"readEncryptedToInputStream@" + this.hashCode()).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
public byte[] readEncryptedToByteArray(long offset, long fileoffset, long size) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
readEncryptedToOutputStream(out, offset, size);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public InputStream readDecryptedToInputStream(long offset, long fileoffset, long size, byte[] key, byte[] iv) throws IOException {
|
||||
PipedInputStream in = new PipedInputStream();
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
readDecryptedToOutputStream(out, offset, fileoffset, size, key, iv);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
},"readDecryptedToInputStream@" + this.hashCode()).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
public byte[] readDecryptedToByteArray(long offset, long fileoffset, long size, byte[] key, byte[] iv) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
readDecryptedToOutputStream(out, offset, fileoffset, size, key, iv);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
protected abstract void readEncryptedToOutputStream(OutputStream out, long offset, long size) throws IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param readOffset
|
||||
* Needs to be aligned to 0x8000
|
||||
* @param key
|
||||
* @param IV
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public byte[] readDecryptedChunk(long readOffset, byte[] key, byte[] IV) throws IOException {
|
||||
int chunkSize = 0x8000;
|
||||
|
||||
byte[] encryptedChunk = readEncryptedToByteArray(readOffset, 0, chunkSize);
|
||||
byte[] decryptedChunk = new byte[chunkSize];
|
||||
|
||||
AESDecryption aesDecryption = new AESDecryption(key, IV);
|
||||
decryptedChunk = aesDecryption.decrypt(encryptedChunk);
|
||||
|
||||
return decryptedChunk;
|
||||
}
|
||||
|
||||
public void readDecryptedToOutputStream(OutputStream outputStream, long clusterOffset, long fileOffset, long size, byte[] key, byte[] IV)
|
||||
throws IOException {
|
||||
byte[] usedIV = IV;
|
||||
if (usedIV == null) {
|
||||
usedIV = new byte[0x10];
|
||||
}
|
||||
long usedSize = size;
|
||||
long usedFileOffset = fileOffset;
|
||||
byte[] buffer;
|
||||
|
||||
long maxCopySize;
|
||||
long copySize;
|
||||
|
||||
long readOffset;
|
||||
|
||||
int blockSize = 0x8000;
|
||||
long totalread = 0;
|
||||
|
||||
do {
|
||||
long blockNumber = (usedFileOffset / blockSize);
|
||||
long blockOffset = (usedFileOffset % blockSize);
|
||||
|
||||
readOffset = clusterOffset + (blockNumber * blockSize);
|
||||
// (long)WiiUDisc.WIIU_DECRYPTED_AREA_OFFSET + volumeOffset + clusterOffset + (blockStructure.getBlockNumber() * 0x8000);
|
||||
|
||||
buffer = readDecryptedChunk(readOffset, key, usedIV);
|
||||
maxCopySize = 0x8000 - blockOffset;
|
||||
copySize = (usedSize > maxCopySize) ? maxCopySize : usedSize;
|
||||
|
||||
outputStream.write(Arrays.copyOfRange(buffer, (int) blockOffset, (int) copySize));
|
||||
totalread += copySize;
|
||||
|
||||
// update counters
|
||||
usedSize -= copySize;
|
||||
usedFileOffset += copySize;
|
||||
} while (totalread < usedSize);
|
||||
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
public RandomAccessFile getRandomAccessFileStream() throws FileNotFoundException {
|
||||
if (getImage() == null || getImage().getFileHandle() == null) {
|
||||
log.warning("No image or image filehandle set.");
|
||||
System.exit(1); //TODO: NOOOOOOOOOOOOO
|
||||
}
|
||||
return new RandomAccessFile(getImage().getFileHandle(), "r");
|
||||
}
|
||||
}
|
@ -1,69 +1,73 @@
|
||||
package de.mas.jnus.lib.implementations.wud.reader;
|
||||
package de.mas.wiiu.jnus.implementations.wud.reader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.mas.jnus.lib.implementations.wud.WUDImage;
|
||||
import de.mas.jnus.lib.implementations.wud.WUDImageCompressedInfo;
|
||||
import lombok.extern.java.Log;
|
||||
import de.mas.wiiu.jnus.implementations.wud.WUDImage;
|
||||
import de.mas.wiiu.jnus.implementations.wud.WUDImageCompressedInfo;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class WUDDiscReaderCompressed extends WUDDiscReader{
|
||||
|
||||
public class WUDDiscReaderCompressed extends WUDDiscReader {
|
||||
|
||||
public WUDDiscReaderCompressed(WUDImage image) {
|
||||
super(image);
|
||||
}
|
||||
/**
|
||||
* Expects the .wux format by Exzap. You can more infos about it here.
|
||||
* https://gbatemp.net/threads/wii-u-image-wud-compression-tool.397901/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Expects the .wux format by Exzap. You can more infos about it here.
|
||||
* https://gbatemp.net/threads/wii-u-image-wud-compression-tool.397901/
|
||||
*/
|
||||
@Override
|
||||
protected void readEncryptedToOutputStream(OutputStream out, long offset, long size) throws IOException {
|
||||
// make sure there is no out-of-bounds read
|
||||
WUDImageCompressedInfo info = getImage().getCompressedInfo();
|
||||
|
||||
|
||||
long fileBytesLeft = info.getUncompressedSize() - offset;
|
||||
if( fileBytesLeft <= 0 ){
|
||||
|
||||
long usedOffset = offset;
|
||||
long usedSize = size;
|
||||
|
||||
if (fileBytesLeft <= 0) {
|
||||
log.warning("offset too big");
|
||||
System.exit(1);
|
||||
}
|
||||
if( fileBytesLeft < size ){
|
||||
size = fileBytesLeft;
|
||||
if (fileBytesLeft < usedSize) {
|
||||
usedSize = fileBytesLeft;
|
||||
}
|
||||
// compressed read must be handled on a per-sector level
|
||||
|
||||
|
||||
int bufferSize = 0x8000;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
|
||||
|
||||
RandomAccessFile input = getRandomAccessFileStream();
|
||||
while( size > 0 ){
|
||||
long sectorOffset = (offset % info.getSectorSize());
|
||||
while (usedSize > 0) {
|
||||
long sectorOffset = (usedOffset % info.getSectorSize());
|
||||
long remainingSectorBytes = info.getSectorSize() - sectorOffset;
|
||||
long sectorIndex = (offset / info.getSectorSize());
|
||||
int bytesToRead = (int) ((remainingSectorBytes<size)?remainingSectorBytes:size); // read only up to the end of the current sector
|
||||
long sectorIndex = (usedOffset / info.getSectorSize());
|
||||
int bytesToRead = (int) ((remainingSectorBytes < usedSize) ? remainingSectorBytes : usedSize); // read only up to the end of the current sector
|
||||
// look up real sector index
|
||||
long realSectorIndex = info.getSectorIndex((int) sectorIndex);
|
||||
long offset2 = info.getOffsetSectorArray() + realSectorIndex*info.getSectorSize()+sectorOffset;
|
||||
|
||||
long offset2 = info.getOffsetSectorArray() + realSectorIndex * info.getSectorSize() + sectorOffset;
|
||||
|
||||
input.seek(offset2);
|
||||
int read = input.read(buffer);
|
||||
if(read < 0) return;
|
||||
try{
|
||||
if (read < 0) return;
|
||||
try {
|
||||
out.write(Arrays.copyOfRange(buffer, 0, bytesToRead));
|
||||
}catch(IOException e){
|
||||
if(e.getMessage().equals("Pipe closed")){
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage().equals("Pipe closed")) {
|
||||
break;
|
||||
}else{
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
size -= bytesToRead;
|
||||
offset += bytesToRead;
|
||||
|
||||
usedSize -= bytesToRead;
|
||||
usedOffset += bytesToRead;
|
||||
}
|
||||
input.close();
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.mas.jnus.lib.implementations.wud.reader;
|
||||
package de.mas.wiiu.jnus.implementations.wud.reader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -6,90 +6,92 @@ import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.mas.jnus.lib.implementations.wud.WUDImage;
|
||||
import de.mas.wiiu.jnus.implementations.wud.WUDImage;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class WUDDiscReaderSplitted extends WUDDiscReader {
|
||||
public static long WUD_SPLITTED_FILE_SIZE = 0x100000L * 0x800L;
|
||||
public static long NUMBER_OF_FILES = 12;
|
||||
public static String WUD_SPLITTED_DEFAULT_FILEPATTERN = "game_part%d.wud";
|
||||
|
||||
public class WUDDiscReaderSplitted extends WUDDiscReader{
|
||||
public WUDDiscReaderSplitted(WUDImage image) {
|
||||
super(image);
|
||||
}
|
||||
public static long WUD_SPLITTED_FILE_SIZE = 0x100000L * 0x800L;
|
||||
public static long NUMBER_OF_FILES = 12;
|
||||
public static String WUD_SPLITTED_DEFAULT_FILEPATTERN = "game_part%d.wud";
|
||||
|
||||
|
||||
@Override
|
||||
protected void readEncryptedToOutputStream(OutputStream outputStream, long offset, long size) throws IOException {
|
||||
RandomAccessFile input = getFileByOffset(offset);
|
||||
|
||||
|
||||
int bufferSize = 0x8000;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
long totalread = 0;
|
||||
long curOffset = offset;
|
||||
|
||||
|
||||
int part = getFilePartByOffset(offset);
|
||||
long offsetInFile = getOffsetInFilePart(part, curOffset);
|
||||
|
||||
do{
|
||||
|
||||
do {
|
||||
offsetInFile = getOffsetInFilePart(part, curOffset);
|
||||
int curReadSize = bufferSize;
|
||||
if((offsetInFile+bufferSize) >= WUD_SPLITTED_FILE_SIZE){ //Will we read above the part?
|
||||
long toRead = WUD_SPLITTED_FILE_SIZE - offsetInFile;
|
||||
if(toRead == 0){ //just load the new file
|
||||
if ((offsetInFile + bufferSize) >= WUD_SPLITTED_FILE_SIZE) { // Will we read above the part?
|
||||
long toRead = WUD_SPLITTED_FILE_SIZE - offsetInFile;
|
||||
if (toRead == 0) { // just load the new file
|
||||
input.close();
|
||||
input = getFileByOffset(curOffset);
|
||||
part++;
|
||||
offsetInFile = getOffsetInFilePart(part, curOffset);
|
||||
}else{
|
||||
curReadSize = (int) toRead; //And first only read until the part ends
|
||||
} else {
|
||||
curReadSize = (int) toRead; // And first only read until the part ends
|
||||
}
|
||||
}
|
||||
|
||||
int read = input.read(buffer,0,curReadSize);
|
||||
if(read < 0) break;
|
||||
if(totalread + read > size){
|
||||
|
||||
int read = input.read(buffer, 0, curReadSize);
|
||||
if (read < 0) break;
|
||||
if (totalread + read > size) {
|
||||
read = (int) (size - totalread);
|
||||
}
|
||||
try{
|
||||
try {
|
||||
outputStream.write(Arrays.copyOfRange(buffer, 0, read));
|
||||
}catch(IOException e){
|
||||
if(e.getMessage().equals("Pipe closed")){
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage().equals("Pipe closed")) {
|
||||
break;
|
||||
}else{
|
||||
} else {
|
||||
input.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
totalread += read;
|
||||
curOffset += read;
|
||||
}while(totalread < size);
|
||||
|
||||
|
||||
} while (totalread < size);
|
||||
|
||||
input.close();
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
private int getFilePartByOffset(long offset){
|
||||
return (int) (offset/WUD_SPLITTED_FILE_SIZE)+1;
|
||||
|
||||
private int getFilePartByOffset(long offset) {
|
||||
return (int) (offset / WUD_SPLITTED_FILE_SIZE) + 1;
|
||||
}
|
||||
|
||||
private long getOffsetInFilePart(int part,long offset){
|
||||
return offset - ((long)(part-1) * WUD_SPLITTED_FILE_SIZE);
|
||||
|
||||
private long getOffsetInFilePart(int part, long offset) {
|
||||
return offset - ((long) (part - 1) * WUD_SPLITTED_FILE_SIZE);
|
||||
}
|
||||
|
||||
private RandomAccessFile getFileByOffset(long offset) throws IOException{
|
||||
File filehandlePart1 = new File(getImage().getFileHandle().getAbsolutePath()); //Create copy
|
||||
|
||||
private RandomAccessFile getFileByOffset(long offset) throws IOException {
|
||||
File filehandlePart1 = getImage().getFileHandle();
|
||||
String pathToFiles = filehandlePart1.getParentFile().getAbsolutePath();
|
||||
|
||||
|
||||
int filePart = getFilePartByOffset(offset);
|
||||
|
||||
|
||||
String filePartPath = pathToFiles + File.separator + String.format(WUD_SPLITTED_DEFAULT_FILEPATTERN, filePart);
|
||||
|
||||
|
||||
File part = new File(filePartPath);
|
||||
|
||||
if(!part.exists()){
|
||||
System.out.println("File does not exist");
|
||||
|
||||
if (!part.exists()) {
|
||||
log.info("File does not exist");
|
||||
return null;
|
||||
}
|
||||
RandomAccessFile result = new RandomAccessFile(part, "r");
|
||||
RandomAccessFile result = new RandomAccessFile(part, "r");
|
||||
result.seek(getOffsetInFilePart(filePart, offset));
|
||||
return result;
|
||||
}
|
@ -1,45 +1,48 @@
|
||||
package de.mas.jnus.lib.implementations.wud.reader;
|
||||
package de.mas.wiiu.jnus.implementations.wud.reader;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.mas.jnus.lib.implementations.wud.WUDImage;
|
||||
import de.mas.wiiu.jnus.implementations.wud.WUDImage;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
|
||||
public class WUDDiscReaderUncompressed extends WUDDiscReader {
|
||||
public WUDDiscReaderUncompressed(WUDImage image) {
|
||||
super(image);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void readEncryptedToOutputStream(OutputStream outputStream, long offset,long size) throws IOException{
|
||||
|
||||
protected void readEncryptedToOutputStream(OutputStream outputStream, long offset, long size) throws IOException {
|
||||
|
||||
FileInputStream input = new FileInputStream(getImage().getFileHandle());
|
||||
input.skip(offset);
|
||||
|
||||
StreamUtils.skipExactly(input, offset);
|
||||
|
||||
int bufferSize = 0x8000;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
long totalread = 0;
|
||||
do{
|
||||
do {
|
||||
int read = input.read(buffer);
|
||||
if(read < 0) break;
|
||||
if(totalread + read > size){
|
||||
if (read < 0) break;
|
||||
if (totalread + read > size) {
|
||||
read = (int) (size - totalread);
|
||||
}
|
||||
try{
|
||||
try {
|
||||
outputStream.write(Arrays.copyOfRange(buffer, 0, read));
|
||||
}catch(IOException e){
|
||||
if(e.getMessage().equals("Pipe closed")){
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage().equals("Pipe closed")) {
|
||||
break;
|
||||
}else{
|
||||
} else {
|
||||
input.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
totalread += read;
|
||||
}while(totalread < size);
|
||||
} while (totalread < size);
|
||||
input.close();
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
|
||||
}
|
25
src/de/mas/wiiu/jnus/utils/ByteArrayBuffer.java
Normal file
25
src/de/mas/wiiu/jnus/utils/ByteArrayBuffer.java
Normal file
@ -0,0 +1,25 @@
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class ByteArrayBuffer {
|
||||
@Getter public byte[] buffer;
|
||||
@Getter @Setter int lengthOfDataInBuffer;
|
||||
|
||||
public ByteArrayBuffer(int length) {
|
||||
buffer = new byte[(int) length];
|
||||
}
|
||||
|
||||
public int getSpaceLeft() {
|
||||
return buffer.length - getLengthOfDataInBuffer();
|
||||
}
|
||||
|
||||
public void addLengthOfDataInBuffer(int bytesRead) {
|
||||
lengthOfDataInBuffer += bytesRead;
|
||||
}
|
||||
|
||||
public void resetLengthOfDataInBuffer() {
|
||||
setLengthOfDataInBuffer(0);
|
||||
}
|
||||
}
|
27
src/de/mas/wiiu/jnus/utils/ByteArrayWrapper.java
Normal file
27
src/de/mas/wiiu/jnus/utils/ByteArrayWrapper.java
Normal file
@ -0,0 +1,27 @@
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class ByteArrayWrapper {
|
||||
private final byte[] data;
|
||||
|
||||
public ByteArrayWrapper(byte[] data) {
|
||||
if (data == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof ByteArrayWrapper)) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.equals(data, ((ByteArrayWrapper) other).data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(data);
|
||||
}
|
||||
}
|
85
src/de/mas/wiiu/jnus/utils/ByteUtils.java
Normal file
85
src/de/mas/wiiu/jnus/utils/ByteUtils.java
Normal file
@ -0,0 +1,85 @@
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class ByteUtils {
|
||||
|
||||
private ByteUtils() {
|
||||
// Utility Class
|
||||
}
|
||||
|
||||
public static int getIntFromBytes(byte[] input, int offset) {
|
||||
return getIntFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
|
||||
public static int getIntFromBytes(byte[] input, int offset, ByteOrder bo) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.order(bo);
|
||||
Arrays.copyOfRange(input, offset, offset + 4);
|
||||
buffer.put(Arrays.copyOfRange(input, offset, offset + 4));
|
||||
|
||||
return buffer.getInt(0);
|
||||
}
|
||||
|
||||
public static long getUnsingedIntFromBytes(byte[] input, int offset) {
|
||||
return getUnsingedIntFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
|
||||
public static long getUnsingedIntFromBytes(byte[] input, int offset, ByteOrder bo) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(8);
|
||||
buffer.order(bo);
|
||||
if (bo.equals(ByteOrder.BIG_ENDIAN)) {
|
||||
buffer.position(4);
|
||||
} else {
|
||||
buffer.position(0);
|
||||
}
|
||||
buffer.put(Arrays.copyOfRange(input, offset, offset + 4));
|
||||
|
||||
return buffer.getLong(0);
|
||||
}
|
||||
|
||||
public static long getLongFromBytes(byte[] input, int offset) {
|
||||
return getLongFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
|
||||
public static long getLongFromBytes(byte[] input, int offset, ByteOrder bo) {
|
||||
return ByteBuffer.wrap(Arrays.copyOfRange(input, offset, offset + 8)).order(bo).getLong(0);
|
||||
}
|
||||
|
||||
public static short getShortFromBytes(byte[] input, int offset) {
|
||||
return getShortFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
|
||||
public static short getShortFromBytes(byte[] input, int offset, ByteOrder bo) {
|
||||
return ByteBuffer.wrap(Arrays.copyOfRange(input, offset, offset + 2)).order(bo).getShort();
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromLong(long value) {
|
||||
return getBytesFromLong(value, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromLong(long value, ByteOrder bo) {
|
||||
byte[] result = new byte[0x08];
|
||||
ByteBuffer.allocate(8).order(bo).putLong(value).get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromInt(int value) {
|
||||
return getBytesFromInt(value, ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromInt(int value, ByteOrder bo) {
|
||||
byte[] result = new byte[0x04];
|
||||
ByteBuffer.allocate(4).order(bo).putInt(value).get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromShort(short value) {
|
||||
byte[] result = new byte[0x02];
|
||||
ByteBuffer.allocate(2).putShort(value).get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
20
src/de/mas/wiiu/jnus/utils/CheckSumWrongException.java
Normal file
20
src/de/mas/wiiu/jnus/utils/CheckSumWrongException.java
Normal file
@ -0,0 +1,20 @@
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class CheckSumWrongException extends Exception {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 5781223264453732269L;
|
||||
private final byte[] givenHash;
|
||||
private final byte[] expectedHash;
|
||||
|
||||
public CheckSumWrongException(String string, byte[] given, byte[] expected) {
|
||||
super(string);
|
||||
this.givenHash = given;
|
||||
this.expectedHash = expected;
|
||||
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.mas.jnus.lib.utils;
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@ -7,29 +7,32 @@ import java.io.InputStream;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
public class FileUtils {
|
||||
public final class FileUtils {
|
||||
private FileUtils() {
|
||||
// Utility Class
|
||||
}
|
||||
|
||||
public static boolean saveByteArrayToFile(String filePath,byte[] data) throws IOException {
|
||||
public static boolean saveByteArrayToFile(String filePath, byte[] data) throws IOException {
|
||||
File target = new File(filePath);
|
||||
if(target.isDirectory()){
|
||||
if (target.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
File parent = new File(target.getAbsolutePath()).getParentFile();
|
||||
if(parent != null){
|
||||
File parent = target.getParentFile();
|
||||
if (parent != null) {
|
||||
Utils.createDir(parent.getAbsolutePath());
|
||||
}
|
||||
return saveByteArrayToFile(target,data);
|
||||
return saveByteArrayToFile(target, data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves a byte array to a file (and overwrite it if its already exists)
|
||||
* DOES NOT IF THE PATH/FILE EXIST OR IS IT EVEN A FILE
|
||||
* Saves a byte array to a file (and overwrite it if its already exists) DOES NOT IF THE PATH/FILE EXIST OR IS IT EVEN A FILE
|
||||
*
|
||||
* @param output
|
||||
* @param data
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static boolean saveByteArrayToFile(@NonNull File output,byte[] data) throws IOException {
|
||||
public static boolean saveByteArrayToFile(@NonNull File output, byte[] data) throws IOException {
|
||||
FileOutputStream out = new FileOutputStream(output);
|
||||
out.write(data);
|
||||
out.close();
|
||||
@ -37,16 +40,15 @@ public class FileUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a byte array to a file (and overwrite it if its already exists)
|
||||
* DOES NOT IF THE PATH/FILE EXIST OR IS IT EVEN A FILE
|
||||
* Saves a byte array to a file (and overwrite it if its already exists) DOES NOT IF THE PATH/FILE EXIST OR IS IT EVEN A FILE
|
||||
*
|
||||
* @param output
|
||||
* @param inputStream
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void saveInputStreamToFile(@NonNull File output,InputStream inputStream,long filesize) throws IOException {
|
||||
|
||||
public static void saveInputStreamToFile(@NonNull File output, InputStream inputStream, long filesize) throws IOException {
|
||||
FileOutputStream out = new FileOutputStream(output);
|
||||
StreamUtils.saveInputStreamToOutputStream(inputStream,out,filesize);
|
||||
StreamUtils.saveInputStreamToOutputStream(inputStream, out, filesize);
|
||||
}
|
||||
|
||||
}
|
16
src/de/mas/wiiu/jnus/utils/HashResult.java
Normal file
16
src/de/mas/wiiu/jnus/utils/HashResult.java
Normal file
@ -0,0 +1,16 @@
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class HashResult {
|
||||
private final byte[] SHA1;
|
||||
private final byte[] MD5;
|
||||
private final byte[] CRC32;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HashResult [SHA1=" + Utils.ByteArrayToString(SHA1) + ", MD5=" + Utils.ByteArrayToString(MD5) + ", CRC32=" + Utils.ByteArrayToString(CRC32)
|
||||
+ "]";
|
||||
}
|
||||
}
|
265
src/de/mas/wiiu/jnus/utils/HashUtil.java
Normal file
265
src/de/mas/wiiu/jnus/utils/HashUtil.java
Normal file
@ -0,0 +1,265 @@
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class HashUtil {
|
||||
private HashUtil() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
public static byte[] hashSHA256(byte[] data) {
|
||||
MessageDigest sha256;
|
||||
try {
|
||||
sha256 = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
return new byte[0x20];
|
||||
}
|
||||
|
||||
return sha256.digest(data);
|
||||
}
|
||||
|
||||
public static byte[] hashSHA256(File file) {
|
||||
return hashSHA256(file, 0);
|
||||
}
|
||||
|
||||
// TODO: testing
|
||||
public static byte[] hashSHA256(File file, int aligmnent) {
|
||||
byte[] hash = new byte[0x20];
|
||||
MessageDigest sha1 = null;
|
||||
try {
|
||||
InputStream in = new FileInputStream(file);
|
||||
sha1 = MessageDigest.getInstance("SHA-256");
|
||||
hash = hash(sha1, in, file.length(), 0x8000, aligmnent);
|
||||
} catch (NoSuchAlgorithmException | FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public static byte[] hashSHA1(byte[] data) {
|
||||
MessageDigest sha1;
|
||||
try {
|
||||
sha1 = MessageDigest.getInstance("SHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
return new byte[0x14];
|
||||
}
|
||||
|
||||
return sha1.digest(data);
|
||||
}
|
||||
|
||||
public static byte[] hashSHA1(InputStream in, long length) {
|
||||
return hashSHA1(in, length, 0);
|
||||
}
|
||||
|
||||
public static byte[] hashSHA1(InputStream in, long length, int aligmnent) {
|
||||
byte[] hash = new byte[0x14];
|
||||
MessageDigest sha1 = null;
|
||||
try {
|
||||
sha1 = MessageDigest.getInstance("SHA1");
|
||||
hash = hash(sha1, in, length, 0x8000, aligmnent);
|
||||
} catch (NoSuchAlgorithmException | FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public static byte[] hashSHA1(File file) {
|
||||
return hashSHA1(file, 0);
|
||||
}
|
||||
|
||||
public static byte[] hashSHA1(File file, int aligmnent) {
|
||||
InputStream in;
|
||||
try {
|
||||
in = new FileInputStream(file);
|
||||
return hashSHA1(in, file.length(), aligmnent);
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static byte[] hash(MessageDigest digest, InputStream in, long inputSize1, int bufferSize, int alignment) throws IOException {
|
||||
long target_size = alignment == 0 ? inputSize1 : Utils.align(inputSize1, alignment);
|
||||
long cur_position = 0;
|
||||
int inBlockBufferRead = 0;
|
||||
byte[] blockBuffer = new byte[bufferSize];
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(bufferSize);
|
||||
do {
|
||||
inBlockBufferRead = StreamUtils.getChunkFromStream(in, blockBuffer, overflow, bufferSize);
|
||||
|
||||
if (inBlockBufferRead <= 0) break;
|
||||
|
||||
digest.update(blockBuffer, 0, inBlockBufferRead);
|
||||
cur_position += inBlockBufferRead;
|
||||
|
||||
} while (cur_position < target_size);
|
||||
long missing_bytes = target_size - cur_position;
|
||||
if(missing_bytes > 0){
|
||||
byte[] missing = new byte[(int) missing_bytes];
|
||||
digest.update(missing, 0, (int) missing_bytes);
|
||||
}
|
||||
|
||||
in.close();
|
||||
|
||||
return digest.digest();
|
||||
}
|
||||
|
||||
public static boolean compareHashFolder(File input1, File input2) {
|
||||
List<File> expectedFiles = getInputFilesForFolder(input1);
|
||||
List<File> givenFiles = getInputFilesForFolder(input2);
|
||||
String regexInput = input1.getAbsolutePath().toLowerCase();
|
||||
String regexOutput = input2.getAbsolutePath().toLowerCase();
|
||||
|
||||
List<File> exptectedFileWithAdjustedPath = new ArrayList<>();
|
||||
for (File f : expectedFiles) {
|
||||
String newPath = Utils.replaceStringInStringEscaped(f.getAbsolutePath().toLowerCase(), regexInput, regexOutput).toLowerCase();
|
||||
exptectedFileWithAdjustedPath.add(new File(newPath.toLowerCase()));
|
||||
}
|
||||
|
||||
if (!givenFiles.equals(exptectedFileWithAdjustedPath)) {
|
||||
log.info("Folder doesn't contain the same files.");
|
||||
List<File> additionalFileList = new ArrayList<>(givenFiles);
|
||||
List<File> missingFileList = new ArrayList<>(exptectedFileWithAdjustedPath);
|
||||
additionalFileList.removeAll(exptectedFileWithAdjustedPath);
|
||||
missingFileList.removeAll(givenFiles);
|
||||
if (!additionalFileList.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Additional files:\n");
|
||||
for (File f : additionalFileList) {
|
||||
sb.append(f.toString() + "\n");
|
||||
}
|
||||
log.info(sb.toString());
|
||||
}
|
||||
|
||||
if (!missingFileList.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Missing files:\n");
|
||||
for (File f : missingFileList) {
|
||||
sb.append(f.toString() + "\n");
|
||||
}
|
||||
log.info(sb.toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
for (File f : expectedFiles) {
|
||||
File expected = f;
|
||||
String newPath = Utils.replaceStringInStringEscaped(f.getAbsolutePath().toLowerCase(), regexInput, regexOutput).toLowerCase();
|
||||
File given = new File(newPath);
|
||||
if (!compareHashFile(expected, given)) {
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean compareHashFile(File file1, File file2) {
|
||||
if (file1 == null || !file1.exists() || file2 == null || !file2.exists()) {
|
||||
return false;
|
||||
}
|
||||
if (file1.isDirectory() && file2.isDirectory()) {
|
||||
return true;
|
||||
} else if (file1.isDirectory() || file2.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
byte[] hash1 = HashUtil.hashSHA1(file1);
|
||||
byte[] hash2 = HashUtil.hashSHA1(file2);
|
||||
boolean result = Arrays.equals(hash1, hash2);
|
||||
if (!result) {
|
||||
log.info("Hash doesn't match for " + file1.getAbsolutePath() + "(" + Utils.ByteArrayToString(hash1) + ") and " + file2.getAbsolutePath() + "("
|
||||
+ Utils.ByteArrayToString(hash2) + ")!");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<File> getInputFilesForFolder(File input1) {
|
||||
if (input1 == null || !input1.exists()) return new ArrayList<>();
|
||||
List<File> result = new ArrayList<>();
|
||||
for (File f : input1.listFiles()) {
|
||||
if (f.isDirectory()) {
|
||||
result.add(f);
|
||||
result.addAll(getInputFilesForFolder(f));
|
||||
} else {
|
||||
result.add(f);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void checkFileChunkHashes(byte[] hashes, byte[] h3Hashes, byte[] output, int block) throws CheckSumWrongException {
|
||||
int H0_start = (block % 16) * 20;
|
||||
int H1_start = (16 + (block / 16) % 16) * 20;
|
||||
int H2_start = (32 + (block / 256) % 16) * 20;
|
||||
int H3_start = ((block / 4096) % 16) * 20;
|
||||
|
||||
byte[] real_h0_hash = HashUtil.hashSHA1(output);
|
||||
byte[] expected_h0_hash = Arrays.copyOfRange(hashes, H0_start, H0_start + 20);
|
||||
|
||||
if (!Arrays.equals(real_h0_hash, expected_h0_hash)) {
|
||||
if (!Arrays.equals(expected_h0_hash, new byte[20])) { // Fix for /meta/WUP-N-HASP-EUR.bfma in 0005001b10059200
|
||||
throw new CheckSumWrongException("h0 checksumfail", real_h0_hash, expected_h0_hash);
|
||||
}
|
||||
} else {
|
||||
log.finest("h1 checksum right!");
|
||||
}
|
||||
|
||||
if ((block % 16) == 0) {
|
||||
byte[] expected_h1_hash = Arrays.copyOfRange(hashes, H1_start, H1_start + 20);
|
||||
byte[] real_h1_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes, H0_start, H0_start + (16 * 20)));
|
||||
|
||||
if (!Arrays.equals(expected_h1_hash, real_h1_hash)) {
|
||||
throw new CheckSumWrongException("h1 checksumfail", real_h1_hash, expected_h1_hash);
|
||||
} else {
|
||||
log.finer("h1 checksum right!");
|
||||
}
|
||||
}
|
||||
|
||||
if ((block % 256) == 0) {
|
||||
byte[] expected_h2_hash = Arrays.copyOfRange(hashes, H2_start, H2_start + 20);
|
||||
byte[] real_h2_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes, H1_start, H1_start + (16 * 20)));
|
||||
|
||||
if (!Arrays.equals(expected_h2_hash, real_h2_hash)) {
|
||||
throw new CheckSumWrongException("h2 checksumfail", real_h2_hash, expected_h2_hash);
|
||||
} else {
|
||||
log.fine("h2 checksum right!");
|
||||
}
|
||||
}
|
||||
|
||||
if (h3Hashes == null) {
|
||||
log.info("didn't check the h3, its missing.");
|
||||
return;
|
||||
}
|
||||
if ((block % 4096) == 0) {
|
||||
byte[] expected_h3_hash = Arrays.copyOfRange(h3Hashes, H3_start, H3_start + 20);
|
||||
byte[] real_h3_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes, H2_start, H2_start + (16 * 20)));
|
||||
|
||||
if (!Arrays.equals(expected_h3_hash, real_h3_hash)) {
|
||||
throw new CheckSumWrongException("h3 checksumfail", real_h3_hash, expected_h3_hash);
|
||||
} else {
|
||||
log.fine("h3 checksum right!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package de.mas.jnus.lib.utils;
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@ -10,118 +11,142 @@ import java.util.Arrays;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class StreamUtils {
|
||||
public static byte[] getBytesFromStream(InputStream in,int size) throws IOException{
|
||||
public final class StreamUtils {
|
||||
private StreamUtils() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromStream(InputStream in, int size) throws IOException {
|
||||
byte[] result = new byte[size];
|
||||
byte[] buffer = new byte[0x8000];
|
||||
int totalRead = 0;
|
||||
do{
|
||||
do {
|
||||
int read = in.read(buffer);
|
||||
if(read < 0) break;
|
||||
if (read < 0) break;
|
||||
System.arraycopy(buffer, 0, result, totalRead, read);
|
||||
totalRead += read;
|
||||
}while(totalRead < size);
|
||||
} while (totalRead < size);
|
||||
in.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int getChunkFromStream(InputStream inputStream,byte[] output, ByteArrayBuffer overflowbuffer,int BLOCKSIZE) throws IOException {
|
||||
|
||||
public static int getChunkFromStream(InputStream inputStream, byte[] output, ByteArrayBuffer overflowbuffer, int BLOCKSIZE) throws IOException {
|
||||
int bytesRead = -1;
|
||||
int inBlockBuffer = 0;
|
||||
byte[] overflowbuf = overflowbuffer.getBuffer();
|
||||
do{
|
||||
try{
|
||||
bytesRead = inputStream.read(overflowbuf,overflowbuffer.getLengthOfDataInBuffer(),overflowbuffer.getSpaceLeft());
|
||||
}catch(IOException e){
|
||||
if(!e.getMessage().equals("Write end dead")){
|
||||
do {
|
||||
try {
|
||||
bytesRead = inputStream.read(overflowbuf, overflowbuffer.getLengthOfDataInBuffer(), overflowbuffer.getSpaceLeft());
|
||||
|
||||
} catch (IOException e) {
|
||||
log.info(e.getMessage());
|
||||
if (!e.getMessage().equals("Write end dead")) {
|
||||
throw e;
|
||||
}
|
||||
bytesRead = -1;
|
||||
}
|
||||
if(bytesRead <= 0){
|
||||
if(overflowbuffer.getLengthOfDataInBuffer() > 0){
|
||||
|
||||
if (bytesRead <= 0) {
|
||||
if (overflowbuffer.getLengthOfDataInBuffer() > 0) {
|
||||
System.arraycopy(overflowbuf, 0, output, 0, overflowbuffer.getLengthOfDataInBuffer());
|
||||
inBlockBuffer = overflowbuffer.getLengthOfDataInBuffer();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
overflowbuffer.addLengthOfDataInBuffer(bytesRead);
|
||||
|
||||
if(inBlockBuffer + overflowbuffer.getLengthOfDataInBuffer() > BLOCKSIZE){
|
||||
|
||||
if (inBlockBuffer + overflowbuffer.getLengthOfDataInBuffer() > BLOCKSIZE) {
|
||||
int tooMuch = (inBlockBuffer + bytesRead) - BLOCKSIZE;
|
||||
int toRead = BLOCKSIZE - inBlockBuffer;
|
||||
|
||||
|
||||
System.arraycopy(overflowbuf, 0, output, inBlockBuffer, toRead);
|
||||
inBlockBuffer += toRead;
|
||||
|
||||
|
||||
System.arraycopy(overflowbuf, toRead, overflowbuf, 0, tooMuch);
|
||||
overflowbuffer.setLengthOfDataInBuffer(tooMuch);
|
||||
}else{
|
||||
System.arraycopy(overflowbuf, 0, output, inBlockBuffer, overflowbuffer.getLengthOfDataInBuffer());
|
||||
inBlockBuffer +=overflowbuffer.getLengthOfDataInBuffer();
|
||||
} else {
|
||||
System.arraycopy(overflowbuf, 0, output, inBlockBuffer, overflowbuffer.getLengthOfDataInBuffer());
|
||||
inBlockBuffer += overflowbuffer.getLengthOfDataInBuffer();
|
||||
overflowbuffer.resetLengthOfDataInBuffer();
|
||||
}
|
||||
}while(inBlockBuffer != BLOCKSIZE);
|
||||
} while (inBlockBuffer != BLOCKSIZE);
|
||||
return inBlockBuffer;
|
||||
}
|
||||
|
||||
|
||||
public static void saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException {
|
||||
saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null,0L);
|
||||
try {
|
||||
saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L);
|
||||
} catch (CheckSumWrongException e) {
|
||||
// Should never happen because the hash is not set. Lets print it anyway.
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream,long filesize, byte[] hash,long expectedSizeForHash) throws IOException {
|
||||
MessageDigest sha1 = null;
|
||||
if(hash != null){
|
||||
public static void saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream, long filesize, byte[] hash,
|
||||
long expectedSizeForHash) throws IOException, CheckSumWrongException {
|
||||
MessageDigest sha1 = null;
|
||||
if (hash != null) {
|
||||
try {
|
||||
sha1 = MessageDigest.getInstance("SHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int BUFFER_SIZE = 0x8000;
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read = 0;
|
||||
long totalRead = 0;
|
||||
long written = 0;
|
||||
do{
|
||||
do {
|
||||
read = inputStream.read(buffer);
|
||||
if(read < 0){
|
||||
if (read < 0) {
|
||||
break;
|
||||
}
|
||||
totalRead +=read;
|
||||
|
||||
if(totalRead > filesize){
|
||||
totalRead += read;
|
||||
|
||||
if (totalRead > filesize) {
|
||||
read = (int) (read - (totalRead - filesize));
|
||||
}
|
||||
|
||||
|
||||
outputStream.write(buffer, 0, read);
|
||||
written += read;
|
||||
|
||||
if(sha1 != null){
|
||||
sha1.update(buffer,0,read);
|
||||
|
||||
if (sha1 != null) {
|
||||
sha1.update(buffer, 0, read);
|
||||
}
|
||||
}while(written < filesize);
|
||||
|
||||
if(sha1 != null){
|
||||
long missingInHash = expectedSizeForHash - written;
|
||||
if(missingInHash > 0){
|
||||
} while (written < filesize);
|
||||
|
||||
if (sha1 != null && hash != null) {
|
||||
long missingInHash = expectedSizeForHash - written;
|
||||
if (missingInHash > 0) {
|
||||
sha1.update(new byte[(int) missingInHash]);
|
||||
}
|
||||
|
||||
|
||||
byte[] calculated_hash = sha1.digest();
|
||||
byte[] expected_hash = hash;
|
||||
if(!Arrays.equals(calculated_hash, expected_hash)){
|
||||
log.info(Utils.ByteArrayToString(calculated_hash));
|
||||
log.info(Utils.ByteArrayToString(expected_hash));
|
||||
log.info("Hash doesn't match saves output stream.");
|
||||
}else{
|
||||
//log.warning("Hash DOES match saves output stream.");
|
||||
if (!Arrays.equals(calculated_hash, expected_hash)) {
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
throw new CheckSumWrongException("Hash doesn't match saves output stream.", calculated_hash, expected_hash);
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.close();
|
||||
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
}
|
||||
|
||||
public static void skipExactly(InputStream in, long offset) throws IOException {
|
||||
long n = offset;
|
||||
while (n != 0) {
|
||||
long skipped = in.skip(n);
|
||||
if (skipped == 0) {
|
||||
in.close();
|
||||
throw new EOFException();
|
||||
}
|
||||
n -= skipped;
|
||||
}
|
||||
}
|
||||
}
|
149
src/de/mas/wiiu/jnus/utils/Utils.java
Normal file
149
src/de/mas/wiiu/jnus/utils/Utils.java
Normal file
@ -0,0 +1,149 @@
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import org.xml.sax.ErrorHandler;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.SAXParseException;
|
||||
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public final class Utils {
|
||||
private Utils() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
public static long align(long numToRound, int multiple) {
|
||||
if ((multiple > 0) && ((multiple & (multiple - 1)) == 0)) {
|
||||
return alignPower2(numToRound, multiple);
|
||||
} else {
|
||||
return alignGeneric(numToRound, multiple);
|
||||
}
|
||||
}
|
||||
|
||||
private static long alignGeneric(long numToRound, int multiple) {
|
||||
int isPositive = 0;
|
||||
if (numToRound >= 0) {
|
||||
isPositive = 1;
|
||||
}
|
||||
return ((numToRound + isPositive * (multiple - 1)) / multiple) * multiple;
|
||||
}
|
||||
|
||||
private static long alignPower2(long numToRound, int multiple) {
|
||||
if (!((multiple > 0) && ((multiple & (multiple - 1)) == 0))) return 0L;
|
||||
return (numToRound + (multiple - 1)) & ~(multiple - 1);
|
||||
}
|
||||
|
||||
public static String ByteArrayToString(byte[] ba) {
|
||||
if (ba == null) return null;
|
||||
StringBuilder hex = new StringBuilder(ba.length * 2);
|
||||
for (byte b : ba) {
|
||||
hex.append(String.format("%02X", b));
|
||||
}
|
||||
return hex.toString();
|
||||
}
|
||||
|
||||
public static byte[] StringToByteArray(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public static boolean createDir(String path) {
|
||||
if (path == null || path.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
File pathFile = new File(path);
|
||||
if (!pathFile.exists()) {
|
||||
boolean success = new File(path).mkdirs();
|
||||
if (!success) {
|
||||
log.fine("Creating directory \"" + path + "\" failed.");
|
||||
return false;
|
||||
}
|
||||
} else if (!pathFile.isDirectory()) {
|
||||
log.info("\"" + path + "\" already exists but is no directoy");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void deleteDir(File path) {
|
||||
if (path == null || !path.exists()) return;
|
||||
for (File file : path.listFiles()) {
|
||||
if (file.isDirectory()) deleteDir(file);
|
||||
file.delete();
|
||||
}
|
||||
path.delete();
|
||||
}
|
||||
|
||||
public static String replaceStringInStringEscaped(String input, String replace, String replaceWith) {
|
||||
return input.replaceAll(Pattern.quote(replace), Matcher.quoteReplacement(replaceWith));
|
||||
}
|
||||
|
||||
public static boolean checkXML(InputSource in) {
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setValidating(false);
|
||||
factory.setNamespaceAware(true);
|
||||
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
|
||||
builder.setErrorHandler(new ErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void warning(SAXParseException exception) throws SAXException {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(SAXParseException exception) throws SAXException {
|
||||
throw exception;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fatalError(SAXParseException exception) throws SAXException {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
builder.parse(in);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean checkXML(InputStream in) {
|
||||
return checkXML(new InputSource(in));
|
||||
}
|
||||
|
||||
public static boolean checkXML(byte[] data) {
|
||||
return checkXML(new ByteArrayInputStream(data));
|
||||
}
|
||||
|
||||
public static boolean checkXML(File file) {
|
||||
return checkXML(new InputSource(file.getAbsolutePath()));
|
||||
}
|
||||
|
||||
public static void sleep(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.mas.jnus.lib.utils;
|
||||
package de.mas.wiiu.jnus.utils;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
@ -13,8 +13,10 @@ import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import com.sun.istack.internal.NotNull;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class XMLParser {
|
||||
private Document document;
|
||||
|
||||
@ -43,7 +45,7 @@ public class XMLParser {
|
||||
}
|
||||
public Node getNodeByValue(String element,int index){
|
||||
if(document == null){
|
||||
System.out.println("Please load the document first.");
|
||||
log.info("Please load the document first.");
|
||||
}
|
||||
NodeList list = document.getElementsByTagName(element);
|
||||
if(list == null){
|
||||
@ -55,23 +57,23 @@ public class XMLParser {
|
||||
public String getValueOfElementAttribute(String element,int index,String attribute){
|
||||
Node node = getNodeByValue(element, index);
|
||||
if(node == null){
|
||||
//System.out.println("Node is null");
|
||||
//log.info("Node is null");
|
||||
return "";
|
||||
}
|
||||
return getAttributeValueFromNode(node,attribute);
|
||||
}
|
||||
|
||||
public static String getAttributeValueFromNode(@NotNull Node element,String attribute){
|
||||
public static String getAttributeValueFromNode(@NonNull Node element,String attribute){
|
||||
return element.getAttributes().getNamedItem(attribute).getTextContent().toString();
|
||||
}
|
||||
|
||||
public String getValueOfElement(String element,int index){
|
||||
Node node = getNodeByValue(element, index);
|
||||
if(node == null){
|
||||
//System.out.println("Node is null");
|
||||
//log.info("Node is null");
|
||||
return "";
|
||||
}
|
||||
|
||||
return node.getTextContent().toString();
|
||||
return node.getTextContent().toString();
|
||||
}
|
||||
}
|
71
src/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java
Normal file
71
src/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java
Normal file
@ -0,0 +1,71 @@
|
||||
package de.mas.wiiu.jnus.utils.cryptography;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class AESDecryption {
|
||||
private Cipher cipher;
|
||||
|
||||
@Getter @Setter private byte[] AESKey;
|
||||
@Getter @Setter private byte[] IV;
|
||||
|
||||
public AESDecryption(byte[] AESKey, byte[] IV) {
|
||||
try {
|
||||
cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
setAESKey(AESKey);
|
||||
setIV(IV);
|
||||
init();
|
||||
}
|
||||
|
||||
protected final void init() {
|
||||
init(getAESKey(), getIV());
|
||||
}
|
||||
|
||||
protected void init(byte[] decryptedKey, byte[] iv) {
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES");
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] input) {
|
||||
try {
|
||||
return cipher.doFinal(input);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(2);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] input, int len) {
|
||||
return decrypt(input, 0, len);
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] input, int offset, int len) {
|
||||
try {
|
||||
return cipher.doFinal(input, offset, len);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(2);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
}
|
184
src/de/mas/wiiu/jnus/utils/cryptography/NUSDecryption.java
Normal file
184
src/de/mas/wiiu/jnus/utils/cryptography/NUSDecryption.java
Normal file
@ -0,0 +1,184 @@
|
||||
package de.mas.wiiu.jnus.utils.cryptography;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.utils.ByteArrayBuffer;
|
||||
import de.mas.wiiu.jnus.utils.CheckSumWrongException;
|
||||
import de.mas.wiiu.jnus.utils.HashUtil;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
|
||||
public class NUSDecryption extends AESDecryption {
|
||||
public NUSDecryption(byte[] AESKey, byte[] IV) {
|
||||
super(AESKey, IV);
|
||||
}
|
||||
|
||||
public NUSDecryption(Ticket ticket) {
|
||||
this(ticket.getDecryptedKey(), ticket.getIV());
|
||||
}
|
||||
|
||||
private byte[] decryptFileChunk(byte[] blockBuffer, int BLOCKSIZE, byte[] IV) {
|
||||
return decryptFileChunk(blockBuffer, 0, BLOCKSIZE, IV);
|
||||
}
|
||||
|
||||
private byte[] decryptFileChunk(byte[] blockBuffer, int offset, int BLOCKSIZE, byte[] IV) {
|
||||
if (IV != null) {
|
||||
setIV(IV);
|
||||
init();
|
||||
}
|
||||
return decrypt(blockBuffer, offset, BLOCKSIZE);
|
||||
}
|
||||
|
||||
public void decryptFileStream(InputStream inputStream, OutputStream outputStream, long filesize, short contentIndex, byte[] h3hash,
|
||||
long expectedSizeForHash) throws IOException, CheckSumWrongException {
|
||||
MessageDigest sha1 = null;
|
||||
MessageDigest sha1fallback = null;
|
||||
if (h3hash != null) {
|
||||
try {
|
||||
sha1 = MessageDigest.getInstance("SHA1");
|
||||
sha1fallback = MessageDigest.getInstance("SHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
int BLOCKSIZE = 0x8000;
|
||||
// long dlFileLength = filesize;
|
||||
// if(dlFileLength > (dlFileLength/BLOCKSIZE)*BLOCKSIZE){
|
||||
// dlFileLength = ((dlFileLength/BLOCKSIZE)*BLOCKSIZE) +BLOCKSIZE;
|
||||
// }
|
||||
|
||||
byte[] IV = new byte[0x10];
|
||||
|
||||
IV[0] = (byte) ((contentIndex >> 8) & 0xFF);
|
||||
IV[1] = (byte) (contentIndex);
|
||||
|
||||
byte[] blockBuffer = new byte[BLOCKSIZE];
|
||||
|
||||
int inBlockBuffer;
|
||||
long written = 0;
|
||||
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
|
||||
|
||||
do {
|
||||
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, BLOCKSIZE);
|
||||
|
||||
byte[] output = decryptFileChunk(blockBuffer, (int) Utils.align(inBlockBuffer, 16), IV);
|
||||
|
||||
IV = Arrays.copyOfRange(blockBuffer, BLOCKSIZE - 16, BLOCKSIZE);
|
||||
|
||||
int toWrite = inBlockBuffer;
|
||||
if ((written + inBlockBuffer) > filesize) {
|
||||
toWrite = (int) (filesize - written);
|
||||
}
|
||||
|
||||
written += toWrite;
|
||||
|
||||
outputStream.write(output, 0, toWrite);
|
||||
|
||||
if (sha1 != null && sha1fallback != null) {
|
||||
sha1.update(output, 0, toWrite);
|
||||
sha1fallback.update(output, 0, toWrite);
|
||||
}
|
||||
} while (inBlockBuffer == BLOCKSIZE);
|
||||
|
||||
if (sha1 != null && sha1fallback != null) {
|
||||
long missingInHash = expectedSizeForHash - written;
|
||||
if (missingInHash > 0) {
|
||||
sha1fallback.update(new byte[(int) missingInHash]);
|
||||
}
|
||||
|
||||
byte[] calculated_hash1 = sha1.digest();
|
||||
byte[] calculated_hash2 = sha1fallback.digest();
|
||||
byte[] expected_hash = h3hash;
|
||||
if (!Arrays.equals(calculated_hash1, expected_hash) && !Arrays.equals(calculated_hash2, expected_hash)) {
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
throw new CheckSumWrongException("hash checksum failed", calculated_hash1, expected_hash);
|
||||
} else {
|
||||
// log.warning("Hash DOES match saves output stream.");
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
}
|
||||
|
||||
public void decryptFileStreamHashed(InputStream inputStream, OutputStream outputStream, long filesize, long fileoffset, short contentIndex, byte[] h3Hash)
|
||||
throws IOException, CheckSumWrongException {
|
||||
int BLOCKSIZE = 0x10000;
|
||||
int HASHBLOCKSIZE = 0xFC00;
|
||||
|
||||
long writeSize = HASHBLOCKSIZE;
|
||||
|
||||
long block = (fileoffset / HASHBLOCKSIZE);
|
||||
long soffset = fileoffset - (fileoffset / HASHBLOCKSIZE * HASHBLOCKSIZE);
|
||||
|
||||
if (soffset + filesize > writeSize) writeSize = writeSize - soffset;
|
||||
|
||||
byte[] encryptedBlockBuffer = new byte[BLOCKSIZE];
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
|
||||
|
||||
long wrote = 0;
|
||||
int inBlockBuffer;
|
||||
do {
|
||||
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, encryptedBlockBuffer, overflow, BLOCKSIZE);
|
||||
|
||||
if (writeSize > filesize) writeSize = filesize;
|
||||
|
||||
byte[] output;
|
||||
try {
|
||||
output = decryptFileChunkHash(encryptedBlockBuffer, (int) block, contentIndex, h3Hash);
|
||||
} catch (CheckSumWrongException e) {
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
throw e;
|
||||
}
|
||||
|
||||
if ((wrote + writeSize) > filesize) {
|
||||
writeSize = (int) (filesize - wrote);
|
||||
}
|
||||
|
||||
outputStream.write(output, (int) (0 + soffset), (int) writeSize);
|
||||
|
||||
wrote += writeSize;
|
||||
|
||||
block++;
|
||||
|
||||
if (soffset > 0) {
|
||||
writeSize = HASHBLOCKSIZE;
|
||||
soffset = 0;
|
||||
}
|
||||
} while (wrote < filesize && (inBlockBuffer == BLOCKSIZE));
|
||||
// System.out.println("Decryption okay");
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
}
|
||||
|
||||
private byte[] decryptFileChunkHash(byte[] blockBuffer, int block, int contentIndex, byte[] h3_hashes) throws CheckSumWrongException {
|
||||
int hashSize = 0x400;
|
||||
int blocksize = 0xFC00;
|
||||
byte[] IV = ByteBuffer.allocate(16).putShort((short) contentIndex).array();
|
||||
|
||||
byte[] hashes = decryptFileChunk(blockBuffer, hashSize, IV);
|
||||
|
||||
hashes[0] ^= (byte) ((contentIndex >> 8) & 0xFF);
|
||||
hashes[1] ^= (byte) (contentIndex & 0xFF);
|
||||
|
||||
int H0_start = (block % 16) * 20;
|
||||
|
||||
IV = Arrays.copyOfRange(hashes, H0_start, H0_start + 16);
|
||||
byte[] output = decryptFileChunk(blockBuffer, hashSize, blocksize, IV);
|
||||
|
||||
HashUtil.checkFileChunkHashes(hashes, h3_hashes, output, block);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
@ -1,36 +1,43 @@
|
||||
package de.mas.jnus.lib.utils.download;
|
||||
package de.mas.wiiu.jnus.utils.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
public class Downloader {
|
||||
public static byte[] downloadFileToByteArray(String fileURL) throws IOException {
|
||||
int BUFFER_SIZE = 0x800;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public abstract class Downloader {
|
||||
public Downloader() {
|
||||
//
|
||||
}
|
||||
|
||||
public static byte[] downloadFileToByteArray(String fileURL) throws IOException {
|
||||
int BUFFER_SIZE = 0x800;
|
||||
URL url = new URL(fileURL);
|
||||
HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
|
||||
int responseCode = httpConn.getResponseCode();
|
||||
|
||||
|
||||
byte[] file = null;
|
||||
|
||||
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
int contentLength = httpConn.getContentLength();
|
||||
|
||||
|
||||
file = new byte[contentLength];
|
||||
|
||||
|
||||
InputStream inputStream = httpConn.getInputStream();
|
||||
|
||||
|
||||
int bytesRead = -1;
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int filePostion = 0;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
System.arraycopy(buffer, 0, file, filePostion,bytesRead);
|
||||
filePostion+=bytesRead;
|
||||
System.arraycopy(buffer, 0, file, filePostion, bytesRead);
|
||||
filePostion += bytesRead;
|
||||
}
|
||||
inputStream.close();
|
||||
}else{
|
||||
System.out.println("File not found: " + fileURL);
|
||||
} else {
|
||||
log.info("File not found: " + fileURL);
|
||||
}
|
||||
httpConn.disconnect();
|
||||
return file;
|
69
src/de/mas/wiiu/jnus/utils/download/NUSDownloadService.java
Normal file
69
src/de/mas/wiiu/jnus/utils/download/NUSDownloadService.java
Normal file
@ -0,0 +1,69 @@
|
||||
package de.mas.wiiu.jnus.utils.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
|
||||
public final class NUSDownloadService extends Downloader {
|
||||
private static NUSDownloadService defaultInstance = new NUSDownloadService(Settings.URL_BASE);
|
||||
private static Map<String, NUSDownloadService> instances = new HashMap<>();
|
||||
|
||||
private final String URL_BASE;
|
||||
|
||||
private NUSDownloadService(String URL) {
|
||||
this.URL_BASE = URL;
|
||||
}
|
||||
|
||||
public static NUSDownloadService getDefaultInstance() {
|
||||
return defaultInstance;
|
||||
}
|
||||
|
||||
public static NUSDownloadService getInstance(String URL) {
|
||||
if (!instances.containsKey(URL)) {
|
||||
NUSDownloadService instance = new NUSDownloadService(URL);
|
||||
instances.put(URL, instance);
|
||||
}
|
||||
return instances.get(URL);
|
||||
}
|
||||
|
||||
public byte[] downloadTMDToByteArray(long titleID, int version) throws IOException {
|
||||
String version_suf = "";
|
||||
if (version > Settings.LATEST_TMD_VERSION) version_suf = "." + version;
|
||||
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd" + version_suf;
|
||||
return downloadFileToByteArray(URL);
|
||||
}
|
||||
|
||||
public byte[] downloadTicketToByteArray(long titleID) throws IOException {
|
||||
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk";
|
||||
return downloadFileToByteArray(URL);
|
||||
}
|
||||
|
||||
public byte[] downloadToByteArray(String url) throws IOException {
|
||||
String URL = URL_BASE + "/" + url;
|
||||
return downloadFileToByteArray(URL);
|
||||
}
|
||||
|
||||
public InputStream getInputStream(String URL, long offset) throws IOException {
|
||||
URL url_obj = new URL(URL);
|
||||
HttpURLConnection connection = (HttpURLConnection) url_obj.openConnection();
|
||||
connection.setRequestProperty("Range", "bytes=" + offset + "-");
|
||||
try {
|
||||
connection.connect();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return connection.getInputStream();
|
||||
}
|
||||
|
||||
public InputStream getInputStreamForURL(String url, long offset) throws IOException {
|
||||
String URL = URL_BASE + "/" + url;
|
||||
|
||||
return getInputStream(URL, offset);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user