Code clean up, changed to a maven project

This commit is contained in:
Maschell 2017-05-17 19:20:42 +02:00
parent b332dc1bcd
commit 14c987456d
94 changed files with 4567 additions and 3575 deletions

View File

@ -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
View File

@ -1 +1,2 @@
/bin/
/target/

View File

@ -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
View 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
View 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>

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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) + "]";
}
}

View File

@ -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) + "]";
}
}

View File

@ -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 + "]";
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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() {
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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!");
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View 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();
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package de.mas.wiiu.jnus;
import java.io.Closeable;
public interface InputStreamWithException extends Closeable {
public void checkForException() throws Exception;
}

View 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 + "]";
}
}

View 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;
}

View 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);
}

View 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());
}
}

View 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());
}
}

View 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());
}
}

View 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());
}
}

View 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;
}
}
}
}

View File

@ -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";

View 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;
}
}

View 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; //
}
}

View 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) + "]";
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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) + "]";

View 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;
}
}

View File

@ -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;
}
}

View 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();
}
}

View File

@ -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;
}

View File

@ -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 + "]";
}
}

View File

@ -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
}
}

View 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 + "]";
}
}

View File

@ -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];
}
}

View File

@ -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;
}

View 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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View 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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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);
}
}

View 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)
+ "]";
}
}

View 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!");
}
}
}
}

View File

@ -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;
}
}
}

View 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) {
}
}
}

View File

@ -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();
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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;

View 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);
}
}